diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml new file mode 100644 index 000000000..93f30fd37 --- /dev/null +++ b/.github/workflows/android-release.yml @@ -0,0 +1,307 @@ +# Builds the signed release APK when an `android-v*` tag is pushed and attaches +# it to the matching GitHub Release. +# +# The signing key lives in the repository secrets, never in the repository: +# ANDROID_KEYSTORE_BASE64 base64 of release.keystore (`base64 -w0 release.keystore`) +# ANDROID_KEYSTORE_PASSWORD store password +# ANDROID_KEY_ALIAS key alias inside the keystore +# ANDROID_KEY_PASSWORD key password +# Secrets are only passed as environment variables into the build step; the +# workflow never echoes them. The decoded key lives in $RUNNER_TEMP and is +# removed by an `if: always()` step, so a failed build leaves nothing behind. +# Both jobs run in the `android-release` environment: it needs required +# reviewers, so a stray tag cannot sign or publish an APK on its own. All +# actions are pinned to commit SHAs (same rule as publish.yml/verify-pr.yml), +# and checkout keeps no credentials in .git/config for the Gradle build. +# Locally the same key is used through OMPWEB_SIGNING_PROPERTIES (see AGENTS.md, +# "Android app"). +name: Android release + +on: + push: + tags: + - "android-v*" + +permissions: + contents: read + +env: + # The certificate this repository's users verify against (docs/android/README.md). + # A different keystore must not sign a release silently; rotate this value + # together with the published docs. The build job compares the APK against it + # before staging, the publish job again before the upload — one constant for + # both, so the two checks cannot drift apart. + RELEASE_CERT_SHA256: d73354b52dc7c3f0c74623c439ddb7b52e252811c6e6b5f851c9d04b7015fc87 + # The build-tools the release pipeline pins: `aapt2` and `apksigner` are taken + # from exactly this directory instead of "latest found under build-tools". + RELEASE_BUILD_TOOLS: "37.0.0" + +jobs: + build: + runs-on: ubuntu-latest + environment: android-release + defaults: + run: + working-directory: android + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1 + with: + distribution: temurin + java-version: "21" + cache: gradle + + # The action's default package list asks sdkmanager for "tools", which the + # SDK repository no longer serves; only install what this job needs. + - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + with: + packages: platform-tools + + - name: Install SDK packages + run: sdkmanager "platforms;android-37.0" "build-tools;37.0.0" + + - name: Make the wrapper executable + run: chmod +x gradlew + + - name: Decode the signing key + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + set -euo pipefail + signing_dir="$RUNNER_TEMP/ompweb-signing" + mkdir -p "$signing_dir" + chmod 700 "$signing_dir" + printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$signing_dir/release.keystore" + chmod 600 "$signing_dir/release.keystore" + # Written with printf from env vars; GitHub masks the values in logs. + { + printf 'storeFile=%s\n' "$signing_dir/release.keystore" + printf 'storePassword=%s\n' "$ANDROID_KEYSTORE_PASSWORD" + printf 'keyAlias=%s\n' "$ANDROID_KEY_ALIAS" + printf 'keyPassword=%s\n' "$ANDROID_KEY_PASSWORD" + } > "$signing_dir/signing.properties" + chmod 600 "$signing_dir/signing.properties" + + - name: Assemble the signed release + env: + OMPWEB_SIGNING_PROPERTIES: ${{ runner.temp }}/ompweb-signing/signing.properties + run: ./gradlew assembleRelease --no-daemon + + - name: Verify the build and stage the release files + id: verify + run: | + set -euo pipefail + + # The tag names the release, the APK's manifest names the build. A + # tag on an un-bumped tree would publish ompweb-1.0.1.apk containing + # versionName 1.0.0 and the in-app updater would offer it forever, so + # refuse to publish a mismatch. + version="${GITHUB_REF_NAME#android-v}" + if [ -z "$version" ] || [ "$version" = "$GITHUB_REF_NAME" ]; then + echo "::error::$GITHUB_REF_NAME is not an android-v tag" + exit 1 + fi + # The tag string is attacker-controlled by anyone who can push a tag, + # and it ends up in a file name, a release body and a URL: accept only + # a plain dotted version, never a path or shell metacharacter. + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::$GITHUB_REF_NAME is not an android-v.. tag" + exit 1 + fi + + # The release build names its output after the version + # (`outputFileName` in app/build.gradle.kts), not app-release.apk. + apk="app/build/outputs/apk/release/ompweb-$version.apk" + test -f "$apk" + # The exact build-tools the workflow installs, not whatever another + # SDK package left behind in the runner image. + aapt2="$ANDROID_HOME/build-tools/$RELEASE_BUILD_TOOLS/aapt2" + test -x "$aapt2" + apk_version="$("$aapt2" dump badging "$apk" | + sed -n "s/^package:.* versionName='\([^']*\)'.*/\1/p")" + if [ "$apk_version" != "$version" ]; then + echo "::error::Tag android-v$version does not match the APK's versionName '$apk_version'" + exit 1 + fi + echo "versionName $apk_version matches $GITHUB_REF_NAME" + + out_dir="$RUNNER_TEMP/ompweb-release" + rm -rf "$out_dir" + mkdir -p "$out_dir" + asset="$out_dir/ompweb-$version.apk" + sha_file="$asset.sha256" + cp "$apk" "$asset" + + apksigner="$ANDROID_HOME/build-tools/$RELEASE_BUILD_TOOLS/apksigner" + test -x "$apksigner" + certs="$("$apksigner" verify --print-certs "$asset")" + printf '%s\n' "$certs" + expected_cert_sha256="$RELEASE_CERT_SHA256" + test -n "$expected_cert_sha256" + case "$certs" in + *"SHA-256 digest: $expected_cert_sha256"*) ;; + *) + echo "::error::The APK is not signed with the expected release certificate" + exit 1 + ;; + esac + + # Relative filename so `sha256sum -c ompweb-.apk.sha256` + # works after downloading both files into the same directory. + ( cd "$out_dir" && sha256sum "ompweb-$version.apk" ) > "$sha_file" + cat "$sha_file" + ls -l "$asset" + echo "size=$(wc -c < "$asset" | tr -d ' ')" >> "$GITHUB_OUTPUT" + echo "sha256=$(cut -d' ' -f1 < "$sha_file")" >> "$GITHUB_OUTPUT" + echo "cert_sha256=$expected_cert_sha256" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Prepare release notes + env: + VERSION: ${{ steps.verify.outputs.version }} + SIZE: ${{ steps.verify.outputs.size }} + SHA256: ${{ steps.verify.outputs.sha256 }} + CERT_SHA256: ${{ steps.verify.outputs.cert_sha256 }} + run: | + set -euo pipefail + notes="../docs/android/RELEASE_NOTES_${VERSION}.md" + if [ ! -f "$notes" ]; then + # Never publish another version's checksum in this release's body. + echo "::error::Missing $notes; add the release notes for $VERSION before tagging" + exit 1 + fi + body="$RUNNER_TEMP/ompweb-release/release-notes.md" + cp "$notes" "$body" + # The notes usually carry their own "Verify the APK" block. Append the + # generated one only when the built APK's SHA-256 is missing there, so + # the release body never states the same checksum twice. A notes file + # naming a *different* SHA-256 fails the build instead of shipping two + # contradicting hashes. + note_hashes="$(grep -i 'sha-256' "$notes" | grep -oE '[0-9a-fA-F]{64}' | sort -u || true)" + if [ -n "$note_hashes" ] && ! grep -qx "$SHA256" <<< "$note_hashes"; then + echo "::error::$notes contains a SHA-256 that does not match the built APK" + exit 1 + fi + if ! grep -qx "$SHA256" <<< "$note_hashes"; then + printf '\n---\n\n**Verify this download:** `ompweb-%s.apk`, %s bytes, SHA-256 `%s`, signing certificate SHA-256 `%s`.\n' \ + "$VERSION" "$SIZE" "$SHA256" "$CERT_SHA256" >> "$body" + fi + cat "$body" + + - name: Remove the decoded signing key + if: always() + run: | + set -euo pipefail + signing_dir="$RUNNER_TEMP/ompweb-signing" + rm -rf "$signing_dir" + if [ -e "$signing_dir" ]; then + echo "::error::The decoded signing key still exists at $signing_dir" + exit 1 + fi + echo "removed $signing_dir" + + - name: Upload build artifacts + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: android-release + path: ${{ runner.temp }}/ompweb-release/ + if-no-files-found: ignore + + - name: Upload build reports + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: android-release-reports + path: android/app/build/reports/ + if-no-files-found: ignore + + publish: + needs: build + runs-on: ubuntu-latest + environment: android-release + permissions: + contents: write + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: android-release + path: dist + + # The action's default package list asks sdkmanager for "tools", which the + # SDK repository no longer serves; only install what this job needs. + - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + with: + packages: platform-tools + + # The same build-tools the build job used: `aapt2` and `apksigner` must be + # the pinned ones, not whatever the runner image happens to ship. + - name: Install the SDK tools the verification needs + run: sdkmanager "build-tools;$RELEASE_BUILD_TOOLS" + + - name: Resolve the staged release files + id: staged + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#android-v}" + # Same validation as the build job: the tag reaches a file name here. + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::$GITHUB_REF_NAME is not an android-v.. tag" + exit 1 + fi + asset="dist/ompweb-$version.apk" + test -f "$asset" + test -f "$asset.sha256" + test -f dist/release-notes.md + echo "asset=$asset" >> "$GITHUB_OUTPUT" + echo "sha_file=$asset.sha256" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Re-verify the staged APK + env: + ASSET: ${{ steps.staged.outputs.asset }} + VERSION: ${{ steps.staged.outputs.version }} + run: | + set -euo pipefail + # The artifact travelled through storage between the two jobs. Publish + # nothing unless this job can re-derive the claims the build made: + # checksum, versionName and signing certificate. + recorded="$(cut -d' ' -f1 < "$ASSET.sha256")" + actual="$(sha256sum "$ASSET" | cut -d' ' -f1)" + if [ -z "$recorded" ] || [ "$recorded" != "$actual" ]; then + echo "::error::The staged APK does not match its recorded SHA-256" + exit 1 + fi + aapt2="$ANDROID_HOME/build-tools/$RELEASE_BUILD_TOOLS/aapt2" + apksigner="$ANDROID_HOME/build-tools/$RELEASE_BUILD_TOOLS/apksigner" + test -x "$aapt2" + test -x "$apksigner" + apk_version="$("$aapt2" dump badging "$ASSET" | + sed -n "s/^package:.* versionName='\([^']*\)'.*/\1/p")" + if [ "$apk_version" != "$VERSION" ]; then + echo "::error::The staged APK reports versionName '$apk_version', expected '$VERSION'" + exit 1 + fi + certs="$("$apksigner" verify --print-certs "$ASSET")" + case "$certs" in + *"SHA-256 digest: $RELEASE_CERT_SHA256"*) ;; + *) + echo "::error::The staged APK is not signed with the expected release certificate" + exit 1 + ;; + esac + echo "staged APK verified: versionName $apk_version, SHA-256 $actual" + + - name: Upload the release APK + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 + with: + files: | + ${{ steps.staged.outputs.asset }} + ${{ steps.staged.outputs.sha_file }} + body_path: dist/release-notes.md diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 000000000..71b68ab9d --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,58 @@ +name: Android + +on: + push: + paths: + - "android/**" + - ".github/workflows/android.yml" + pull_request: + paths: + - "android/**" + - ".github/workflows/android.yml" + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: android + steps: + # Every action is pinned to a commit SHA (same rule as + # android-release.yml/publish.yml/verify-pr.yml). + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1 + with: + distribution: temurin + java-version: "21" + cache: gradle + + # The action's default package list asks sdkmanager for "tools", which the + # SDK repository no longer serves; only install what this job needs. + - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + with: + packages: platform-tools + + - name: Install SDK packages + run: sdkmanager "platforms;android-37.0" "build-tools;37.0.0" + + - name: Make the wrapper executable + run: chmod +x gradlew + + - name: Assemble, test and lint + run: ./gradlew assembleDebug testDebugUnitTest lintDebug --no-daemon + + - name: Upload test and lint reports + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: android-reports + path: | + android/app/build/reports/ + android/app/build/test-results/ + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 9ea957aaa..f9a721b42 100644 --- a/.gitignore +++ b/.gitignore @@ -24,13 +24,25 @@ coverage/ # rendered / generated artifacts (mockups, screenshots, images) /output/ *.png.png -screenshots/ +# Root-only: the Android app's committed test goldens live in +# android/app/src/test/screenshots and must survive a fresh clone. +/screenshots/ /mockups/ # misc .DS_Store *.pem +# Signing material must never be committed, in any directory. android/.gitignore +# repeats this for the app module; these rules also cover the repo root and any +# other worktree. +*.jks +*.keystore +*.p12 +*.bks +signing.properties +keystore.properties + # debug .logs/ *.log diff --git a/AGENTS.md b/AGENTS.md index 197270781..e5e22078b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -768,6 +768,42 @@ during the wait. targets are accepted). The worker navigates non-app windows to the session URL; the app falls back to that URL when `/api/sessions` cannot be read. +## Android app + +The native Android client lives in [`android/`](./android): Kotlin, Jetpack +Compose, one `app` module. User documentation is +[`docs/android/README.md`](./docs/android/README.md); the wire contract it +implements is [`docs/android/protocol.md`](./docs/android/protocol.md). + +- Build with JDK 21: `cd android && ./gradlew assembleDebug`. Unit tests + (including the golden screenshots): `./gradlew testDebugUnitTest`; lint: + `./gradlew lintDebug`. `.github/workflows/android.yml` runs those three on + every change under `android/`. +- The screenshot goldens live in `android/app/src/test/screenshots/` (30 cells + per surface: five widths, light plus one dark theme, three font scales) and + are rendered through Robolectric on the JVM. Re-record them with + `./gradlew recordScreenshots`; review every changed image before committing. +- Never send `Origin` or `Sec-Fetch-Site` from the app: ompweb's origin gate + answers those headers with 403, so `ServerProbe` is the reference request. +- Plain HTTP is allowed only for loopback, RFC 1918, `100.64.0.0/10`, + link-local, `.local`, `.ts.net` and single-label hosts (`ServerAddress`, + unit-tested). `HttpPolicyInterceptor` enforces the same policy on every + request and redirect hop, and an `EventListener` additionally requires every + resolved peer address to be loopback/private. +- The session cookie is the only persisted credential: `EncryptedCookieRecords` + keeps it AES-GCM-encrypted under `filesDir/` with a Keystore key, and + `OriginBoundCookieJar` hands it to exactly one origin. The password is never + stored or logged; `allowBackup` stays `false` and the backup rules exclude + app data. +- Release signing is opt-in: set `OMPWEB_SIGNING_PROPERTIES` to a properties + file with `storeFile`, `storePassword`, `keyAlias` and `keyPassword` before + `assembleRelease`. CI uses the `ANDROID_KEYSTORE_BASE64`, + `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS` and `ANDROID_KEY_PASSWORD` + secrets in the `android-release` environment. Never commit signing material. +- Onboarding order matters: reachability (`/api/sessions`) → `/api/app-info` → + password → `/api/app-info` again, because the auth gate runs before routing. + The app requires a server with `/api/app-info` (`"api": 1`). + ## Git history and PR landing rules - **Never rewrite `main`'s history** — no force-push, `filter-branch`, rebase diff --git a/README.md b/README.md index 7e7fec4d2..62a7ee553 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,19 @@ support. Local Chromium verification covered cookie-gated metadata with HTTP icon URLs blocked (zero installability errors). The owner also confirmed that installation on a physical phone works as expected behind Cloudflare Access. +## Android app + +A native Android client lives in [`android/`](./android) and is distributed as +an APK on [GitHub Releases](https://github.com/kahme247/ompweb/releases) (tags +`android-v*`). It needs a server with `/api/app-info` (API 1; ompweb 0.5.2 and +older predate that endpoint) and Android 8.0+; Tailscale is recommended for +access from anywhere. Its update check reads the public releases list and the +APK it downloads is verified (SHA-256, package name, `versionCode`, signing +certificate) before Android's installer sees it. + +Installation, first connection, notifications, troubleshooting, privacy and +APK verification are documented in [`docs/android/README.md`](./docs/android/README.md). + Skill startup notices require an OMP runtime with `get_skill_diagnostics`, `set_skill_startup_diagnostics`, and `skill_diagnostics_update`. Conflicts and redundant installations appear above the composer when its OMP session starts; diff --git a/android/.gitattributes b/android/.gitattributes new file mode 100644 index 000000000..6e4180cdf --- /dev/null +++ b/android/.gitattributes @@ -0,0 +1,3 @@ +# Windows batch scripts need CRLF in the working tree; the POSIX wrapper stays LF. +*.bat text eol=crlf +gradlew text eol=lf diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 000000000..1a23e2711 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,28 @@ +# Gradle +.gradle/ +.kotlin/ +build/ +**/build/ + +# Local machine configuration +local.properties +*.iml + +# IDE +.idea/ +.cxx/ +captures/ + +# Signing material must never be committed. +*.jks +*.keystore +*.p12 +*.pem +*.bks +signing.properties +# Property files may carry a password; gradle.properties and the Gradle wrapper +# property file carry no secrets and are required by a fresh clone, so keep the +# ignore scoped to the directories that can hold local secrets. +/app/**/*.properties +/*.properties +!/gradle.properties diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 000000000..6de7c6a62 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,208 @@ +import java.io.File +import java.util.Properties +import org.gradle.api.tasks.testing.Test +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) +} + +// Release signing is opt-in: point OMPWEB_SIGNING_PROPERTIES at a properties +// file with storeFile, storePassword, keyAlias and keyPassword. Without it, +// release builds stay unsigned and are only useful for compile checks. +val signingPropertiesPath: String? = System.getenv("OMPWEB_SIGNING_PROPERTIES") +val releaseSigning = signingPropertiesPath?.takeIf { it.isNotBlank() }?.let { path -> + val file = File(path) + require(file.isFile) { "OMPWEB_SIGNING_PROPERTIES points at a missing file: $path" } + Properties().apply { file.inputStream().use(::load) } +} + +// One place for the release version. versionCode scheme: +// MAJOR * 10000 + MINOR * 100 + PATCH (1.0.11 -> 10011). It is a monotonically +// increasing integer, never reused, so every release installs over the +// previous one. The update server's latest.json sidecar is generated from +// these two values by writeReleaseSidecar. +val ompWebVersionName = "1.0.17" +val ompWebVersionCode = 10017 + +android { + namespace = "io.github.kahme247.ompweb" + compileSdk = 37 + buildToolsVersion = "37.0.0" + + defaultConfig { + applicationId = "io.github.kahme247.ompweb" + minSdk = 26 + targetSdk = 37 + versionCode = ompWebVersionCode + versionName = ompWebVersionName + } + + buildTypes { + debug { + applicationIdSuffix = ".debug" + versionNameSuffix = "-debug" + } + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + if (releaseSigning != null) { + signingConfig = signingConfigs.create("release") { + storeFile = File(releaseSigning.getProperty("storeFile")) + storePassword = releaseSigning.getProperty("storePassword") + keyAlias = releaseSigning.getProperty("keyAlias") + keyPassword = releaseSigning.getProperty("keyPassword") + } + } + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + } + + lint { + abortOnError = true + checkReleaseBuilds = true + } + + testOptions { + unitTests { + // Robolectric JPEG/PNG-backed Compose screenshots need the merged + // resources and manifest of the debug variant on the test classpath. + isIncludeAndroidResources = true + } + } + + packaging { + resources.excludes += "/META-INF/{AL2.0,LGPL2.1}" + } +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 + } +} + +// Name the release APK after the version and write the sidecar the internal +// update feed (`latest.json`) derives versionName/versionCode from. Both live +// next to the APK under build/outputs/apk/release/. +androidComponents { + onVariants(selector().withBuildType("release")) { variant -> + variant.outputs.forEach { output -> + output.outputFileName.set("ompweb-$ompWebVersionName.apk") + } + } +} + +val writeReleaseSidecar = tasks.register("writeReleaseSidecar") { + group = "build" + description = "Writes ompweb-.apk.json next to the release APK." + val outputDirectory = layout.buildDirectory.dir("outputs/apk/release") + outputs.file(outputDirectory.map { it.file("ompweb-$ompWebVersionName.apk.json") }) + doLast { + val sidecar = outputDirectory.get().file("ompweb-$ompWebVersionName.apk.json").asFile + sidecar.parentFile.mkdirs() + sidecar.writeText( + "{\"versionName\":\"$ompWebVersionName\",\"versionCode\":$ompWebVersionCode}\n", + ) + } +} + +tasks.matching { it.name == "assembleRelease" }.configureEach { + finalizedBy(writeReleaseSidecar) +} + +// ---------------------------------------------------------------- screenshots +// +// The hardening matrix renders composables through Robolectric's native Skia +// backend and compares the pixels against committed goldens under +// app/src/test/screenshots/. `verifyScreenshots` is a gate task; a normal +// `testDebugUnitTest` run verifies too. `recordScreenshots` rewrites the +// goldens (or set OMPWEB_SCREENSHOT_RECORD=1 / -Pscreenshot.record=true). +val screenshotFilter = "io.github.kahme247.ompweb.screenshot.*" +val requestedTasks = gradle.startParameter.taskNames.map { it.substringAfterLast(':') } +val screenshotTasksRequested = requestedTasks.any { it == "verifyScreenshots" || it == "recordScreenshots" } +val screenshotRecordingRequested = requestedTasks.contains("recordScreenshots") + +tasks.withType().configureEach { + val record = when { + screenshotTasksRequested -> screenshotRecordingRequested + else -> providers.gradleProperty("screenshot.record").orNull == "true" || + System.getenv("OMPWEB_SCREENSHOT_RECORD") == "1" + } + systemProperty("ompweb.screenshot.record", record.toString()) + if (screenshotTasksRequested) { + filter { includeTestsMatching(screenshotFilter) } + } + // Robolectric's native graphics runtime reflects into these packages on + // JDK 17+; without the opens/exports flags it aborts before the first + // render (SharedSecrets is needed for the app file descriptors). + jvmArgs( + "--add-opens=java.base/java.io=ALL-UNNAMED", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/jdk.internal.access=ALL-UNNAMED", + "--add-exports=java.base/jdk.internal.access=ALL-UNNAMED", + ) + maxHeapSize = "3g" +} + +tasks.register("verifyScreenshots") { + group = "verification" + description = "Verifies the golden screenshot matrix (JVM, no device)." + dependsOn(tasks.named("testDebugUnitTest")) +} + +tasks.register("recordScreenshots") { + group = "verification" + description = "Records/updates the golden screenshots from the current UI." + dependsOn(tasks.named("testDebugUnitTest")) +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.lifecycle.process) + implementation(libs.androidx.core.splashscreen) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.core) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.datastore.preferences) + implementation(libs.okhttp) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.coil.compose) + implementation(libs.coil.network.okhttp) + implementation(libs.coil.svg) + implementation(libs.latex.renderer) + implementation(libs.latex.fonts.katex) + implementation(libs.androidx.webkit) + implementation(libs.androidx.browser) + + testImplementation(libs.junit) + testImplementation(libs.okhttp.mockwebserver) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(platform(libs.androidx.compose.bom)) + testImplementation(libs.androidx.compose.ui.test.junit4) + testImplementation(libs.robolectric) + testImplementation(libs.androidx.test.core) + testImplementation(libs.androidx.test.ext.junit) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 000000000..899ebafd1 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,35 @@ +# R8 rules for the release build. Debug builds do not run R8, so every rule +# here exists for minified builds only; verify changes with `assembleRelease` +# plus a live smoke test (Mermaid, images, chat) on a device. +# +# kotlinx.serialization ships consumer rules for its generated serializers; +# the app calls serializer() explicitly, so no keep rule for model classes is +# needed. Keep the metadata the library reflects on. +-keepattributes *Annotation*, InnerClasses, Signature +-dontnote kotlinx.serialization.** + +# OkHttp ships the needed rules in its own artifact; it probes optional TLS +# providers that are not on the classpath. +-dontwarn okhttp3.** +-dontwarn okio.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** + +# Mermaid renders inside a sandboxed WebView and calls back through a bridge +# object that is only reachable from JavaScript. R8 cannot see those calls and +# would otherwise strip or rename postMessage(). +-keepclassmembers class * { + @android.webkit.JavascriptInterface ; +} +-keepattributes JavascriptInterface + +# Coil resolves decoders/fetchers through ServiceLoader next to the ones the +# app registers explicitly (SVG + OkHttp). Keep both extensions intact; both +# are small and reach into androidx/androidsvg plumbing. +-keep class coil3.svg.** { *; } +-keep class coil3.network.okhttp.** { *; } +-dontwarn coil3.** + +# Compose and its runtime ship consumer rules. The app owns no reflective +# entry points besides the WebView bridge above. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..308fa8794 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/assets/mermaid.min.js b/android/app/src/main/assets/mermaid.min.js new file mode 100644 index 000000000..79b89d7cb --- /dev/null +++ b/android/app/src/main/assets/mermaid.min.js @@ -0,0 +1,3636 @@ +"use strict";var __esbuild_esm_mermaid_nm;(__esbuild_esm_mermaid_nm||={}).mermaid=(()=>{var SGe=Object.create;var Ix=Object.defineProperty;var wGe=Object.getOwnPropertyDescriptor;var kGe=Object.getOwnPropertyNames;var EGe=Object.getPrototypeOf,AGe=Object.prototype.hasOwnProperty;var s=(e,t)=>Ix(e,"name",{value:t,configurable:!0});var F=(e,t)=>()=>(e&&(t=e(e=0)),t);var As=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),sr=(e,t)=>{for(var r in t)Ix(e,r,{get:t[r],enumerable:!0})},OX=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of kGe(t))!AGe.call(e,i)&&i!==r&&Ix(e,i,{get:()=>t[i],enumerable:!(n=wGe(t,i))||n.enumerable});return e};var Rs=(e,t,r)=>(r=e!=null?SGe(EGe(e)):{},OX(t||!e||!e.__esModule?Ix(r,"default",{value:e,enumerable:!0}):r,e)),RGe=e=>OX(Ix({},"__esModule",{value:!0}),e);var _Ge,B0,uD,BX,fw=F(()=>{"use strict";_Ge=Object.freeze({left:0,top:0,width:16,height:16}),B0=Object.freeze({rotate:0,vFlip:!1,hFlip:!1}),uD=Object.freeze({..._Ge,...B0}),BX=Object.freeze({...uD,body:"",hidden:!1})});var LGe,$X,FX=F(()=>{"use strict";fw();LGe=Object.freeze({width:null,height:null}),$X=Object.freeze({...LGe,...B0})});var hD,pw,GX=F(()=>{"use strict";hD=s((e,t,r,n="")=>{let i=e.split(":");if(e.slice(0,1)==="@"){if(i.length<2||i.length>3)return null;n=i.shift().slice(1)}if(i.length>3||!i.length)return null;if(i.length>1){let l=i.pop(),u=i.pop(),h={provider:i.length>0?i[0]:n,prefix:u,name:l};return t&&!pw(h)?null:h}let a=i[0],o=a.split("-");if(o.length>1){let l={provider:n,prefix:o.shift(),name:o.join("-")};return t&&!pw(l)?null:l}if(r&&n===""){let l={provider:n,prefix:"",name:a};return t&&!pw(l,r)?null:l}return null},"stringToIcon"),pw=s((e,t)=>e?!!((t&&e.prefix===""||e.prefix)&&e.name):!1,"validateIconName")});function zX(e,t){let r={};!e.hFlip!=!t.hFlip&&(r.hFlip=!0),!e.vFlip!=!t.vFlip&&(r.vFlip=!0);let n=((e.rotate||0)+(t.rotate||0))%4;return n&&(r.rotate=n),r}var VX=F(()=>{"use strict";s(zX,"mergeIconTransformations")});function dD(e,t){let r=zX(e,t);for(let n in BX)n in B0?n in e&&!(n in r)&&(r[n]=B0[n]):n in t?r[n]=t[n]:n in e&&(r[n]=e[n]);return r}var WX=F(()=>{"use strict";fw();VX();s(dD,"mergeIconData")});function qX(e,t){let r=e.icons,n=e.aliases||Object.create(null),i=Object.create(null);function a(o){if(r[o])return i[o]=[];if(!(o in i)){i[o]=null;let l=n[o]&&n[o].parent,u=l&&a(l);u&&(i[o]=[l].concat(u))}return i[o]}return s(a,"resolve"),(t||Object.keys(r).concat(Object.keys(n))).forEach(a),i}var HX=F(()=>{"use strict";s(qX,"getIconsTree")});function UX(e,t,r){let n=e.icons,i=e.aliases||Object.create(null),a={};function o(l){a=dD(n[l]||i[l],a)}return s(o,"parse"),o(t),r.forEach(o),dD(e,a)}function fD(e,t){if(e.icons[t])return UX(e,t,[]);let r=qX(e,[t])[t];return r?UX(e,t,r):null}var YX=F(()=>{"use strict";WX();HX();s(UX,"internalGetIconData");s(fD,"getIconData")});function pD(e,t,r){if(t===1)return e;if(r=r||100,typeof e=="number")return Math.ceil(e*t*r)/r;if(typeof e!="string")return e;let n=e.split(DGe);if(n===null||!n.length)return e;let i=[],a=n.shift(),o=IGe.test(a);for(;;){if(o){let l=parseFloat(a);isNaN(l)?i.push(a):i.push(Math.ceil(l*t*r)/r)}else i.push(a);if(a=n.shift(),a===void 0)return i.join("");o=!o}}var DGe,IGe,jX=F(()=>{"use strict";DGe=/(-?[0-9.]*[0-9]+[0-9.]*)/g,IGe=/^-?[0-9.]*[0-9]+[0-9.]*$/g;s(pD,"calculateSize")});function MGe(e,t="defs"){let r="",n=e.indexOf("<"+t);for(;n>=0;){let i=e.indexOf(">",n),a=e.indexOf("",a);if(o===-1)break;r+=e.slice(i+1,a).trim(),e=e.slice(0,n).trim()+e.slice(o+1)}return{defs:r,content:e}}function NGe(e,t){return e?""+e+""+t:t}function XX(e,t,r){let n=MGe(e);return NGe(n.defs,t+n.content+r)}var KX=F(()=>{"use strict";s(MGe,"splitSVGDefs");s(NGe,"mergeDefsAndContent");s(XX,"wrapSVGContent")});function mD(e,t){let r={...uD,...e},n={...$X,...t},i={left:r.left,top:r.top,width:r.width,height:r.height},a=r.body;[r,n].forEach(y=>{let v=[],x=y.hFlip,b=y.vFlip,T=y.rotate;x?b?T+=2:(v.push("translate("+(i.width+i.left).toString()+" "+(0-i.top).toString()+")"),v.push("scale(-1 1)"),i.top=i.left=0):b&&(v.push("translate("+(0-i.left).toString()+" "+(i.height+i.top).toString()+")"),v.push("scale(1 -1)"),i.top=i.left=0);let w;switch(T<0&&(T-=Math.floor(T/4)*4),T=T%4,T){case 1:w=i.height/2+i.top,v.unshift("rotate(90 "+w.toString()+" "+w.toString()+")");break;case 2:v.unshift("rotate(180 "+(i.width/2+i.left).toString()+" "+(i.height/2+i.top).toString()+")");break;case 3:w=i.width/2+i.left,v.unshift("rotate(-90 "+w.toString()+" "+w.toString()+")");break}T%2===1&&(i.left!==i.top&&(w=i.left,i.left=i.top,i.top=w),i.width!==i.height&&(w=i.width,i.width=i.height,i.height=w)),v.length&&(a=XX(a,'',""))});let o=n.width,l=n.height,u=i.width,h=i.height,d,f;o===null?(f=l===null?"1em":l==="auto"?h:l,d=pD(f,u/h)):(d=o==="auto"?u:o,f=l===null?pD(d,h/u):l==="auto"?h:l);let p={},m=s((y,v)=>{PGe(v)||(p[y]=v.toString())},"setAttr");m("width",d),m("height",f);let g=[i.left,i.top,u,h];return p.viewBox=g.join(" "),{attributes:p,viewBox:g,body:a}}var PGe,ZX=F(()=>{"use strict";fw();FX();jX();KX();PGe=s(e=>e==="unset"||e==="undefined"||e==="none","isUnsetKeyword");s(mD,"iconToSVG")});function gD(e,t=BGe){let r=[],n;for(;n=OGe.exec(e);)r.push(n[1]);if(!r.length)return e;let i="suffix"+(Math.random()*16777216|Date.now()).toString(16);return r.forEach(a=>{let o=typeof t=="function"?t(a):t+($Ge++).toString(),l=a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");e=e.replace(new RegExp('([#;"])('+l+')([")]|\\.[a-z])',"g"),"$1"+o+i+"$3")}),e=e.replace(new RegExp(i,"g"),""),e}var OGe,BGe,$Ge,QX=F(()=>{"use strict";OGe=/\sid="(\S+)"/g,BGe="IconifyId"+Date.now().toString(16)+(Math.random()*16777216|0).toString(16),$Ge=0;s(gD,"replaceIDs")});function yD(e,t){let r=e.indexOf("xlink:")===-1?"":' xmlns:xlink="http://www.w3.org/1999/xlink"';for(let n in t)r+=" "+n+'="'+t[n]+'"';return'"+e+""}var JX=F(()=>{"use strict";s(yD,"iconToHTML")});var eK=F(()=>{"use strict";GX();YX();ZX();QX();JX()});var vD,ii,$0=F(()=>{"use strict";vD=s((e,t,{depth:r=2}={})=>{let n={depth:r};if(Array.isArray(t)&&!Array.isArray(e))return t.forEach(i=>vD(e,i,n)),e;if(Array.isArray(t)&&Array.isArray(e))return t.forEach(i=>{e.includes(i)||e.push(i)}),e;if(e==null||r<=0)return e!=null&&typeof e=="object"&&typeof t=="object"?Object.assign(e,t):t;if(t!=null&&typeof e=="object"&&typeof t=="object"){let i=e;Object.entries(t).forEach(([a,o])=>{if(typeof o=="object"){if(o===null)return;Object.hasOwn(e,a)||Object.defineProperty(e,a,{value:void 0,writable:!0,enumerable:!0,configurable:!0}),i[a]===void 0&&(i[a]=Array.isArray(o)?[]:{}),typeof i[a]=="object"&&(i[a]=vD(i[a],o,{depth:r-1}))}else typeof i[a]!="object"&&(Object.hasOwn(e,a)?i[a]=o:Object.defineProperty(e,a,{value:o,writable:!0,enumerable:!0,configurable:!0}))})}return e},"assignWithDepth"),ii=vD});var mw=As((xD,bD)=>{"use strict";(function(e,t){typeof xD=="object"&&typeof bD<"u"?bD.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self).dayjs=t()})(xD,(function(){"use strict";var e=1e3,t=6e4,r=36e5,n="millisecond",i="second",a="minute",o="hour",l="day",u="week",h="month",d="quarter",f="year",p="date",m="Invalid Date",g=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,v={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:s(function(_){var A=["th","st","nd","rd"],I=_%100;return"["+_+(A[(I-20)%10]||A[I]||A[0])+"]"},"ordinal")},x=s(function(_,A,I){var D=String(_);return!D||D.length>=A?_:""+Array(A+1-D.length).join(I)+_},"m"),b={s:x,z:s(function(_){var A=-_.utcOffset(),I=Math.abs(A),D=Math.floor(I/60),L=I%60;return(A<=0?"+":"-")+x(D,2,"0")+":"+x(L,2,"0")},"z"),m:s(function _(A,I){if(A.date()1)return _(O[0])}else{var $=A.name;w[$]=A,L=$}return!D&&L&&(T=L),L||!D&&T},"t"),R=s(function(_,A){if(S(_))return _.clone();var I=typeof A=="object"?A:{};return I.date=_,I.args=arguments,new P(I)},"O"),M=b;M.l=k,M.i=S,M.w=function(_,A){return R(_,{locale:A.$L,utc:A.$u,x:A.$x,$offset:A.$offset})};var P=(function(){function _(I){this.$L=k(I.locale,null,!0),this.parse(I),this.$x=this.$x||I.x||{},this[C]=!0}s(_,"M");var A=_.prototype;return A.parse=function(I){this.$d=(function(D){var L=D.date,B=D.utc;if(L===null)return new Date(NaN);if(M.u(L))return new Date;if(L instanceof Date)return new Date(L);if(typeof L=="string"&&!/Z$/i.test(L)){var O=L.match(g);if(O){var $=O[2]-1||0,W=(O[7]||"0").substring(0,3);return B?new Date(Date.UTC(O[1],$,O[3]||1,O[4]||0,O[5]||0,O[6]||0,W)):new Date(O[1],$,O[3]||1,O[4]||0,O[5]||0,O[6]||0,W)}}return new Date(L)})(I),this.init()},A.init=function(){var I=this.$d;this.$y=I.getFullYear(),this.$M=I.getMonth(),this.$D=I.getDate(),this.$W=I.getDay(),this.$H=I.getHours(),this.$m=I.getMinutes(),this.$s=I.getSeconds(),this.$ms=I.getMilliseconds()},A.$utils=function(){return M},A.isValid=function(){return this.$d.toString()!==m},A.isSame=function(I,D){var L=R(I);return this.startOf(D)<=L&&L<=this.endOf(D)},A.isAfter=function(I,D){return R(I){"use strict";tK=Rs(mw(),1),Ju={trace:0,debug:1,info:2,warn:3,error:4,fatal:5},se={trace:s((...e)=>{},"trace"),debug:s((...e)=>{},"debug"),info:s((...e)=>{},"info"),warn:s((...e)=>{},"warn"),error:s((...e)=>{},"error"),fatal:s((...e)=>{},"fatal")},Mx=s(function(e="fatal"){let t=Ju.fatal;typeof e=="string"?e.toLowerCase()in Ju&&(t=Ju[e]):typeof e=="number"&&(t=e),se.trace=()=>{},se.debug=()=>{},se.info=()=>{},se.warn=()=>{},se.error=()=>{},se.fatal=()=>{},t<=Ju.fatal&&(se.fatal=console.error?console.error.bind(console,ul("FATAL"),"color: orange"):console.log.bind(console,"\x1B[35m",ul("FATAL"))),t<=Ju.error&&(se.error=console.error?console.error.bind(console,ul("ERROR"),"color: orange"):console.log.bind(console,"\x1B[31m",ul("ERROR"))),t<=Ju.warn&&(se.warn=console.warn?console.warn.bind(console,ul("WARN"),"color: orange"):console.log.bind(console,"\x1B[33m",ul("WARN"))),t<=Ju.info&&(se.info=console.info?console.info.bind(console,ul("INFO"),"color: lightblue"):console.log.bind(console,"\x1B[34m",ul("INFO"))),t<=Ju.debug&&(se.debug=console.debug?console.debug.bind(console,ul("DEBUG"),"color: lightgreen"):console.log.bind(console,"\x1B[32m",ul("DEBUG"))),t<=Ju.trace&&(se.trace=console.debug?console.debug.bind(console,ul("TRACE"),"color: lightgreen"):console.log.bind(console,"\x1B[32m",ul("TRACE")))},"setLogLevel"),ul=s(e=>`%c${(0,tK.default)().format("ss.SSS")} : ${e} : `,"format")});var gw,rK,nK=F(()=>{"use strict";gw={min:{r:0,g:0,b:0,s:0,l:0,a:0},max:{r:255,g:255,b:255,h:360,s:100,l:100,a:1},clamp:{r:s(e=>e>=255?255:e<0?0:e,"r"),g:s(e=>e>=255?255:e<0?0:e,"g"),b:s(e=>e>=255?255:e<0?0:e,"b"),h:s(e=>e%360,"h"),s:s(e=>e>=100?100:e<0?0:e,"s"),l:s(e=>e>=100?100:e<0?0:e,"l"),a:s(e=>e>=1?1:e<0?0:e,"a")},toLinear:s(e=>{let t=e/255;return e>.03928?Math.pow((t+.055)/1.055,2.4):t/12.92},"toLinear"),hue2rgb:s((e,t,r)=>(r<0&&(r+=1),r>1&&(r-=1),r<.16666666666666666?e+(t-e)*6*r:r<.5?t:r<.6666666666666666?e+(t-e)*(.6666666666666666-r)*6:e),"hue2rgb"),hsl2rgb:s(({h:e,s:t,l:r},n)=>{if(!t)return r*2.55;e/=360,t/=100,r/=100;let i=r<.5?r*(1+t):r+t-r*t,a=2*r-i;switch(n){case"r":return gw.hue2rgb(a,i,e+.3333333333333333)*255;case"g":return gw.hue2rgb(a,i,e)*255;case"b":return gw.hue2rgb(a,i,e-.3333333333333333)*255}},"hsl2rgb"),rgb2hsl:s(({r:e,g:t,b:r},n)=>{e/=255,t/=255,r/=255;let i=Math.max(e,t,r),a=Math.min(e,t,r),o=(i+a)/2;if(n==="l")return o*100;if(i===a)return 0;let l=i-a,u=o>.5?l/(2-i-a):l/(i+a);if(n==="s")return u*100;switch(i){case e:return((t-r)/l+(t{"use strict";FGe={clamp:s((e,t,r)=>t>r?Math.min(t,Math.max(r,e)):Math.min(r,Math.max(t,e)),"clamp"),round:s(e=>Math.round(e*1e10)/1e10,"round")},iK=FGe});var GGe,sK,oK=F(()=>{"use strict";GGe={dec2hex:s(e=>{let t=Math.round(e).toString(16);return t.length>1?t:`0${t}`},"dec2hex")},sK=GGe});var zGe,pr,Ic=F(()=>{"use strict";nK();aK();oK();zGe={channel:rK,lang:iK,unit:sK},pr=zGe});var eh,ua,Nx=F(()=>{"use strict";Ic();eh={};for(let e=0;e<=255;e++)eh[e]=pr.unit.dec2hex(e);ua={ALL:0,RGB:1,HSL:2}});var TD,lK,cK=F(()=>{"use strict";Nx();TD=class{static{s(this,"Type")}constructor(){this.type=ua.ALL}get(){return this.type}set(t){if(this.type&&this.type!==t)throw new Error("Cannot change both RGB and HSL channels at the same time");this.type=t}reset(){this.type=ua.ALL}is(t){return this.type===t}},lK=TD});var CD,uK,hK=F(()=>{"use strict";Ic();cK();Nx();CD=class{static{s(this,"Channels")}constructor(t,r){this.color=r,this.changed=!1,this.data=t,this.type=new lK}set(t,r){return this.color=r,this.changed=!1,this.data=t,this.type.type=ua.ALL,this}_ensureHSL(){let t=this.data,{h:r,s:n,l:i}=t;r===void 0&&(t.h=pr.channel.rgb2hsl(t,"h")),n===void 0&&(t.s=pr.channel.rgb2hsl(t,"s")),i===void 0&&(t.l=pr.channel.rgb2hsl(t,"l"))}_ensureRGB(){let t=this.data,{r,g:n,b:i}=t;r===void 0&&(t.r=pr.channel.hsl2rgb(t,"r")),n===void 0&&(t.g=pr.channel.hsl2rgb(t,"g")),i===void 0&&(t.b=pr.channel.hsl2rgb(t,"b"))}get r(){let t=this.data,r=t.r;return!this.type.is(ua.HSL)&&r!==void 0?r:(this._ensureHSL(),pr.channel.hsl2rgb(t,"r"))}get g(){let t=this.data,r=t.g;return!this.type.is(ua.HSL)&&r!==void 0?r:(this._ensureHSL(),pr.channel.hsl2rgb(t,"g"))}get b(){let t=this.data,r=t.b;return!this.type.is(ua.HSL)&&r!==void 0?r:(this._ensureHSL(),pr.channel.hsl2rgb(t,"b"))}get h(){let t=this.data,r=t.h;return!this.type.is(ua.RGB)&&r!==void 0?r:(this._ensureRGB(),pr.channel.rgb2hsl(t,"h"))}get s(){let t=this.data,r=t.s;return!this.type.is(ua.RGB)&&r!==void 0?r:(this._ensureRGB(),pr.channel.rgb2hsl(t,"s"))}get l(){let t=this.data,r=t.l;return!this.type.is(ua.RGB)&&r!==void 0?r:(this._ensureRGB(),pr.channel.rgb2hsl(t,"l"))}get a(){return this.data.a}set r(t){this.type.set(ua.RGB),this.changed=!0,this.data.r=t}set g(t){this.type.set(ua.RGB),this.changed=!0,this.data.g=t}set b(t){this.type.set(ua.RGB),this.changed=!0,this.data.b=t}set h(t){this.type.set(ua.HSL),this.changed=!0,this.data.h=t}set s(t){this.type.set(ua.HSL),this.changed=!0,this.data.s=t}set l(t){this.type.set(ua.HSL),this.changed=!0,this.data.l=t}set a(t){this.changed=!0,this.data.a=t}},uK=CD});var VGe,Sd,Px=F(()=>{"use strict";hK();VGe=new uK({r:0,g:0,b:0,a:0},"transparent"),Sd=VGe});var dK,Fp,SD=F(()=>{"use strict";Px();Nx();dK={re:/^#((?:[a-f0-9]{2}){2,4}|[a-f0-9]{3})$/i,parse:s(e=>{if(e.charCodeAt(0)!==35)return;let t=e.match(dK.re);if(!t)return;let r=t[1],n=parseInt(r,16),i=r.length,a=i%4===0,o=i>4,l=o?1:17,u=o?8:4,h=a?0:-1,d=o?255:15;return Sd.set({r:(n>>u*(h+3)&d)*l,g:(n>>u*(h+2)&d)*l,b:(n>>u*(h+1)&d)*l,a:a?(n&d)*l/255:1},e)},"parse"),stringify:s(e=>{let{r:t,g:r,b:n,a:i}=e;return i<1?`#${eh[Math.round(t)]}${eh[Math.round(r)]}${eh[Math.round(n)]}${eh[Math.round(i*255)]}`:`#${eh[Math.round(t)]}${eh[Math.round(r)]}${eh[Math.round(n)]}`},"stringify")},Fp=dK});var yw,Ox,fK=F(()=>{"use strict";Ic();Px();yw={re:/^hsla?\(\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e-?\d+)?(?:deg|grad|rad|turn)?)\s*?(?:,|\s)\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e-?\d+)?%)\s*?(?:,|\s)\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e-?\d+)?%)(?:\s*?(?:,|\/)\s*?\+?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e-?\d+)?(%)?))?\s*?\)$/i,hueRe:/^(.+?)(deg|grad|rad|turn)$/i,_hue2deg:s(e=>{let t=e.match(yw.hueRe);if(t){let[,r,n]=t;switch(n){case"grad":return pr.channel.clamp.h(parseFloat(r)*.9);case"rad":return pr.channel.clamp.h(parseFloat(r)*180/Math.PI);case"turn":return pr.channel.clamp.h(parseFloat(r)*360)}}return pr.channel.clamp.h(parseFloat(e))},"_hue2deg"),parse:s(e=>{let t=e.charCodeAt(0);if(t!==104&&t!==72)return;let r=e.match(yw.re);if(!r)return;let[,n,i,a,o,l]=r;return Sd.set({h:yw._hue2deg(n),s:pr.channel.clamp.s(parseFloat(i)),l:pr.channel.clamp.l(parseFloat(a)),a:o?pr.channel.clamp.a(l?parseFloat(o)/100:parseFloat(o)):1},e)},"parse"),stringify:s(e=>{let{h:t,s:r,l:n,a:i}=e;return i<1?`hsla(${pr.lang.round(t)}, ${pr.lang.round(r)}%, ${pr.lang.round(n)}%, ${i})`:`hsl(${pr.lang.round(t)}, ${pr.lang.round(r)}%, ${pr.lang.round(n)}%)`},"stringify")},Ox=yw});var vw,wD,pK=F(()=>{"use strict";SD();vw={colors:{aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyanaqua:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",rebeccapurple:"#663399",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",transparent:"#00000000",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"},parse:s(e=>{e=e.toLowerCase();let t=vw.colors[e];if(t)return Fp.parse(t)},"parse"),stringify:s(e=>{let t=Fp.stringify(e);for(let r in vw.colors)if(vw.colors[r]===t)return r},"stringify")},wD=vw});var mK,Bx,gK=F(()=>{"use strict";Ic();Px();mK={re:/^rgba?\(\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e\d+)?(%?))\s*?(?:,|\s)\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e\d+)?(%?))\s*?(?:,|\s)\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e\d+)?(%?))(?:\s*?(?:,|\/)\s*?\+?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e\d+)?(%?)))?\s*?\)$/i,parse:s(e=>{let t=e.charCodeAt(0);if(t!==114&&t!==82)return;let r=e.match(mK.re);if(!r)return;let[,n,i,a,o,l,u,h,d]=r;return Sd.set({r:pr.channel.clamp.r(i?parseFloat(n)*2.55:parseFloat(n)),g:pr.channel.clamp.g(o?parseFloat(a)*2.55:parseFloat(a)),b:pr.channel.clamp.b(u?parseFloat(l)*2.55:parseFloat(l)),a:h?pr.channel.clamp.a(d?parseFloat(h)/100:parseFloat(h)):1},e)},"parse"),stringify:s(e=>{let{r:t,g:r,b:n,a:i}=e;return i<1?`rgba(${pr.lang.round(t)}, ${pr.lang.round(r)}, ${pr.lang.round(n)}, ${pr.lang.round(i)})`:`rgb(${pr.lang.round(t)}, ${pr.lang.round(r)}, ${pr.lang.round(n)})`},"stringify")},Bx=mK});var WGe,ha,th=F(()=>{"use strict";SD();fK();pK();gK();Nx();WGe={format:{keyword:wD,hex:Fp,rgb:Bx,rgba:Bx,hsl:Ox,hsla:Ox},parse:s(e=>{if(typeof e!="string")return e;let t=Fp.parse(e)||Bx.parse(e)||Ox.parse(e)||wD.parse(e);if(t)return t;throw new Error(`Unsupported color format: "${e}"`)},"parse"),stringify:s(e=>!e.changed&&e.color?e.color:e.type.is(ua.HSL)||e.data.r===void 0?Ox.stringify(e):e.a<1||!Number.isInteger(e.r)||!Number.isInteger(e.g)||!Number.isInteger(e.b)?Bx.stringify(e):Fp.stringify(e),"stringify")},ha=WGe});var qGe,xw,kD=F(()=>{"use strict";Ic();th();qGe=s((e,t)=>{let r=ha.parse(e);for(let n in t)r[n]=pr.channel.clamp[n](t[n]);return ha.stringify(r)},"change"),xw=qGe});var HGe,Wi,ED=F(()=>{"use strict";Ic();Px();th();kD();HGe=s((e,t,r=0,n=1)=>{if(typeof e!="number")return xw(e,{a:t});let i=Sd.set({r:pr.channel.clamp.r(e),g:pr.channel.clamp.g(t),b:pr.channel.clamp.b(r),a:pr.channel.clamp.a(n)});return ha.stringify(i)},"rgba"),Wi=HGe});var UGe,Gp,yK=F(()=>{"use strict";Ic();th();UGe=s((e,t)=>pr.lang.round(ha.parse(e)[t]),"channel"),Gp=UGe});var YGe,vK,xK=F(()=>{"use strict";Ic();th();YGe=s(e=>{let{r:t,g:r,b:n}=ha.parse(e),i=.2126*pr.channel.toLinear(t)+.7152*pr.channel.toLinear(r)+.0722*pr.channel.toLinear(n);return pr.lang.round(i)},"luminance"),vK=YGe});var jGe,bK,TK=F(()=>{"use strict";xK();jGe=s(e=>vK(e)>=.5,"isLight"),bK=jGe});var XGe,En,CK=F(()=>{"use strict";TK();XGe=s(e=>!bK(e),"isDark"),En=XGe});var KGe,F0,bw=F(()=>{"use strict";Ic();th();KGe=s((e,t,r)=>{let n=ha.parse(e),i=n[t],a=pr.channel.clamp[t](i+r);return i!==a&&(n[t]=a),ha.stringify(n)},"adjustChannel"),F0=KGe});var ZGe,Qe,SK=F(()=>{"use strict";bw();ZGe=s((e,t)=>F0(e,"l",t),"lighten"),Qe=ZGe});var QGe,Je,wK=F(()=>{"use strict";bw();QGe=s((e,t)=>F0(e,"l",-t),"darken"),Je=QGe});var JGe,Tw,kK=F(()=>{"use strict";bw();JGe=s((e,t)=>F0(e,"a",-t),"transparentize"),Tw=JGe});var eze,ge,EK=F(()=>{"use strict";th();kD();eze=s((e,t)=>{let r=ha.parse(e),n={};for(let i in t)t[i]&&(n[i]=r[i]+t[i]);return xw(e,n)},"adjust"),ge=eze});var tze,AK,RK=F(()=>{"use strict";th();ED();tze=s((e,t,r=50)=>{let{r:n,g:i,b:a,a:o}=ha.parse(e),{r:l,g:u,b:h,a:d}=ha.parse(t),f=r/100,p=f*2-1,m=o-d,y=((p*m===-1?p:(p+m)/(1+p*m))+1)/2,v=1-y,x=n*y+l*v,b=i*y+u*v,T=a*y+h*v,w=o*f+d*(1-f);return Wi(x,b,T,w)},"mix"),AK=tze});var rze,je,_K=F(()=>{"use strict";th();RK();rze=s((e,t=100)=>{let r=ha.parse(e);return r.r=255-r.r,r.g=255-r.g,r.b=255-r.b,AK(r,e,t)},"invert"),je=rze});var LK=F(()=>{"use strict";ED();yK();CK();SK();wK();kK();EK();_K()});var Xi=F(()=>{"use strict";LK()});var Ki,Zi,Wl=F(()=>{"use strict";Ki="#ffffff",Zi="#f2f2f2"});var ur,hl=F(()=>{"use strict";Xi();ur=s((e,t)=>t?ge(e,{s:-40,l:10}):ge(e,{s:-40,l:-10}),"mkBorder")});var RD,DK,IK=F(()=>{"use strict";Xi();Wl();hl();RD=class{static{s(this,"Theme")}constructor(){this.background="#f4f4f4",this.primaryColor="#fff4dd",this.noteBkgColor="#fff5ad",this.noteTextColor="#333",this.THEME_COLOR_LIMIT=12,this.radius=5,this.strokeWidth=1,this.fontFamily='"trebuchet ms", verdana, arial, sans-serif',this.fontSize="16px",this.useGradient=!0,this.dropShadow="drop-shadow( 1px 2px 2px rgba(185,185,185,1))"}updateColors(){if(this.primaryTextColor=this.primaryTextColor||(this.darkMode?"#eee":"#333"),this.secondaryColor=this.secondaryColor||ge(this.primaryColor,{h:-120}),this.tertiaryColor=this.tertiaryColor||ge(this.primaryColor,{h:180,l:5}),this.primaryBorderColor=this.primaryBorderColor||ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=this.secondaryBorderColor||ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=this.tertiaryBorderColor||ur(this.tertiaryColor,this.darkMode),this.noteBorderColor=this.noteBorderColor||ur(this.noteBkgColor,this.darkMode),this.noteBkgColor=this.noteBkgColor||"#fff5ad",this.noteTextColor=this.noteTextColor||"#333",this.secondaryTextColor=this.secondaryTextColor||je(this.secondaryColor),this.tertiaryTextColor=this.tertiaryTextColor||je(this.tertiaryColor),this.lineColor=this.lineColor||je(this.background),this.arrowheadColor=this.arrowheadColor||je(this.background),this.textColor=this.textColor||this.primaryTextColor,this.border2=this.border2||this.tertiaryBorderColor,this.nodeBkg=this.nodeBkg||this.primaryColor,this.mainBkg=this.mainBkg||this.primaryColor,this.nodeBorder=this.nodeBorder||this.primaryBorderColor,this.clusterBkg=this.clusterBkg||this.tertiaryColor,this.clusterBorder=this.clusterBorder||this.tertiaryBorderColor,this.defaultLinkColor=this.defaultLinkColor||this.lineColor,this.titleColor=this.titleColor||this.tertiaryTextColor,this.edgeLabelBackground=this.edgeLabelBackground||(this.darkMode?Je(this.secondaryColor,30):this.secondaryColor),this.nodeTextColor=this.nodeTextColor||this.primaryTextColor,this.actorBorder=this.actorBorder||this.primaryBorderColor,this.actorBkg=this.actorBkg||this.mainBkg,this.actorTextColor=this.actorTextColor||this.primaryTextColor,this.actorLineColor=this.actorLineColor||this.actorBorder,this.labelBoxBkgColor=this.labelBoxBkgColor||this.actorBkg,this.signalColor=this.signalColor||this.textColor,this.signalTextColor=this.signalTextColor||this.textColor,this.labelBoxBorderColor=this.labelBoxBorderColor||this.actorBorder,this.labelTextColor=this.labelTextColor||this.actorTextColor,this.loopTextColor=this.loopTextColor||this.actorTextColor,this.activationBorderColor=this.activationBorderColor||Je(this.secondaryColor,10),this.activationBkgColor=this.activationBkgColor||this.secondaryColor,this.sequenceNumberColor=this.sequenceNumberColor||je(this.lineColor),this.rectBkgColor=this.rectBkgColor||this.tertiaryColor,this.sectionBkgColor=this.sectionBkgColor||this.tertiaryColor,this.altSectionBkgColor=this.altSectionBkgColor||"white",this.sectionBkgColor=this.sectionBkgColor||this.secondaryColor,this.sectionBkgColor2=this.sectionBkgColor2||this.primaryColor,this.excludeBkgColor=this.excludeBkgColor||"#eeeeee",this.taskBorderColor=this.taskBorderColor||this.primaryBorderColor,this.taskBkgColor=this.taskBkgColor||this.primaryColor,this.activeTaskBorderColor=this.activeTaskBorderColor||this.primaryColor,this.activeTaskBkgColor=this.activeTaskBkgColor||Qe(this.primaryColor,23),this.gridColor=this.gridColor||"lightgrey",this.doneTaskBkgColor=this.doneTaskBkgColor||"lightgrey",this.doneTaskBorderColor=this.doneTaskBorderColor||"grey",this.critBorderColor=this.critBorderColor||"#ff8888",this.critBkgColor=this.critBkgColor||"red",this.todayLineColor=this.todayLineColor||"red",this.vertLineColor=this.vertLineColor||"navy",this.taskTextColor=this.taskTextColor||this.textColor,this.taskTextOutsideColor=this.taskTextOutsideColor||this.textColor,this.taskTextLightColor=this.taskTextLightColor||this.textColor,this.taskTextColor=this.taskTextColor||this.primaryTextColor,this.taskTextDarkColor=this.taskTextDarkColor||this.textColor,this.taskTextClickableColor=this.taskTextClickableColor||"#003163",this.noteFontWeight=this.noteFontWeight||"normal",this.fontWeight=this.fontWeight||"normal",this.personBorder=this.personBorder||this.primaryBorderColor,this.personBkg=this.personBkg||this.mainBkg,this.darkMode?(this.rowOdd=this.rowOdd||Je(this.mainBkg,5)||"#ffffff",this.rowEven=this.rowEven||Je(this.mainBkg,10)):(this.rowOdd=this.rowOdd||Qe(this.mainBkg,75)||"#ffffff",this.rowEven=this.rowEven||Qe(this.mainBkg,5)),this.transitionColor=this.transitionColor||this.lineColor,this.transitionLabelColor=this.transitionLabelColor||this.textColor,this.stateLabelColor=this.stateLabelColor||this.stateBkg||this.primaryTextColor,this.stateBkg=this.stateBkg||this.mainBkg,this.labelBackgroundColor=this.labelBackgroundColor||this.stateBkg,this.compositeBackground=this.compositeBackground||this.background||this.tertiaryColor,this.altBackground=this.altBackground||this.tertiaryColor,this.compositeTitleBackground=this.compositeTitleBackground||this.mainBkg,this.compositeBorder=this.compositeBorder||this.nodeBorder,this.innerEndBackground=this.nodeBorder,this.errorBkgColor=this.errorBkgColor||this.tertiaryColor,this.errorTextColor=this.errorTextColor||this.tertiaryTextColor,this.transitionColor=this.transitionColor||this.lineColor,this.specialStateColor=this.lineColor,this.cScale0=this.cScale0||this.primaryColor,this.cScale1=this.cScale1||this.secondaryColor,this.cScale2=this.cScale2||this.tertiaryColor,this.cScale3=this.cScale3||ge(this.primaryColor,{h:30}),this.cScale4=this.cScale4||ge(this.primaryColor,{h:60}),this.cScale5=this.cScale5||ge(this.primaryColor,{h:90}),this.cScale6=this.cScale6||ge(this.primaryColor,{h:120}),this.cScale7=this.cScale7||ge(this.primaryColor,{h:150}),this.cScale8=this.cScale8||ge(this.primaryColor,{h:210,l:150}),this.cScale9=this.cScale9||ge(this.primaryColor,{h:270}),this.cScale10=this.cScale10||ge(this.primaryColor,{h:300}),this.cScale11=this.cScale11||ge(this.primaryColor,{h:330}),this.darkMode)for(let r=0;r{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},DK=s(e=>{let t=new RD;return t.calculate(e),t},"getThemeVariables")});var _D,MK,NK=F(()=>{"use strict";Xi();hl();_D=class{static{s(this,"Theme")}constructor(){this.background="#333",this.primaryColor="#1f2020",this.secondaryColor=Qe(this.primaryColor,16),this.tertiaryColor=ge(this.primaryColor,{h:-160}),this.primaryBorderColor=je(this.background),this.secondaryBorderColor=ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=ur(this.tertiaryColor,this.darkMode),this.primaryTextColor=je(this.primaryColor),this.secondaryTextColor=je(this.secondaryColor),this.tertiaryTextColor=je(this.tertiaryColor),this.lineColor=je(this.background),this.textColor=je(this.background),this.mainBkg="#1f2020",this.secondBkg="calculated",this.mainContrastColor="lightgrey",this.darkTextColor=Qe(je("#323D47"),10),this.lineColor="calculated",this.border1="#ccc",this.border2=Wi(255,255,255,.25),this.arrowheadColor="calculated",this.fontFamily='"trebuchet ms", verdana, arial, sans-serif',this.fontSize="16px",this.labelBackground="#181818",this.textColor="#ccc",this.THEME_COLOR_LIMIT=12,this.radius=5,this.strokeWidth=1,this.nodeBkg="calculated",this.nodeBorder="calculated",this.clusterBkg="calculated",this.clusterBorder="calculated",this.defaultLinkColor="calculated",this.titleColor="#F9FFFE",this.edgeLabelBackground="calculated",this.actorBorder="calculated",this.actorBkg="calculated",this.actorTextColor="calculated",this.actorLineColor="calculated",this.signalColor="calculated",this.signalTextColor="calculated",this.labelBoxBkgColor="calculated",this.labelBoxBorderColor="calculated",this.labelTextColor="calculated",this.loopTextColor="calculated",this.noteBorderColor="calculated",this.noteBkgColor="#fff5ad",this.noteTextColor="calculated",this.activationBorderColor="calculated",this.activationBkgColor="calculated",this.sequenceNumberColor="black",this.clusterBkg="#302F3D",this.sectionBkgColor=Je("#EAE8D9",30),this.altSectionBkgColor="calculated",this.sectionBkgColor2="#EAE8D9",this.excludeBkgColor=Je(this.sectionBkgColor,10),this.taskBorderColor=Wi(255,255,255,70),this.taskBkgColor="calculated",this.taskTextColor="calculated",this.taskTextLightColor="calculated",this.taskTextOutsideColor="calculated",this.taskTextClickableColor="#003163",this.activeTaskBorderColor=Wi(255,255,255,50),this.activeTaskBkgColor="#81B1DB",this.gridColor="calculated",this.doneTaskBkgColor="calculated",this.doneTaskBorderColor="grey",this.critBorderColor="#E83737",this.critBkgColor="#E83737",this.taskTextDarkColor="calculated",this.todayLineColor="#DB5757",this.vertLineColor="#00BFFF",this.personBorder=this.primaryBorderColor,this.personBkg=this.mainBkg,this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.rowOdd=this.rowOdd||Qe(this.mainBkg,5)||"#ffffff",this.rowEven=this.rowEven||Je(this.mainBkg,10),this.labelColor="calculated",this.errorBkgColor="#a44141",this.errorTextColor="#ddd",this.useGradient=!0,this.gradientStart=this.primaryBorderColor,this.gradientStop=this.secondaryBorderColor,this.dropShadow="drop-shadow( 1px 2px 2px rgba(185,185,185,1))",this.noteFontWeight=this.noteFontWeight||"normal",this.fontWeight=this.fontWeight||"normal"}updateColors(){this.secondBkg=Qe(this.mainBkg,16),this.lineColor=this.mainContrastColor,this.arrowheadColor=this.mainContrastColor,this.nodeBkg=this.mainBkg,this.nodeBorder=this.border1,this.clusterBkg=this.secondBkg,this.clusterBorder=this.border2,this.defaultLinkColor=this.lineColor,this.edgeLabelBackground=Qe(this.labelBackground,25),this.actorBorder=this.border1,this.actorBkg=this.mainBkg,this.actorTextColor=this.mainContrastColor,this.actorLineColor=this.actorBorder,this.signalColor=this.mainContrastColor,this.signalTextColor=this.mainContrastColor,this.labelBoxBkgColor=this.actorBkg,this.labelBoxBorderColor=this.actorBorder,this.labelTextColor=this.mainContrastColor,this.loopTextColor=this.mainContrastColor,this.noteBorderColor=this.secondaryBorderColor,this.noteBkgColor=this.secondBkg,this.noteTextColor=this.secondaryTextColor,this.activationBorderColor=this.border1,this.activationBkgColor=this.secondBkg,this.rectBkgColor=this.rectBkgColor||this.tertiaryColor,this.altSectionBkgColor=this.background,this.taskBkgColor=Qe(this.mainBkg,23),this.taskTextColor=this.darkTextColor,this.taskTextLightColor=this.mainContrastColor,this.taskTextOutsideColor=this.taskTextLightColor,this.gridColor=this.mainContrastColor,this.doneTaskBkgColor=this.mainContrastColor,this.taskTextDarkColor=je(this.doneTaskBkgColor),this.archEdgeColor=this.lineColor,this.archEdgeArrowColor=this.lineColor,this.transitionColor=this.transitionColor||this.lineColor,this.transitionLabelColor=this.transitionLabelColor||this.textColor,this.stateLabelColor=this.stateLabelColor||this.stateBkg||this.primaryTextColor,this.stateBkg=this.stateBkg||this.mainBkg,this.labelBackgroundColor=this.labelBackgroundColor||this.stateBkg,this.compositeBackground=this.compositeBackground||this.background||this.tertiaryColor,this.altBackground=this.altBackground||"#555",this.compositeTitleBackground=this.compositeTitleBackground||this.mainBkg,this.compositeBorder=this.compositeBorder||this.nodeBorder,this.innerEndBackground=this.primaryBorderColor,this.specialStateColor="#f4f4f4",this.errorBkgColor=this.errorBkgColor||this.tertiaryColor,this.errorTextColor=this.errorTextColor||this.tertiaryTextColor,this.fillType0=this.primaryColor,this.fillType1=this.secondaryColor,this.fillType2=ge(this.primaryColor,{h:64}),this.fillType3=ge(this.secondaryColor,{h:64}),this.fillType4=ge(this.primaryColor,{h:-64}),this.fillType5=ge(this.secondaryColor,{h:-64}),this.fillType6=ge(this.primaryColor,{h:128}),this.fillType7=ge(this.secondaryColor,{h:128}),this.cScale1=this.cScale1||"#0b0000",this.cScale2=this.cScale2||"#4d1037",this.cScale3=this.cScale3||"#3f5258",this.cScale4=this.cScale4||"#4f2f1b",this.cScale5=this.cScale5||"#6e0a0a",this.cScale6=this.cScale6||"#3b0048",this.cScale7=this.cScale7||"#995a01",this.cScale8=this.cScale8||"#154706",this.cScale9=this.cScale9||"#161722",this.cScale10=this.cScale10||"#00296f",this.cScale11=this.cScale11||"#01629c",this.cScale12=this.cScale12||"#010029",this.cScale0=this.cScale0||this.primaryColor,this.cScale1=this.cScale1||this.secondaryColor,this.cScale2=this.cScale2||this.tertiaryColor,this.cScale3=this.cScale3||ge(this.primaryColor,{h:30}),this.cScale4=this.cScale4||ge(this.primaryColor,{h:60}),this.cScale5=this.cScale5||ge(this.primaryColor,{h:90}),this.cScale6=this.cScale6||ge(this.primaryColor,{h:120}),this.cScale7=this.cScale7||ge(this.primaryColor,{h:150}),this.cScale8=this.cScale8||ge(this.primaryColor,{h:210}),this.cScale9=this.cScale9||ge(this.primaryColor,{h:270}),this.cScale10=this.cScale10||ge(this.primaryColor,{h:300}),this.cScale11=this.cScale11||ge(this.primaryColor,{h:330});for(let t=0;t{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},MK=s(e=>{let t=new _D;return t.calculate(e),t},"getThemeVariables")});var LD,ka,Mc=F(()=>{"use strict";Xi();hl();Wl();LD=class{static{s(this,"Theme")}constructor(){this.background="#f4f4f4",this.primaryColor="#ECECFF",this.secondaryColor=ge(this.primaryColor,{h:120}),this.secondaryColor="#ffffde",this.tertiaryColor=ge(this.primaryColor,{h:-160}),this.primaryBorderColor=ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=ur(this.tertiaryColor,this.darkMode),this.primaryTextColor=je(this.primaryColor),this.secondaryTextColor=je(this.secondaryColor),this.tertiaryTextColor=je(this.tertiaryColor),this.lineColor=je(this.background),this.textColor=je(this.background),this.background="white",this.mainBkg="#ECECFF",this.secondBkg="#ffffde",this.lineColor="#333333",this.border1="#9370DB",this.primaryBorderColor=ur(this.primaryColor,this.darkMode),this.border2="#aaaa33",this.arrowheadColor="#333333",this.fontFamily='"trebuchet ms", verdana, arial, sans-serif',this.fontSize="16px",this.labelBackground="rgba(232,232,232, 0.8)",this.textColor="#333",this.THEME_COLOR_LIMIT=12,this.radius=5,this.strokeWidth=1,this.nodeBkg="calculated",this.nodeBorder="calculated",this.clusterBkg="calculated",this.clusterBorder="calculated",this.defaultLinkColor="calculated",this.titleColor="calculated",this.edgeLabelBackground="calculated",this.actorBorder="calculated",this.actorBkg="calculated",this.actorTextColor="black",this.actorLineColor="calculated",this.signalColor="calculated",this.signalTextColor="calculated",this.labelBoxBkgColor="calculated",this.labelBoxBorderColor="calculated",this.labelTextColor="calculated",this.loopTextColor="calculated",this.noteBorderColor="calculated",this.noteBkgColor="#fff5ad",this.noteTextColor="calculated",this.activationBorderColor="#666",this.activationBkgColor="#f4f4f4",this.sequenceNumberColor="white",this.clusterBkg="#FBFBFF",this.sectionBkgColor="calculated",this.altSectionBkgColor="calculated",this.sectionBkgColor2="calculated",this.excludeBkgColor="#eeeeee",this.taskBorderColor="calculated",this.taskBkgColor="calculated",this.taskTextLightColor="calculated",this.taskTextColor=this.taskTextLightColor,this.taskTextDarkColor="calculated",this.taskTextOutsideColor=this.taskTextDarkColor,this.taskTextClickableColor="calculated",this.activeTaskBorderColor="calculated",this.activeTaskBkgColor="calculated",this.gridColor="calculated",this.doneTaskBkgColor="calculated",this.doneTaskBorderColor="calculated",this.critBorderColor="calculated",this.critBkgColor="calculated",this.todayLineColor="calculated",this.vertLineColor="calculated",this.sectionBkgColor=Wi(102,102,255,.49),this.altSectionBkgColor="white",this.sectionBkgColor2="#fff400",this.taskBorderColor="#534fbc",this.taskBkgColor="#8a90dd",this.taskTextLightColor="white",this.taskTextColor="calculated",this.taskTextDarkColor="black",this.taskTextOutsideColor="calculated",this.taskTextClickableColor="#003163",this.activeTaskBorderColor="#534fbc",this.activeTaskBkgColor="#bfc7ff",this.gridColor="lightgrey",this.doneTaskBkgColor="lightgrey",this.doneTaskBorderColor="grey",this.critBorderColor="#ff8888",this.critBkgColor="red",this.todayLineColor="red",this.vertLineColor="navy",this.noteFontWeight=this.noteFontWeight||"normal",this.fontWeight=this.fontWeight||"normal",this.personBorder=this.primaryBorderColor,this.personBkg=this.mainBkg,this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.rowOdd="calculated",this.rowEven="calculated",this.labelColor="black",this.errorBkgColor="#552222",this.errorTextColor="#552222",this.useGradient=!1,this.gradientStart=this.primaryBorderColor,this.gradientStop=this.secondaryBorderColor,this.dropShadow="drop-shadow(1px 2px 2px rgba(185, 185, 185, 1))",this.updateColors()}updateColors(){this.cScale0=this.cScale0||this.primaryColor,this.cScale1=this.cScale1||this.secondaryColor,this.cScale2=this.cScale2||this.tertiaryColor,this.cScale3=this.cScale3||ge(this.primaryColor,{h:30}),this.cScale4=this.cScale4||ge(this.primaryColor,{h:60}),this.cScale5=this.cScale5||ge(this.primaryColor,{h:90}),this.cScale6=this.cScale6||ge(this.primaryColor,{h:120}),this.cScale7=this.cScale7||ge(this.primaryColor,{h:150}),this.cScale8=this.cScale8||ge(this.primaryColor,{h:210}),this.cScale9=this.cScale9||ge(this.primaryColor,{h:270}),this.cScale10=this.cScale10||ge(this.primaryColor,{h:300}),this.cScale11=this.cScale11||ge(this.primaryColor,{h:330}),this.cScalePeer1=this.cScalePeer1||Je(this.secondaryColor,45),this.cScalePeer2=this.cScalePeer2||Je(this.tertiaryColor,40);for(let t=0;t{this[n]==="calculated"&&(this[n]=void 0)}),typeof t!="object"){this.updateColors();return}let r=Object.keys(t);r.forEach(n=>{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},ka=s(e=>{let t=new LD;return t.calculate(e),t},"getThemeVariables")});var DD,PK,OK=F(()=>{"use strict";Xi();Wl();hl();DD=class{static{s(this,"Theme")}constructor(){this.background="#f4f4f4",this.primaryColor="#cde498",this.secondaryColor="#cdffb2",this.background="white",this.mainBkg="#cde498",this.secondBkg="#cdffb2",this.lineColor="green",this.border1="#13540c",this.border2="#6eaa49",this.arrowheadColor="green",this.fontFamily='"trebuchet ms", verdana, arial, sans-serif',this.fontSize="16px",this.tertiaryColor=Qe("#cde498",10),this.primaryBorderColor=ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=ur(this.tertiaryColor,this.darkMode),this.primaryTextColor=je(this.primaryColor),this.secondaryTextColor=je(this.secondaryColor),this.tertiaryTextColor=je(this.primaryColor),this.lineColor=je(this.background),this.textColor=je(this.background),this.THEME_COLOR_LIMIT=12,this.radius=5,this.strokeWidth=1,this.nodeBkg="calculated",this.nodeBorder="calculated",this.clusterBkg="calculated",this.clusterBorder="calculated",this.defaultLinkColor="calculated",this.titleColor="#333",this.edgeLabelBackground="#e8e8e8",this.actorBorder="calculated",this.actorBkg="calculated",this.actorTextColor="black",this.actorLineColor="calculated",this.signalColor="#333",this.signalTextColor="#333",this.labelBoxBkgColor="calculated",this.labelBoxBorderColor="#326932",this.labelTextColor="calculated",this.loopTextColor="calculated",this.noteBorderColor="calculated",this.noteBkgColor="#fff5ad",this.noteTextColor="calculated",this.activationBorderColor="#666",this.activationBkgColor="#f4f4f4",this.sequenceNumberColor="white",this.sectionBkgColor="#6eaa49",this.altSectionBkgColor="white",this.sectionBkgColor2="#6eaa49",this.excludeBkgColor="#eeeeee",this.taskBorderColor="calculated",this.taskBkgColor="#487e3a",this.taskTextLightColor="white",this.taskTextColor="calculated",this.taskTextDarkColor="black",this.taskTextOutsideColor="calculated",this.taskTextClickableColor="#003163",this.activeTaskBorderColor="calculated",this.activeTaskBkgColor="calculated",this.gridColor="lightgrey",this.doneTaskBkgColor="lightgrey",this.doneTaskBorderColor="grey",this.critBorderColor="#ff8888",this.critBkgColor="red",this.todayLineColor="red",this.vertLineColor="#00BFFF",this.personBorder=this.primaryBorderColor,this.personBkg=this.mainBkg,this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.noteFontWeight="normal",this.fontWeight="normal",this.labelColor="black",this.errorBkgColor="#552222",this.errorTextColor="#552222",this.useGradient=!0,this.gradientStart=this.primaryBorderColor,this.gradientStop=this.secondaryBorderColor,this.dropShadow="drop-shadow( 1px 2px 2px rgba(185,185,185,0.5))"}updateColors(){this.actorBorder=Je(this.mainBkg,20),this.actorBkg=this.mainBkg,this.labelBoxBkgColor=this.actorBkg,this.labelTextColor=this.actorTextColor,this.loopTextColor=this.actorTextColor,this.noteBorderColor=this.border2,this.noteTextColor=this.actorTextColor,this.actorLineColor=this.actorBorder,this.rectBkgColor=this.rectBkgColor||this.tertiaryColor,this.cScale0=this.cScale0||this.primaryColor,this.cScale1=this.cScale1||this.secondaryColor,this.cScale2=this.cScale2||this.tertiaryColor,this.cScale3=this.cScale3||ge(this.primaryColor,{h:30}),this.cScale4=this.cScale4||ge(this.primaryColor,{h:60}),this.cScale5=this.cScale5||ge(this.primaryColor,{h:90}),this.cScale6=this.cScale6||ge(this.primaryColor,{h:120}),this.cScale7=this.cScale7||ge(this.primaryColor,{h:150}),this.cScale8=this.cScale8||ge(this.primaryColor,{h:210}),this.cScale9=this.cScale9||ge(this.primaryColor,{h:270}),this.cScale10=this.cScale10||ge(this.primaryColor,{h:300}),this.cScale11=this.cScale11||ge(this.primaryColor,{h:330}),this.cScalePeer1=this.cScalePeer1||Je(this.secondaryColor,45),this.cScalePeer2=this.cScalePeer2||Je(this.tertiaryColor,40);for(let t=0;t{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},PK=s(e=>{let t=new DD;return t.calculate(e),t},"getThemeVariables")});var ID,BK,$K=F(()=>{"use strict";Xi();hl();Wl();ID=class{static{s(this,"Theme")}constructor(){this.primaryColor="#eee",this.contrast="#707070",this.secondaryColor=Qe(this.contrast,55),this.background="#ffffff",this.tertiaryColor=ge(this.primaryColor,{h:-160}),this.primaryBorderColor=ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=ur(this.tertiaryColor,this.darkMode),this.primaryTextColor=je(this.primaryColor),this.secondaryTextColor=je(this.secondaryColor),this.tertiaryTextColor=je(this.tertiaryColor),this.lineColor=je(this.background),this.textColor=je(this.background),this.mainBkg="#eee",this.secondBkg="calculated",this.lineColor="#666",this.border1="#999",this.border2="calculated",this.note="#ffa",this.text="#333",this.critical="#d42",this.done="#bbb",this.arrowheadColor="#333333",this.fontFamily='"trebuchet ms", verdana, arial, sans-serif',this.fontSize="16px",this.THEME_COLOR_LIMIT=12,this.radius=5,this.strokeWidth=1,this.nodeBkg="calculated",this.nodeBorder="calculated",this.clusterBkg="calculated",this.clusterBorder="calculated",this.defaultLinkColor="calculated",this.titleColor="calculated",this.edgeLabelBackground="white",this.actorBorder="calculated",this.actorBkg="calculated",this.actorTextColor="calculated",this.actorLineColor=this.actorBorder,this.signalColor="calculated",this.signalTextColor="calculated",this.labelBoxBkgColor="calculated",this.labelBoxBorderColor="calculated",this.labelTextColor="calculated",this.loopTextColor="calculated",this.noteBorderColor="calculated",this.noteBkgColor="calculated",this.noteTextColor="calculated",this.activationBorderColor="#666",this.activationBkgColor="#f4f4f4",this.sequenceNumberColor="white",this.sectionBkgColor="calculated",this.altSectionBkgColor="white",this.sectionBkgColor2="calculated",this.excludeBkgColor="#eeeeee",this.taskBorderColor="calculated",this.taskBkgColor="calculated",this.taskTextLightColor="white",this.taskTextColor="calculated",this.taskTextDarkColor="calculated",this.taskTextOutsideColor="calculated",this.taskTextClickableColor="#003163",this.activeTaskBorderColor="calculated",this.activeTaskBkgColor="calculated",this.gridColor="calculated",this.doneTaskBkgColor="calculated",this.doneTaskBorderColor="calculated",this.critBkgColor="calculated",this.critBorderColor="calculated",this.todayLineColor="calculated",this.vertLineColor="calculated",this.personBorder=this.primaryBorderColor,this.personBkg=this.mainBkg,this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.noteFontWeight="normal",this.fontWeight="normal",this.rowOdd=this.rowOdd||Qe(this.mainBkg,75)||"#ffffff",this.rowEven=this.rowEven||"#f4f4f4",this.labelColor="black",this.errorBkgColor="#552222",this.errorTextColor="#552222",this.useGradient=!0,this.gradientStart=this.primaryBorderColor,this.gradientStop=this.secondaryBorderColor,this.dropShadow="drop-shadow( 1px 2px 2px rgba(185,185,185,1))"}updateColors(){this.secondBkg=Qe(this.contrast,55),this.border2=this.contrast,this.actorBorder=Qe(this.border1,23),this.actorBkg=this.mainBkg,this.actorTextColor=this.text,this.actorLineColor=this.actorBorder,this.rectBkgColor=this.rectBkgColor||this.tertiaryColor,this.signalColor=this.text,this.signalTextColor=this.text,this.labelBoxBkgColor=this.actorBkg,this.labelBoxBorderColor=this.actorBorder,this.labelTextColor=this.text,this.loopTextColor=this.text,this.noteBorderColor="#999",this.noteBkgColor="#666",this.noteTextColor="#fff",this.cScale0=this.cScale0||"#555",this.cScale1=this.cScale1||"#F4F4F4",this.cScale2=this.cScale2||"#555",this.cScale3=this.cScale3||"#BBB",this.cScale4=this.cScale4||"#777",this.cScale5=this.cScale5||"#999",this.cScale6=this.cScale6||"#DDD",this.cScale7=this.cScale7||"#FFF",this.cScale8=this.cScale8||"#DDD",this.cScale9=this.cScale9||"#BBB",this.cScale10=this.cScale10||"#999",this.cScale11=this.cScale11||"#777";for(let t=0;t{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},BK=s(e=>{let t=new ID;return t.calculate(e),t},"getThemeVariables")});var MD,FK,GK=F(()=>{"use strict";Xi();hl();Wl();MD=class{static{s(this,"Theme")}constructor(){this.background="#ffffff",this.primaryColor="#cccccc",this.mainBkg="#ffffff",this.noteBkgColor="#fff5ad",this.noteTextColor="#333",this.THEME_COLOR_LIMIT=12,this.radius=3,this.strokeWidth=2,this.primaryBorderColor=ur(this.primaryColor,this.darkMode),this.fontFamily="arial, sans-serif",this.fontSize="14px",this.nodeBorder="#000000",this.stateBorder="#000000",this.useGradient=!0,this.gradientStart="#0042eb",this.gradientStop="#eb0042",this.dropShadow="drop-shadow( 0px 1px 2px rgba(0, 0, 0, 0.25));",this.tertiaryColor="#ffffff",this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.noteFontWeight="normal",this.fontWeight="normal"}updateColors(){this.primaryTextColor=this.primaryTextColor||(this.darkMode?"#eee":"#333"),this.secondaryColor=this.secondaryColor||ge(this.primaryColor,{h:-120}),this.tertiaryColor=this.tertiaryColor||ge(this.primaryColor,{h:180,l:5}),this.primaryBorderColor=this.primaryBorderColor||ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=this.secondaryBorderColor||ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=this.tertiaryBorderColor||ur(this.tertiaryColor,this.darkMode),this.noteBorderColor=this.noteBorderColor||ur(this.noteBkgColor,this.darkMode),this.noteBkgColor=this.noteBkgColor||"#fff5ad",this.noteTextColor=this.noteTextColor||"#333",this.secondaryTextColor=this.secondaryTextColor||je(this.secondaryColor),this.tertiaryTextColor=this.tertiaryTextColor||je(this.tertiaryColor),this.lineColor=this.lineColor||je(this.background),this.arrowheadColor=this.arrowheadColor||je(this.background),this.textColor=this.textColor||this.primaryTextColor,this.border2=this.border2||this.tertiaryBorderColor,this.nodeBkg=this.nodeBkg||this.primaryColor,this.mainBkg=this.mainBkg||this.primaryColor,this.nodeBorder=this.nodeBorder||this.primaryBorderColor,this.clusterBkg=this.clusterBkg||this.tertiaryColor,this.clusterBorder=this.clusterBorder||this.tertiaryBorderColor,this.defaultLinkColor=this.defaultLinkColor||this.lineColor,this.titleColor=this.titleColor||this.tertiaryTextColor,this.edgeLabelBackground=this.edgeLabelBackground||(this.darkMode?Je(this.secondaryColor,30):this.secondaryColor),this.nodeTextColor=this.nodeTextColor||this.primaryTextColor,this.actorBorder=this.actorBorder||this.primaryBorderColor,this.actorBkg=this.actorBkg||this.mainBkg,this.actorTextColor=this.actorTextColor||this.primaryTextColor,this.actorLineColor=this.actorLineColor||this.actorBorder,this.labelBoxBkgColor=this.labelBoxBkgColor||this.actorBkg,this.signalColor=this.signalColor||this.textColor,this.signalTextColor=this.signalTextColor||this.textColor,this.labelBoxBorderColor=this.labelBoxBorderColor||this.actorBorder,this.labelTextColor=this.labelTextColor||this.actorTextColor,this.loopTextColor=this.loopTextColor||this.actorTextColor,this.activationBorderColor=this.activationBorderColor||Je(this.secondaryColor,10),this.activationBkgColor=this.activationBkgColor||this.secondaryColor,this.sequenceNumberColor=this.sequenceNumberColor||je(this.lineColor),this.rectBkgColor=this.rectBkgColor||this.tertiaryColor;let t="#ECECFE",r="#E9E9F1",n=ge(t,{h:180,l:5});if(this.sectionBkgColor=this.sectionBkgColor||n,this.altSectionBkgColor=this.altSectionBkgColor||"white",this.sectionBkgColor=this.sectionBkgColor||r,this.sectionBkgColor2=this.sectionBkgColor2||t,this.excludeBkgColor=this.excludeBkgColor||"#eeeeee",this.taskBorderColor=this.taskBorderColor||this.primaryBorderColor,this.taskBkgColor=this.taskBkgColor||t,this.activeTaskBorderColor=this.activeTaskBorderColor||t,this.activeTaskBkgColor=this.activeTaskBkgColor||Qe(t,23),this.gridColor=this.gridColor||"lightgrey",this.doneTaskBkgColor=this.doneTaskBkgColor||"lightgrey",this.doneTaskBorderColor=this.doneTaskBorderColor||"grey",this.critBorderColor=this.critBorderColor||"#ff8888",this.critBkgColor=this.critBkgColor||"red",this.todayLineColor=this.todayLineColor||"red",this.taskTextColor=this.taskTextColor||this.textColor,this.taskTextOutsideColor=this.taskTextOutsideColor||this.textColor,this.vertLineColor=this.vertLineColor||this.primaryBorderColor,this.taskTextLightColor=this.taskTextLightColor||this.textColor,this.taskTextColor=this.taskTextColor||this.primaryTextColor,this.taskTextDarkColor=this.taskTextDarkColor||this.textColor,this.taskTextClickableColor=this.taskTextClickableColor||"#003163",this.archEdgeColor=this.lineColor,this.archEdgeArrowColor=this.lineColor,this.personBorder=this.personBorder||this.primaryBorderColor,this.personBkg=this.personBkg||this.mainBkg,this.transitionColor=this.transitionColor||this.lineColor,this.transitionLabelColor=this.transitionLabelColor||this.textColor,this.stateLabelColor=this.stateLabelColor||this.stateBkg||this.primaryTextColor,this.stateBkg=this.stateBkg||this.mainBkg,this.labelBackgroundColor=this.labelBackgroundColor||this.stateBkg,this.compositeBackground=this.compositeBackground||this.background||this.tertiaryColor,this.altBackground=this.altBackground||"#f0f0f0",this.compositeTitleBackground=this.compositeTitleBackground||this.mainBkg,this.compositeBorder=this.compositeBorder||this.nodeBorder,this.innerEndBackground=this.nodeBorder,this.errorBkgColor=this.errorBkgColor||this.tertiaryColor,this.errorTextColor=this.errorTextColor||this.tertiaryTextColor,this.transitionColor=this.transitionColor||this.lineColor,this.specialStateColor=this.lineColor,this.cScale0=this.cScale0||t,this.cScale1=this.cScale1||r,this.cScale2=this.cScale2||n,this.cScale3=this.cScale3||ge(t,{h:30}),this.cScale4=this.cScale4||ge(t,{h:60}),this.cScale5=this.cScale5||ge(t,{h:90}),this.cScale6=this.cScale6||ge(t,{h:120}),this.cScale7=this.cScale7||ge(t,{h:150}),this.cScale8=this.cScale8||ge(t,{h:210,l:150}),this.cScale9=this.cScale9||ge(t,{h:270}),this.cScale10=this.cScale10||ge(t,{h:300}),this.cScale11=this.cScale11||ge(t,{h:330}),this.darkMode)for(let a=0;a{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},FK=s(e=>{let t=new MD;return t.calculate(e),t},"getThemeVariables")});var ND,zK,VK=F(()=>{"use strict";Xi();hl();Wl();ND=class{static{s(this,"Theme")}constructor(){this.background="#333",this.primaryColor="#1f2020",this.secondaryColor=Qe(this.primaryColor,16),this.tertiaryColor=ge(this.primaryColor,{h:-160}),this.primaryBorderColor=je(this.background),this.secondaryBorderColor=ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=ur(this.tertiaryColor,this.darkMode),this.primaryTextColor=je(this.primaryColor),this.secondaryTextColor=je(this.secondaryColor),this.tertiaryTextColor=je(this.tertiaryColor),this.mainBkg="#2a2020",this.secondBkg="calculated",this.mainContrastColor="lightgrey",this.darkTextColor=Qe(je("#323D47"),10),this.border1="#ccc",this.border2=Wi(255,255,255,.25),this.arrowheadColor=je(this.background),this.fontFamily="arial, sans-serif",this.fontSize="14px",this.labelBackground="#181818",this.textColor="#ccc",this.THEME_COLOR_LIMIT=12,this.radius=3,this.strokeWidth=1,this.noteBkgColor="#fff5ad",this.noteTextColor="#333",this.THEME_COLOR_LIMIT=12,this.fontFamily="arial, sans-serif",this.fontSize="14px",this.useGradient=!0,this.gradientStart="#0042eb",this.gradientStop="#eb0042",this.dropShadow="drop-shadow( 1px 2px 2px rgba(185,185,185,0.2))",this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.noteFontWeight="normal",this.fontWeight="normal"}updateColors(){if(this.primaryTextColor=this.primaryTextColor||(this.darkMode?"#eee":"#333"),this.secondaryColor=this.secondaryColor||ge(this.primaryColor,{h:-120}),this.tertiaryColor=this.tertiaryColor||ge(this.primaryColor,{h:180,l:5}),this.primaryBorderColor=this.primaryBorderColor||ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=this.secondaryBorderColor||ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=this.tertiaryBorderColor||ur(this.tertiaryColor,this.darkMode),this.noteBorderColor=this.noteBorderColor||ur(this.noteBkgColor,this.darkMode),this.noteBkgColor=this.noteBkgColor||"#fff5ad",this.noteTextColor=this.noteTextColor||"#333",this.secondaryTextColor=this.secondaryTextColor||je(this.secondaryColor),this.tertiaryTextColor=this.tertiaryTextColor||je(this.tertiaryColor),this.lineColor=this.lineColor||je(this.background),this.arrowheadColor=this.arrowheadColor||je(this.background),this.textColor=this.textColor||this.primaryTextColor,this.border2=this.border2||this.tertiaryBorderColor,this.nodeBkg=this.nodeBkg||this.primaryColor,this.mainBkg=this.mainBkg||this.primaryColor,this.nodeBorder=this.nodeBorder||this.border1,this.clusterBkg=this.clusterBkg||this.tertiaryColor,this.clusterBorder=this.clusterBorder||this.tertiaryBorderColor,this.defaultLinkColor=this.defaultLinkColor||this.lineColor,this.titleColor=this.titleColor||this.tertiaryTextColor,this.edgeLabelBackground=this.edgeLabelBackground||(this.darkMode?Je(this.secondaryColor,30):this.secondaryColor),this.nodeTextColor=this.nodeTextColor||this.primaryTextColor,this.actorBorder=this.actorBorder||this.primaryBorderColor,this.actorBkg=this.actorBkg||this.mainBkg,this.actorTextColor=this.actorTextColor||this.primaryTextColor,this.actorLineColor=this.actorLineColor||this.actorBorder,this.labelBoxBkgColor=this.labelBoxBkgColor||this.actorBkg,this.signalColor=this.signalColor||this.textColor,this.signalTextColor=this.signalTextColor||this.textColor,this.labelBoxBorderColor=this.labelBoxBorderColor||this.actorBorder,this.labelTextColor=this.labelTextColor||this.actorTextColor,this.loopTextColor=this.loopTextColor||this.actorTextColor,this.activationBorderColor=this.activationBorderColor||Je(this.secondaryColor,10),this.activationBkgColor=this.activationBkgColor||this.secondaryColor,this.sequenceNumberColor=this.sequenceNumberColor||je(this.lineColor),this.rectBkgColor=this.rectBkgColor||this.tertiaryColor,this.sectionBkgColor=this.sectionBkgColor||this.tertiaryColor,this.altSectionBkgColor=this.altSectionBkgColor||"white",this.sectionBkgColor=this.sectionBkgColor||this.secondaryColor,this.sectionBkgColor2=this.sectionBkgColor2||this.primaryColor,this.excludeBkgColor=this.excludeBkgColor||"#eeeeee",this.taskBorderColor=this.taskBorderColor||this.primaryBorderColor,this.taskBkgColor=this.taskBkgColor||this.primaryColor,this.activeTaskBorderColor=this.activeTaskBorderColor||this.primaryColor,this.activeTaskBkgColor=this.activeTaskBkgColor||Qe(this.primaryColor,23),this.gridColor=this.gridColor||"lightgrey",this.doneTaskBkgColor=this.doneTaskBkgColor||"lightgrey",this.doneTaskBorderColor=this.doneTaskBorderColor||"grey",this.critBorderColor=this.critBorderColor||"#ff8888",this.critBkgColor=this.critBkgColor||"red",this.todayLineColor=this.todayLineColor||"red",this.vertLineColor=this.vertLineColor||this.primaryBorderColor,this.taskTextColor=this.taskTextColor||this.textColor,this.taskTextOutsideColor=this.taskTextOutsideColor||this.textColor,this.taskTextLightColor=this.taskTextLightColor||this.textColor,this.taskTextColor=this.taskTextColor||this.primaryTextColor,this.taskTextDarkColor=this.taskTextDarkColor||this.textColor,this.taskTextClickableColor=this.taskTextClickableColor||"#003163",this.archEdgeColor=this.lineColor,this.archEdgeArrowColor=this.lineColor,this.personBorder=this.personBorder||this.primaryBorderColor,this.personBkg=this.personBkg||this.mainBkg,this.transitionColor=this.transitionColor||this.lineColor,this.transitionLabelColor=this.transitionLabelColor||this.textColor,this.stateLabelColor=this.stateLabelColor||this.stateBkg||this.primaryTextColor,this.stateBkg=this.stateBkg||this.mainBkg,this.labelBackgroundColor=this.labelBackgroundColor||this.stateBkg,this.compositeBackground=this.compositeBackground||this.background||this.tertiaryColor,this.altBackground=this.altBackground||"#f0f0f0",this.compositeTitleBackground=this.compositeTitleBackground||this.mainBkg,this.compositeBorder=this.compositeBorder||this.nodeBorder,this.innerEndBackground=this.nodeBorder,this.errorBkgColor=this.errorBkgColor||this.tertiaryColor,this.errorTextColor=this.errorTextColor||this.tertiaryTextColor,this.transitionColor=this.transitionColor||this.lineColor,this.specialStateColor=this.lineColor,this.cScale0=this.cScale0||this.primaryColor,this.cScale1=this.cScale1||this.secondaryColor,this.cScale2=this.cScale2||this.tertiaryColor,this.cScale3=this.cScale3||ge(this.primaryColor,{h:30}),this.cScale4=this.cScale4||ge(this.primaryColor,{h:60}),this.cScale5=this.cScale5||ge(this.primaryColor,{h:90}),this.cScale6=this.cScale6||ge(this.primaryColor,{h:120}),this.cScale7=this.cScale7||ge(this.primaryColor,{h:150}),this.cScale8=this.cScale8||ge(this.primaryColor,{h:210,l:150}),this.cScale9=this.cScale9||ge(this.primaryColor,{h:270}),this.cScale10=this.cScale10||ge(this.primaryColor,{h:300}),this.cScale11=this.cScale11||ge(this.primaryColor,{h:330}),this.darkMode)for(let r=0;r{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},zK=s(e=>{let t=new ND;return t.calculate(e),t},"getThemeVariables")});var PD,WK,qK=F(()=>{"use strict";Xi();hl();Wl();PD=class{static{s(this,"Theme")}constructor(){this.background="#ffffff",this.primaryColor="#cccccc",this.mainBkg="#ffffff",this.noteBkgColor="#fff5ad",this.noteTextColor="#28253D",this.THEME_COLOR_LIMIT=12,this.radius=12,this.strokeWidth=2,this.primaryBorderColor=ur("#28253D",this.darkMode),this.fontFamily='"Recursive Variable", arial, sans-serif',this.fontSize="14px",this.nodeBorder="#28253D",this.stateBorder="#28253D",this.useGradient=!1,this.gradientStart="#0042eb",this.gradientStop="#eb0042",this.dropShadow="url(#drop-shadow)",this.nodeShadow=!0,this.tertiaryColor="#ffffff",this.clusterBkg="#F9F9FB",this.clusterBorder="#BDBCCC",this.noteBorderColor="#FACC15",this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.actorBorder="#28253D",this.filterColor="#000000"}updateColors(){this.primaryTextColor=this.primaryTextColor||(this.darkMode?"#eee":"#28253D"),this.secondaryColor=this.secondaryColor||ge(this.primaryColor,{h:-120}),this.tertiaryColor=this.tertiaryColor||ge(this.primaryColor,{h:180,l:5}),this.primaryBorderColor=this.primaryBorderColor||ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=this.secondaryBorderColor||ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=this.tertiaryBorderColor||ur(this.tertiaryColor,this.darkMode),this.noteBorderColor=this.noteBorderColor||ur(this.noteBkgColor,this.darkMode),this.noteBkgColor=this.noteBkgColor||"#FEF9C3",this.noteTextColor=this.noteTextColor||"#28253D",this.secondaryTextColor=this.secondaryTextColor||je(this.secondaryColor),this.tertiaryTextColor=this.tertiaryTextColor||je(this.tertiaryColor),this.lineColor=this.lineColor||je(this.background),this.arrowheadColor=this.arrowheadColor||je(this.background),this.textColor=this.textColor||this.primaryTextColor,this.border2=this.border2||this.tertiaryBorderColor,this.nodeBkg=this.nodeBkg||this.primaryColor,this.mainBkg=this.mainBkg||this.primaryColor,this.nodeBorder=this.nodeBorder||this.primaryBorderColor,this.clusterBkg=this.clusterBkg||this.tertiaryColor,this.clusterBorder=this.clusterBorder||this.tertiaryBorderColor,this.defaultLinkColor=this.defaultLinkColor||this.lineColor,this.titleColor=this.titleColor||this.tertiaryTextColor,this.edgeLabelBackground=this.edgeLabelBackground||(this.darkMode?Je(this.secondaryColor,30):this.secondaryColor),this.nodeTextColor=this.nodeTextColor||this.primaryTextColor,this.noteFontWeight=600,this.actorBorder=this.actorBorder||this.primaryBorderColor,this.actorBkg=this.actorBkg||this.mainBkg,this.actorTextColor=this.actorTextColor||this.primaryTextColor,this.actorLineColor=this.actorLineColor||this.actorBorder,this.labelBoxBkgColor=this.labelBoxBkgColor||this.actorBkg,this.signalColor=this.signalColor||this.textColor,this.signalTextColor=this.signalTextColor||this.textColor,this.labelBoxBorderColor=this.labelBoxBorderColor||this.actorBorder,this.labelTextColor=this.labelTextColor||this.actorTextColor,this.loopTextColor=this.loopTextColor||this.actorTextColor,this.activationBorderColor=this.activationBorderColor||Je(this.secondaryColor,10),this.activationBkgColor=this.activationBkgColor||this.secondaryColor,this.sequenceNumberColor=this.sequenceNumberColor||je(this.lineColor),this.rectBkgColor=this.rectBkgColor||this.tertiaryColor;let t="#ECECFE",r="#E9E9F1",n=ge(t,{h:180,l:5});this.sectionBkgColor=this.sectionBkgColor||n,this.altSectionBkgColor=this.altSectionBkgColor||"white",this.sectionBkgColor=this.sectionBkgColor||r,this.sectionBkgColor2=this.sectionBkgColor2||t,this.excludeBkgColor=this.excludeBkgColor||"#eeeeee",this.taskBorderColor=this.taskBorderColor||this.primaryBorderColor,this.taskBkgColor=this.taskBkgColor||t,this.activeTaskBorderColor=this.activeTaskBorderColor||t,this.activeTaskBkgColor=this.activeTaskBkgColor||Qe(t,23),this.gridColor=this.gridColor||"lightgrey",this.doneTaskBkgColor=this.doneTaskBkgColor||"lightgrey",this.doneTaskBorderColor=this.doneTaskBorderColor||"grey",this.critBorderColor=this.critBorderColor||"#ff8888",this.critBkgColor=this.critBkgColor||"red",this.todayLineColor=this.todayLineColor||"red",this.taskTextColor=this.taskTextColor||this.textColor,this.vertLineColor=this.vertLineColor||this.primaryBorderColor,this.taskTextOutsideColor=this.taskTextOutsideColor||this.textColor,this.taskTextLightColor=this.taskTextLightColor||this.textColor,this.taskTextColor=this.taskTextColor||this.primaryTextColor,this.taskTextDarkColor=this.taskTextDarkColor||this.textColor,this.taskTextClickableColor=this.taskTextClickableColor||"#003163",this.archEdgeColor=this.lineColor,this.archEdgeArrowColor=this.lineColor,this.personBorder=this.personBorder||this.primaryBorderColor,this.personBkg=this.personBkg||this.mainBkg,this.transitionColor=this.transitionColor||this.lineColor,this.transitionLabelColor=this.transitionLabelColor||this.textColor,this.stateLabelColor=this.stateLabelColor||this.stateBkg||this.primaryTextColor,this.compositeTitleBackground="#F9F9FB",this.altBackground="#F9F9FB",this.stateEdgeLabelBackground="#FFFFFF",this.fontWeight=600,this.stateBkg=this.stateBkg||this.mainBkg,this.labelBackgroundColor=this.labelBackgroundColor||this.stateBkg,this.compositeBackground=this.compositeBackground||this.background||this.tertiaryColor,this.altBackground=this.altBackground||"#f0f0f0",this.compositeTitleBackground=this.compositeTitleBackground||this.mainBkg,this.compositeBorder=this.compositeBorder||this.nodeBorder,this.innerEndBackground=this.nodeBorder,this.errorBkgColor=this.errorBkgColor||this.tertiaryColor,this.errorTextColor=this.errorTextColor||this.tertiaryTextColor,this.transitionColor=this.transitionColor||this.lineColor,this.specialStateColor=this.lineColor;for(let a=0;a{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},WK=s(e=>{let t=new PD;return t.calculate(e),t},"getThemeVariables")});var OD,HK,UK=F(()=>{"use strict";Xi();hl();Wl();OD=class{static{s(this,"Theme")}constructor(){this.background="#333",this.primaryColor="#1f2020",this.secondaryColor=Qe(this.primaryColor,16),this.tertiaryColor=ge(this.primaryColor,{h:-160}),this.primaryBorderColor=je(this.background),this.secondaryBorderColor=ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=ur(this.tertiaryColor,this.darkMode),this.primaryTextColor=je(this.primaryColor),this.secondaryTextColor=je(this.secondaryColor),this.tertiaryTextColor=je(this.tertiaryColor),this.mainBkg="#111113",this.secondBkg="calculated",this.mainContrastColor="lightgrey",this.darkTextColor=Qe(je("#323D47"),10),this.border1="#ccc",this.border2=Wi(255,255,255,.25),this.arrowheadColor=je(this.background),this.fontFamily='"Recursive Variable", arial, sans-serif',this.fontSize="14px",this.labelBackground="#111113",this.textColor="#ccc",this.THEME_COLOR_LIMIT=12,this.radius=12,this.strokeWidth=2,this.noteBkgColor=this.noteBkgColor??"#FEF9C3",this.noteTextColor=this.noteTextColor??"#28253D",this.THEME_COLOR_LIMIT=12,this.fontFamily='"Recursive Variable", arial, sans-serif',this.fontSize="14px",this.nodeBorder="#FFFFFF",this.stateBorder="#FFFFFF",this.useGradient=!1,this.gradientStart="#0042eb",this.gradientStop="#eb0042",this.dropShadow="url(#drop-shadow)",this.nodeShadow=!0,this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.clusterBkg="#1E1A2E",this.clusterBorder="#BDBCCC",this.noteBorderColor="#FACC15",this.noteFontWeight=600,this.filterColor="#FFFFFF"}updateColors(){if(this.primaryTextColor=this.primaryTextColor||(this.darkMode?"#eee":"#FFFFFF"),this.secondaryColor=this.secondaryColor||ge(this.primaryColor,{h:-120}),this.tertiaryColor=this.tertiaryColor||ge(this.primaryColor,{h:180,l:5}),this.primaryBorderColor=this.primaryBorderColor||ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=this.secondaryBorderColor||ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=this.tertiaryBorderColor||ur(this.tertiaryColor,this.darkMode),this.noteBorderColor=this.noteBorderColor||ur(this.noteBkgColor,this.darkMode),this.noteBkgColor=this.noteBkgColor||"#fff5ad",this.noteTextColor=this.noteTextColor||"#FFFFFF",this.secondaryTextColor=this.secondaryTextColor||je(this.secondaryColor),this.tertiaryTextColor=this.tertiaryTextColor||je(this.tertiaryColor),this.lineColor=this.lineColor||je(this.background),this.arrowheadColor=this.arrowheadColor||je(this.background),this.textColor=this.textColor||this.primaryTextColor,this.border2=this.border2||this.tertiaryBorderColor,this.nodeBkg=this.nodeBkg||this.primaryColor,this.mainBkg=this.mainBkg||this.primaryColor,this.nodeBorder=this.nodeBorder||this.border1,this.clusterBkg=this.clusterBkg||this.tertiaryColor,this.clusterBorder=this.clusterBorder||this.tertiaryBorderColor,this.defaultLinkColor=this.defaultLinkColor||this.lineColor,this.titleColor=this.titleColor||this.tertiaryTextColor,this.edgeLabelBackground=this.edgeLabelBackground||(this.darkMode?Je(this.secondaryColor,30):this.secondaryColor),this.nodeTextColor=this.nodeTextColor||this.primaryTextColor,this.actorBorder="#FFFFFF",this.signalColor="#FFFFFF",this.labelBoxBorderColor="#BDBCCC",this.actorBorder=this.actorBorder||this.primaryBorderColor,this.actorBkg=this.actorBkg||this.mainBkg,this.actorTextColor=this.actorTextColor||this.primaryTextColor,this.actorLineColor=this.actorLineColor||this.actorBorder,this.labelBoxBkgColor=this.labelBoxBkgColor||this.actorBkg,this.signalColor=this.signalColor||this.textColor,this.signalTextColor=this.signalTextColor||this.textColor,this.labelBoxBorderColor=this.labelBoxBorderColor||this.actorBorder,this.labelTextColor=this.labelTextColor||this.actorTextColor,this.loopTextColor=this.loopTextColor||this.actorTextColor,this.activationBorderColor=this.activationBorderColor||Je(this.secondaryColor,10),this.activationBkgColor=this.activationBkgColor||this.secondaryColor,this.sequenceNumberColor=this.sequenceNumberColor||je(this.lineColor),this.rectBkgColor=this.rectBkgColor||this.tertiaryColor,this.sectionBkgColor=this.sectionBkgColor||this.tertiaryColor,this.altSectionBkgColor=this.altSectionBkgColor||"white",this.sectionBkgColor=this.sectionBkgColor||this.secondaryColor,this.sectionBkgColor2=this.sectionBkgColor2||this.primaryColor,this.excludeBkgColor=this.excludeBkgColor||"#eeeeee",this.taskBorderColor=this.taskBorderColor||this.primaryBorderColor,this.taskBkgColor=this.taskBkgColor||this.primaryColor,this.activeTaskBorderColor=this.activeTaskBorderColor||this.primaryColor,this.activeTaskBkgColor=this.activeTaskBkgColor||Qe(this.primaryColor,23),this.gridColor=this.gridColor||"lightgrey",this.doneTaskBkgColor=this.doneTaskBkgColor||"lightgrey",this.doneTaskBorderColor=this.doneTaskBorderColor||"grey",this.critBorderColor=this.critBorderColor||"#ff8888",this.critBkgColor=this.critBkgColor||"red",this.todayLineColor=this.todayLineColor||"red",this.taskTextColor=this.taskTextColor||this.textColor,this.taskTextOutsideColor=this.taskTextOutsideColor||this.textColor,this.taskTextLightColor=this.taskTextLightColor||this.textColor,this.taskTextColor=this.taskTextColor||this.primaryTextColor,this.taskTextDarkColor=this.taskTextDarkColor||this.textColor,this.taskTextClickableColor=this.taskTextClickableColor||"#003163",this.archEdgeColor=this.lineColor,this.archEdgeArrowColor=this.lineColor,this.personBorder=this.personBorder||this.primaryBorderColor,this.personBkg=this.personBkg||this.mainBkg,this.transitionColor=this.transitionColor||this.lineColor,this.transitionLabelColor=this.transitionLabelColor||this.textColor,this.stateLabelColor=this.stateLabelColor||this.stateBkg||this.primaryTextColor,this.vertLineColor=this.vertLineColor||this.primaryBorderColor,this.compositeBackground="#16141F",this.altBackground="#16141F",this.compositeTitleBackground="#16141F",this.stateEdgeLabelBackground="#16141F",this.fontWeight=600,this.stateBkg=this.stateBkg||this.mainBkg,this.labelBackgroundColor=this.labelBackgroundColor||this.stateBkg,this.compositeBackground=this.compositeBackground||this.background||this.tertiaryColor,this.altBackground=this.altBackground||"#f0f0f0",this.compositeTitleBackground=this.compositeTitleBackground||this.mainBkg,this.compositeBorder=this.compositeBorder||this.nodeBorder,this.innerEndBackground=this.nodeBorder,this.errorBkgColor=this.errorBkgColor||this.tertiaryColor,this.errorTextColor=this.errorTextColor||this.tertiaryTextColor,this.transitionColor=this.transitionColor||this.lineColor,this.specialStateColor=this.lineColor,this.cScale0=this.cScale0||this.primaryColor,this.cScale1=this.cScale1||this.secondaryColor,this.cScale2=this.cScale2||this.tertiaryColor,this.cScale3=this.cScale3||ge(this.primaryColor,{h:30}),this.cScale4=this.cScale4||ge(this.primaryColor,{h:60}),this.cScale5=this.cScale5||ge(this.primaryColor,{h:90}),this.cScale6=this.cScale6||ge(this.primaryColor,{h:120}),this.cScale7=this.cScale7||ge(this.primaryColor,{h:150}),this.cScale8=this.cScale8||ge(this.primaryColor,{h:210,l:150}),this.cScale9=this.cScale9||ge(this.primaryColor,{h:270}),this.cScale10=this.cScale10||ge(this.primaryColor,{h:300}),this.cScale11=this.cScale11||ge(this.primaryColor,{h:330}),this.darkMode)for(let r=0;r{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},HK=s(e=>{let t=new OD;return t.calculate(e),t},"getThemeVariables")});var BD,YK,jK=F(()=>{"use strict";Xi();hl();Wl();BD=class{static{s(this,"Theme")}constructor(){this.background="#ffffff",this.primaryColor="#cccccc",this.mainBkg="#ffffff",this.noteBkgColor="#fff5ad",this.noteTextColor="#28253D",this.THEME_COLOR_LIMIT=12,this.radius=12,this.strokeWidth=2,this.primaryBorderColor=ur(this.primaryColor,this.darkMode),this.fontFamily='"Recursive Variable", arial, sans-serif',this.fontSize="14px",this.nodeBorder="#28253D",this.stateBorder="#28253D",this.useGradient=!1,this.gradientStart="#0042eb",this.gradientStop="#eb0042",this.dropShadow="url(#drop-shadow)",this.nodeShadow=!0,this.tertiaryColor="#ffffff",this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.actorBorder="#28253D",this.noteBorderColor="#FACC15",this.noteFontWeight=600,this.borderColorArray=["#E879F9","#2DD4BF","#FB923C","#22D3EE","#4ADE80","#A78BFA","#F87171","#FACC15","#818CF8","#A3E635 ","#38BDF8","#FB7185"],this.bkgColorArray=["#FDF4FF","#F0FDFA","#FFF7ED","#ECFEFF","#F0FDF4","#F5F3FF","#FEF2F2","#FEFCE8","#EEF2FF","#F7FEE7","#F0F9FF","#FFF1F2"],this.filterColor="#000000"}updateColors(){this.primaryTextColor=this.primaryTextColor||(this.darkMode?"#eee":"#28253D"),this.secondaryColor=this.secondaryColor||ge(this.primaryColor,{h:-120}),this.tertiaryColor=this.tertiaryColor||ge(this.primaryColor,{h:180,l:5}),this.primaryBorderColor=this.primaryBorderColor||ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=this.secondaryBorderColor||ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=this.tertiaryBorderColor||ur(this.tertiaryColor,this.darkMode),this.noteBorderColor=this.noteBorderColor||ur(this.noteBkgColor,this.darkMode),this.noteBkgColor=this.noteBkgColor||"#fff5ad",this.noteTextColor=this.noteTextColor||"#28253D",this.secondaryTextColor=this.secondaryTextColor||je(this.secondaryColor),this.tertiaryTextColor=this.tertiaryTextColor||je(this.tertiaryColor),this.lineColor=this.lineColor||je(this.background),this.arrowheadColor=this.arrowheadColor||je(this.background),this.textColor=this.textColor||this.primaryTextColor,this.border2=this.border2||this.tertiaryBorderColor,this.nodeBkg=this.nodeBkg||this.primaryColor,this.mainBkg=this.mainBkg||this.primaryColor,this.nodeBorder=this.nodeBorder||this.primaryBorderColor,this.clusterBkg=this.clusterBkg||this.tertiaryColor,this.clusterBorder=this.clusterBorder||this.tertiaryBorderColor,this.defaultLinkColor=this.defaultLinkColor||this.lineColor,this.titleColor=this.titleColor||this.tertiaryTextColor,this.edgeLabelBackground=this.edgeLabelBackground||(this.darkMode?Je(this.secondaryColor,30):this.secondaryColor),this.nodeTextColor=this.nodeTextColor||this.primaryTextColor,this.actorBorder=this.actorBorder||this.primaryBorderColor,this.actorBkg=this.actorBkg||this.mainBkg,this.actorTextColor=this.actorTextColor||this.primaryTextColor,this.actorLineColor=this.actorLineColor||this.actorBorder,this.labelBoxBkgColor=this.labelBoxBkgColor||this.actorBkg,this.signalColor=this.signalColor||this.textColor,this.signalTextColor=this.signalTextColor||this.textColor,this.labelBoxBorderColor=this.labelBoxBorderColor||this.actorBorder,this.labelTextColor=this.labelTextColor||this.actorTextColor,this.loopTextColor=this.loopTextColor||this.actorTextColor,this.activationBorderColor=this.activationBorderColor||Je(this.secondaryColor,10),this.activationBkgColor=this.activationBkgColor||this.secondaryColor,this.sequenceNumberColor=this.sequenceNumberColor||je(this.lineColor),this.rectBkgColor=this.rectBkgColor||this.tertiaryColor;let t="#ECECFE",r="#E9E9F1",n=ge(t,{h:180,l:5});this.sectionBkgColor=this.sectionBkgColor||n,this.altSectionBkgColor=this.altSectionBkgColor||"white",this.sectionBkgColor=this.sectionBkgColor||r,this.sectionBkgColor2=this.sectionBkgColor2||t,this.excludeBkgColor=this.excludeBkgColor||"#eeeeee",this.taskBorderColor=this.taskBorderColor||this.primaryBorderColor,this.taskBkgColor=this.taskBkgColor||t,this.activeTaskBorderColor=this.activeTaskBorderColor||t,this.activeTaskBkgColor=this.activeTaskBkgColor||Qe(t,23),this.gridColor=this.gridColor||"lightgrey",this.doneTaskBkgColor=this.doneTaskBkgColor||"lightgrey",this.doneTaskBorderColor=this.doneTaskBorderColor||"grey",this.critBorderColor=this.critBorderColor||"#ff8888",this.critBkgColor=this.critBkgColor||"red",this.todayLineColor=this.todayLineColor||"red",this.taskTextColor=this.taskTextColor||this.textColor,this.vertLineColor=this.vertLineColor||this.primaryBorderColor,this.taskTextOutsideColor=this.taskTextOutsideColor||this.textColor,this.taskTextLightColor=this.taskTextLightColor||this.textColor,this.taskTextColor=this.taskTextColor||this.primaryTextColor,this.taskTextDarkColor=this.taskTextDarkColor||this.textColor,this.taskTextClickableColor=this.taskTextClickableColor||"#003163",this.archEdgeColor=this.lineColor,this.archEdgeArrowColor=this.lineColor,this.personBorder=this.personBorder||this.primaryBorderColor,this.personBkg=this.personBkg||this.mainBkg,this.transitionColor=this.transitionColor||this.lineColor,this.transitionLabelColor=this.transitionLabelColor||this.textColor,this.stateLabelColor=this.stateLabelColor||this.stateBkg||this.primaryTextColor,this.stateBkg=this.stateBkg||this.mainBkg,this.labelBackgroundColor=this.labelBackgroundColor||this.stateBkg,this.compositeBackground=this.compositeBackground||this.background||this.tertiaryColor,this.altBackground=this.altBackground||"#f0f0f0",this.compositeTitleBackground=this.compositeTitleBackground||this.mainBkg,this.compositeBorder=this.compositeBorder||this.nodeBorder,this.innerEndBackground=this.nodeBorder,this.errorBkgColor=this.errorBkgColor||this.tertiaryColor,this.errorTextColor=this.errorTextColor||this.tertiaryTextColor,this.transitionColor=this.transitionColor||this.lineColor,this.specialStateColor=this.lineColor,this.cScale0=this.cScale0||"#f4a8ff",this.cScale1=this.cScale1||"#46ecd5",this.cScale2=this.cScale2||"#ffb86a",this.cScale3=this.cScale3||"#dab2ff",this.cScale4=this.cScale4||"#7bf1a8",this.cScale5=this.cScale5||"#c4b4ff",this.cScale6=this.cScale6||"#ffa2a2",this.cScale7=this.cScale7||"#ffdf20",this.cScale8=this.cScale8||"#a3b3ff",this.cScale9=this.cScale9||"#bbf451",this.cScale10=this.cScale10||"#74d4ff",this.cScale11=this.cScale11||"#ffa1ad";for(let a=0;a{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},YK=s(e=>{let t=new BD;return t.calculate(e),t},"getThemeVariables")});var $D,XK,KK=F(()=>{"use strict";Xi();hl();Wl();$D=class{static{s(this,"Theme")}constructor(){this.background="#333",this.primaryColor="#1f2020",this.secondaryColor=Qe(this.primaryColor,16),this.tertiaryColor=ge(this.primaryColor,{h:-160}),this.primaryBorderColor=je(this.background),this.secondaryBorderColor=ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=ur(this.tertiaryColor,this.darkMode),this.primaryTextColor=je(this.primaryColor),this.secondaryTextColor=je(this.secondaryColor),this.tertiaryTextColor=je(this.tertiaryColor),this.mainBkg="#111113",this.secondBkg="calculated",this.mainContrastColor="lightgrey",this.darkTextColor=Qe(je("#323D47"),10),this.border1="#ccc",this.border2=Wi(255,255,255,.25),this.arrowheadColor=je(this.background),this.fontFamily='"Recursive Variable", arial, sans-serif',this.fontSize="14px",this.labelBackground="#111113",this.textColor="#ccc",this.THEME_COLOR_LIMIT=12,this.radius=12,this.strokeWidth=2,this.noteBkgColor=this.noteBkgColor??"#FEF9C3",this.noteTextColor=this.noteTextColor??"#28253D",this.THEME_COLOR_LIMIT=12,this.fontFamily='"Recursive Variable", arial, sans-serif',this.fontSize="14px",this.nodeBorder="#FFFFFF",this.stateBorder="#FFFFFF",this.useGradient=!1,this.gradientStart="#0042eb",this.gradientStop="#eb0042",this.dropShadow="url(#drop-shadow)",this.nodeShadow=!0,this.archEdgeColor="calculated",this.archEdgeArrowColor="calculated",this.archEdgeWidth="3",this.archGroupBorderColor=this.primaryBorderColor,this.archGroupBorderWidth="2px",this.clusterBkg="#1E1A2E",this.clusterBorder="#BDBCCC",this.noteBorderColor="#FACC15",this.noteFontWeight=600,this.borderColorArray=["#E879F9","#2DD4BF","#FB923C","#22D3EE","#4ADE80","#A78BFA","#F87171","#FACC15","#818CF8","#A3E635 ","#38BDF8","#FB7185"],this.bkgColorArray=[],this.filterColor="#FFFFFF"}updateColors(){this.primaryTextColor=this.primaryTextColor||(this.darkMode?"#eee":"#FFFFFF"),this.secondaryColor=this.secondaryColor||ge(this.primaryColor,{h:-120}),this.tertiaryColor=this.tertiaryColor||ge(this.primaryColor,{h:180,l:5}),this.primaryBorderColor=this.primaryBorderColor||ur(this.primaryColor,this.darkMode),this.secondaryBorderColor=this.secondaryBorderColor||ur(this.secondaryColor,this.darkMode),this.tertiaryBorderColor=this.tertiaryBorderColor||ur(this.tertiaryColor,this.darkMode),this.noteBorderColor=this.noteBorderColor||ur(this.noteBkgColor,this.darkMode),this.noteBkgColor=this.noteBkgColor||"#fff5ad",this.noteTextColor=this.noteTextColor||"#FFFFFF",this.secondaryTextColor=this.secondaryTextColor||je(this.secondaryColor),this.tertiaryTextColor=this.tertiaryTextColor||je(this.tertiaryColor),this.lineColor=this.lineColor||je(this.background),this.arrowheadColor=this.arrowheadColor||je(this.background),this.textColor=this.textColor||this.primaryTextColor,this.border2=this.border2||this.tertiaryBorderColor,this.nodeBkg=this.nodeBkg||this.primaryColor,this.mainBkg=this.mainBkg||this.primaryColor,this.nodeBorder=this.nodeBorder||this.border1,this.clusterBkg=this.clusterBkg||this.tertiaryColor,this.clusterBorder=this.clusterBorder||this.tertiaryBorderColor,this.defaultLinkColor=this.defaultLinkColor||this.lineColor,this.titleColor=this.titleColor||this.tertiaryTextColor,this.edgeLabelBackground=this.edgeLabelBackground||(this.darkMode?Je(this.secondaryColor,30):this.secondaryColor),this.nodeTextColor=this.nodeTextColor||this.primaryTextColor,this.actorBorder="#FFFFFF",this.signalColor="#FFFFFF",this.labelBoxBorderColor="#BDBCCC",this.actorBorder=this.actorBorder||this.primaryBorderColor,this.actorBkg=this.actorBkg||this.mainBkg,this.actorTextColor=this.actorTextColor||this.primaryTextColor,this.actorLineColor=this.actorLineColor||this.actorBorder,this.labelBoxBkgColor=this.labelBoxBkgColor||this.actorBkg,this.signalColor=this.signalColor||this.textColor,this.signalTextColor=this.signalTextColor||this.textColor,this.labelBoxBorderColor=this.labelBoxBorderColor||this.actorBorder,this.labelTextColor=this.labelTextColor||this.actorTextColor,this.loopTextColor=this.loopTextColor||this.actorTextColor,this.activationBorderColor=this.activationBorderColor||Je(this.secondaryColor,10),this.activationBkgColor=this.activationBkgColor||this.secondaryColor,this.sequenceNumberColor=this.sequenceNumberColor||je(this.lineColor),this.rectBkgColor=this.rectBkgColor||this.tertiaryColor,this.rootLabelColor="#FFFFFF",this.sectionBkgColor=this.sectionBkgColor||this.tertiaryColor,this.altSectionBkgColor=this.altSectionBkgColor||"white",this.sectionBkgColor=this.sectionBkgColor||this.secondaryColor,this.sectionBkgColor2=this.sectionBkgColor2||this.primaryColor,this.excludeBkgColor=this.excludeBkgColor||"#eeeeee",this.taskBorderColor=this.taskBorderColor||this.primaryBorderColor,this.taskBkgColor=this.taskBkgColor||this.primaryColor,this.activeTaskBorderColor=this.activeTaskBorderColor||this.primaryColor,this.activeTaskBkgColor=this.activeTaskBkgColor||Qe(this.primaryColor,23),this.gridColor=this.gridColor||"lightgrey",this.doneTaskBkgColor=this.doneTaskBkgColor||"lightgrey",this.doneTaskBorderColor=this.doneTaskBorderColor||"grey",this.critBorderColor=this.critBorderColor||"#ff8888",this.critBkgColor=this.critBkgColor||"red",this.todayLineColor=this.todayLineColor||"red",this.taskTextColor=this.taskTextColor||this.textColor,this.vertLineColor=this.vertLineColor||this.primaryBorderColor,this.taskTextOutsideColor=this.taskTextOutsideColor||this.textColor,this.taskTextLightColor=this.taskTextLightColor||this.textColor,this.taskTextColor=this.taskTextColor||this.primaryTextColor,this.taskTextDarkColor=this.taskTextDarkColor||this.textColor,this.taskTextClickableColor=this.taskTextClickableColor||"#003163",this.archEdgeColor=this.lineColor,this.archEdgeArrowColor=this.lineColor,this.personBorder=this.personBorder||this.primaryBorderColor,this.personBkg=this.personBkg||this.mainBkg,this.transitionColor=this.transitionColor||this.lineColor,this.transitionLabelColor=this.transitionLabelColor||this.textColor,this.stateLabelColor=this.stateLabelColor||this.stateBkg||this.primaryTextColor,this.stateBkg=this.stateBkg||this.mainBkg,this.labelBackgroundColor=this.labelBackgroundColor||this.stateBkg,this.compositeBackground=this.compositeBackground||this.background||this.tertiaryColor,this.altBackground=this.altBackground||"#f0f0f0",this.compositeTitleBackground=this.compositeTitleBackground||this.mainBkg,this.compositeBorder=this.compositeBorder||this.nodeBorder,this.innerEndBackground=this.nodeBorder,this.errorBkgColor=this.errorBkgColor||this.tertiaryColor,this.errorTextColor=this.errorTextColor||this.tertiaryTextColor,this.transitionColor=this.transitionColor||this.lineColor,this.specialStateColor=this.lineColor,this.cScale0=this.cScale0||"#f4a8ff",this.cScale1=this.cScale1||"#46ecd5",this.cScale2=this.cScale2||"#ffb86a",this.cScale3=this.cScale3||"#dab2ff",this.cScale4=this.cScale4||"#7bf1a8",this.cScale5=this.cScale5||"#c4b4ff",this.cScale6=this.cScale6||"#ffa2a2",this.cScale7=this.cScale7||"#ffdf20",this.cScale8=this.cScale8||"#a3b3ff",this.cScale9=this.cScale9||"#bbf451",this.cScale10=this.cScale10||"#74d4ff",this.cScale11=this.cScale11||"#ffa1ad";for(let r=0;r{this[n]=t[n]}),this.updateColors(),r.forEach(n=>{this[n]=t[n]})}},XK=s(e=>{let t=new $D;return t.calculate(e),t},"getThemeVariables")});var dl,Cw=F(()=>{"use strict";IK();NK();Mc();OK();$K();GK();VK();qK();UK();jK();KK();dl={base:{getThemeVariables:DK},dark:{getThemeVariables:MK},default:{getThemeVariables:ka},forest:{getThemeVariables:PK},neutral:{getThemeVariables:BK},neo:{getThemeVariables:FK},"neo-dark":{getThemeVariables:zK},redux:{getThemeVariables:WK},"redux-dark":{getThemeVariables:HK},"redux-color":{getThemeVariables:YK},"redux-dark-color":{getThemeVariables:XK}}});var Ea,ZK=F(()=>{"use strict";Ea={flowchart:{useMaxWidth:!0,titleTopMargin:25,subGraphTitleMargin:{top:0,bottom:0},diagramPadding:8,htmlLabels:null,nodeSpacing:50,rankSpacing:50,curve:"basis",padding:15,defaultRenderer:"dagre-wrapper",wrappingWidth:200,inheritDir:!1},swimlane:{useMaxWidth:!0,lineHops:"arc",ignoreCrossLaneEdges:!0,optimizeRanksByCrossings:!0,automaticLaneOrdering:!1},sequence:{useMaxWidth:!0,hideUnusedParticipants:!1,activationWidth:10,diagramMarginX:50,diagramMarginY:10,actorMargin:50,width:150,height:65,boxMargin:10,boxTextMargin:5,noteMargin:10,messageMargin:35,messageAlign:"center",mirrorActors:!0,forceMenus:!1,bottomMarginAdj:1,rightAngles:!1,showSequenceNumbers:!1,actorFontSize:14,actorFontFamily:'"Open Sans", sans-serif',actorFontWeight:400,noteFontSize:14,noteFontFamily:'"trebuchet ms", verdana, arial, sans-serif',noteFontWeight:400,noteAlign:"center",messageFontSize:16,messageFontFamily:'"trebuchet ms", verdana, arial, sans-serif',messageFontWeight:400,wrap:!1,wrapPadding:10,labelBoxWidth:50,labelBoxHeight:20},gantt:{useMaxWidth:!0,titleTopMargin:25,barHeight:20,barGap:4,topPadding:50,rightPadding:75,leftPadding:75,gridLineStartPadding:35,fontSize:11,sectionFontSize:11,numberSectionStyles:4,axisFormat:"%Y-%m-%d",topAxis:!1,displayMode:"",weekday:"sunday"},journey:{useMaxWidth:!0,diagramMarginX:50,diagramMarginY:10,leftMargin:150,maxLabelWidth:360,width:150,height:50,boxMargin:10,boxTextMargin:5,noteMargin:10,messageMargin:35,messageAlign:"center",bottomMarginAdj:1,rightAngles:!1,taskFontSize:14,taskFontFamily:'"Open Sans", sans-serif',taskMargin:50,activationWidth:10,textPlacement:"fo",actorColours:["#8FBC8F","#7CFC00","#00FFFF","#20B2AA","#B0E0E6","#FFFFE0"],sectionFills:["#191970","#8B008B","#4B0082","#2F4F4F","#800000","#8B4513","#00008B"],sectionColours:["#fff"],titleColor:"",titleFontFamily:'"trebuchet ms", verdana, arial, sans-serif',titleFontSize:"4ex"},class:{useMaxWidth:!0,titleTopMargin:25,arrowMarkerAbsolute:!1,dividerMargin:10,padding:5,textHeight:10,defaultRenderer:"dagre-wrapper",htmlLabels:!1,hideEmptyMembersBox:!1,hierarchicalNamespaces:!0},state:{useMaxWidth:!0,titleTopMargin:25,dividerMargin:10,sizeUnit:5,padding:8,textHeight:10,titleShift:-15,noteMargin:10,forkWidth:70,forkHeight:7,miniPadding:2,fontSizeFactor:5.02,fontSize:24,labelHeight:16,edgeLengthFactor:"20",compositTitleSize:35,radius:5,defaultRenderer:"dagre-wrapper"},er:{useMaxWidth:!0,titleTopMargin:25,diagramPadding:20,layoutDirection:"TB",minEntityWidth:100,minEntityHeight:75,entityPadding:15,nodeSpacing:140,rankSpacing:80,stroke:"gray",fill:"honeydew",fontSize:12},pie:{useMaxWidth:!0,textPosition:.75,donutHole:0,legendPosition:"right",highlightSlice:""},quadrantChart:{useMaxWidth:!0,chartWidth:500,chartHeight:500,titleFontSize:20,titlePadding:10,quadrantPadding:5,xAxisLabelPadding:5,yAxisLabelPadding:5,xAxisLabelFontSize:16,yAxisLabelFontSize:16,quadrantLabelFontSize:16,quadrantTextTopPadding:5,pointTextPadding:5,pointLabelFontSize:12,pointRadius:5,xAxisPosition:"top",yAxisPosition:"left",quadrantInternalBorderStrokeWidth:1,quadrantExternalBorderStrokeWidth:2},xyChart:{useMaxWidth:!0,width:700,height:500,titleFontSize:20,titlePadding:10,showDataLabel:!1,showDataLabelOutsideBar:!1,showTitle:!0,showLegend:!0,legendFontSize:14,legendPadding:10,xAxis:{$ref:"#/$defs/XYChartAxisConfig",showLabel:!0,labelFontSize:14,labelPadding:5,showTitle:!0,titleFontSize:16,titlePadding:5,showTick:!0,tickLength:5,tickWidth:2,showAxisLine:!0,axisLineWidth:2,labelRotation:0},yAxis:{$ref:"#/$defs/XYChartAxisConfig",showLabel:!0,labelFontSize:14,labelPadding:5,showTitle:!0,titleFontSize:16,titlePadding:5,showTick:!0,tickLength:5,tickWidth:2,showAxisLine:!0,axisLineWidth:2,labelRotation:0},chartOrientation:"vertical",plotReservedSpacePercent:50},requirement:{useMaxWidth:!0,rect_fill:"#f9f9f9",text_color:"#333",rect_border_size:"0.5px",rect_border_color:"#bbb",rect_min_width:200,rect_min_height:200,fontSize:14,rect_padding:10,line_height:20},mindmap:{useMaxWidth:!0,padding:10,maxNodeWidth:200,layoutAlgorithm:"cose-bilkent"},ishikawa:{useMaxWidth:!0,diagramPadding:20},kanban:{useMaxWidth:!0,padding:8,sectionWidth:200,ticketBaseUrl:""},timeline:{useMaxWidth:!0,diagramMarginX:50,diagramMarginY:10,leftMargin:150,width:150,height:50,boxMargin:10,boxTextMargin:5,noteMargin:10,messageMargin:35,messageAlign:"center",bottomMarginAdj:1,rightAngles:!1,taskFontSize:14,taskFontFamily:'"Open Sans", sans-serif',taskMargin:50,activationWidth:10,textPlacement:"fo",actorColours:["#8FBC8F","#7CFC00","#00FFFF","#20B2AA","#B0E0E6","#FFFFE0"],sectionFills:["#191970","#8B008B","#4B0082","#2F4F4F","#800000","#8B4513","#00008B"],sectionColours:["#fff"],disableMulticolor:!1},gitGraph:{useMaxWidth:!0,titleTopMargin:25,diagramPadding:8,nodeLabel:{width:75,height:100,x:-25,y:0},mainBranchName:"main",mainBranchOrder:0,showCommitLabel:!0,showBranches:!0,rotateCommitLabel:!0,parallelCommits:!1,arrowMarkerAbsolute:!1},c4:{useMaxWidth:!0,diagramMarginX:50,diagramMarginY:10,c4ShapeMargin:50,c4ShapePadding:20,width:216,height:60,boxMargin:10,c4ShapeInRow:4,nextLinePaddingX:0,c4BoundaryInRow:2,personFontSize:14,personFontFamily:'"Open Sans", sans-serif',personFontWeight:"normal",external_personFontSize:14,external_personFontFamily:'"Open Sans", sans-serif',external_personFontWeight:"normal",systemFontSize:14,systemFontFamily:'"Open Sans", sans-serif',systemFontWeight:"normal",external_systemFontSize:14,external_systemFontFamily:'"Open Sans", sans-serif',external_systemFontWeight:"normal",system_dbFontSize:14,system_dbFontFamily:'"Open Sans", sans-serif',system_dbFontWeight:"normal",external_system_dbFontSize:14,external_system_dbFontFamily:'"Open Sans", sans-serif',external_system_dbFontWeight:"normal",system_queueFontSize:14,system_queueFontFamily:'"Open Sans", sans-serif',system_queueFontWeight:"normal",external_system_queueFontSize:14,external_system_queueFontFamily:'"Open Sans", sans-serif',external_system_queueFontWeight:"normal",boundaryFontSize:14,boundaryFontFamily:'"Open Sans", sans-serif',boundaryFontWeight:"normal",messageFontSize:12,messageFontFamily:'"Open Sans", sans-serif',messageFontWeight:"normal",containerFontSize:14,containerFontFamily:'"Open Sans", sans-serif',containerFontWeight:"normal",external_containerFontSize:14,external_containerFontFamily:'"Open Sans", sans-serif',external_containerFontWeight:"normal",container_dbFontSize:14,container_dbFontFamily:'"Open Sans", sans-serif',container_dbFontWeight:"normal",external_container_dbFontSize:14,external_container_dbFontFamily:'"Open Sans", sans-serif',external_container_dbFontWeight:"normal",container_queueFontSize:14,container_queueFontFamily:'"Open Sans", sans-serif',container_queueFontWeight:"normal",external_container_queueFontSize:14,external_container_queueFontFamily:'"Open Sans", sans-serif',external_container_queueFontWeight:"normal",componentFontSize:14,componentFontFamily:'"Open Sans", sans-serif',componentFontWeight:"normal",external_componentFontSize:14,external_componentFontFamily:'"Open Sans", sans-serif',external_componentFontWeight:"normal",component_dbFontSize:14,component_dbFontFamily:'"Open Sans", sans-serif',component_dbFontWeight:"normal",external_component_dbFontSize:14,external_component_dbFontFamily:'"Open Sans", sans-serif',external_component_dbFontWeight:"normal",component_queueFontSize:14,component_queueFontFamily:'"Open Sans", sans-serif',component_queueFontWeight:"normal",external_component_queueFontSize:14,external_component_queueFontFamily:'"Open Sans", sans-serif',external_component_queueFontWeight:"normal",wrap:!0,wrapPadding:10,person_bg_color:"#08427B",person_border_color:"#073B6F",external_person_bg_color:"#686868",external_person_border_color:"#8A8A8A",system_bg_color:"#1168BD",system_border_color:"#3C7FC0",system_db_bg_color:"#1168BD",system_db_border_color:"#3C7FC0",system_queue_bg_color:"#1168BD",system_queue_border_color:"#3C7FC0",external_system_bg_color:"#999999",external_system_border_color:"#8A8A8A",external_system_db_bg_color:"#999999",external_system_db_border_color:"#8A8A8A",external_system_queue_bg_color:"#999999",external_system_queue_border_color:"#8A8A8A",container_bg_color:"#438DD5",container_border_color:"#3C7FC0",container_db_bg_color:"#438DD5",container_db_border_color:"#3C7FC0",container_queue_bg_color:"#438DD5",container_queue_border_color:"#3C7FC0",external_container_bg_color:"#B3B3B3",external_container_border_color:"#A6A6A6",external_container_db_bg_color:"#B3B3B3",external_container_db_border_color:"#A6A6A6",external_container_queue_bg_color:"#B3B3B3",external_container_queue_border_color:"#A6A6A6",component_bg_color:"#85BBF0",component_border_color:"#78A8D8",component_db_bg_color:"#85BBF0",component_db_border_color:"#78A8D8",component_queue_bg_color:"#85BBF0",component_queue_border_color:"#78A8D8",external_component_bg_color:"#CCCCCC",external_component_border_color:"#BFBFBF",external_component_db_bg_color:"#CCCCCC",external_component_db_border_color:"#BFBFBF",external_component_queue_bg_color:"#CCCCCC",external_component_queue_border_color:"#BFBFBF"},sankey:{useMaxWidth:!0,width:600,height:400,linkColor:"gradient",nodeAlignment:"justify",showValues:!0,prefix:"",suffix:"",nodeWidth:10,nodePadding:12,labelStyle:"legacy"},block:{useMaxWidth:!0,padding:8},packet:{useMaxWidth:!0,rowHeight:32,bitWidth:32,bitsPerRow:32,showBits:!0,paddingX:5,paddingY:5},treeView:{useMaxWidth:!0,rowIndent:10,paddingX:5,paddingY:5,lineThickness:1,showIcons:!1,defaultIconPack:"",filenameIcons:{},extensionIcons:{}},architecture:{useMaxWidth:!0,padding:40,iconSize:80,fontSize:16,randomize:!1,nodeSeparation:75,idealEdgeLengthMultiplier:1.5,edgeElasticity:.45,numIter:2500,seed:1},eventmodeling:{useMaxWidth:!0,padding:30,rowHeight:32},radar:{useMaxWidth:!0,width:600,height:600,marginTop:50,marginRight:50,marginBottom:50,marginLeft:50,axisScaleFactor:1,axisLabelFactor:1.05,curveTension:.17},venn:{useMaxWidth:!0,width:800,height:450,padding:8,useDebugLayout:!1},cynefin:{useMaxWidth:!0,width:800,height:600,padding:40,showDomainDescriptions:!0,boundaryAmplitude:8,seed:0},theme:"default",look:"classic",handDrawnSeed:0,layout:"dagre",maxTextSize:5e4,maxEdges:500,darkMode:!1,fontFamily:'"trebuchet ms", verdana, arial, sans-serif;',logLevel:5,securityLevel:"strict",startOnLoad:!0,arrowMarkerAbsolute:!1,secure:["secure","securityLevel","startOnLoad","maxTextSize","suppressErrorRendering","maxEdges"],legacyMathML:!1,forceLegacyMathML:!1,deterministicIds:!1,fontSize:16,markdownAutoWrap:!0,suppressErrorRendering:!1}});var QK,JK,eZ,mr,Qi=F(()=>{"use strict";Cw();ZK();QK={...Ea,deterministicIDSeed:void 0,elk:{mergeEdges:!1,nodePlacementStrategy:"BRANDES_KOEPF",nodePlacementAlignment:"NONE",forceNodeModelOrder:!1,considerModelOrder:"NODES_AND_EDGES",keepEntryNodeOnTop:!1},themeCSS:void 0,themeVariables:dl.default.getThemeVariables(),sequence:{...Ea.sequence,messageFont:s(function(){return{fontFamily:this.messageFontFamily,fontSize:this.messageFontSize,fontWeight:this.messageFontWeight}},"messageFont"),noteFont:s(function(){return{fontFamily:this.noteFontFamily,fontSize:this.noteFontSize,fontWeight:this.noteFontWeight}},"noteFont"),actorFont:s(function(){return{fontFamily:this.actorFontFamily,fontSize:this.actorFontSize,fontWeight:this.actorFontWeight}},"actorFont")},class:{defaultRenderer:"dagre-wrapper",hideEmptyMembersBox:!1,hierarchicalNamespaces:!0},gantt:{...Ea.gantt,tickInterval:void 0,useWidth:void 0},c4:{...Ea.c4,useWidth:void 0,personFont:s(function(){return{fontFamily:this.personFontFamily,fontSize:this.personFontSize,fontWeight:this.personFontWeight}},"personFont"),flowchart:{...Ea.flowchart,inheritDir:!1},external_personFont:s(function(){return{fontFamily:this.external_personFontFamily,fontSize:this.external_personFontSize,fontWeight:this.external_personFontWeight}},"external_personFont"),systemFont:s(function(){return{fontFamily:this.systemFontFamily,fontSize:this.systemFontSize,fontWeight:this.systemFontWeight}},"systemFont"),external_systemFont:s(function(){return{fontFamily:this.external_systemFontFamily,fontSize:this.external_systemFontSize,fontWeight:this.external_systemFontWeight}},"external_systemFont"),system_dbFont:s(function(){return{fontFamily:this.system_dbFontFamily,fontSize:this.system_dbFontSize,fontWeight:this.system_dbFontWeight}},"system_dbFont"),external_system_dbFont:s(function(){return{fontFamily:this.external_system_dbFontFamily,fontSize:this.external_system_dbFontSize,fontWeight:this.external_system_dbFontWeight}},"external_system_dbFont"),system_queueFont:s(function(){return{fontFamily:this.system_queueFontFamily,fontSize:this.system_queueFontSize,fontWeight:this.system_queueFontWeight}},"system_queueFont"),external_system_queueFont:s(function(){return{fontFamily:this.external_system_queueFontFamily,fontSize:this.external_system_queueFontSize,fontWeight:this.external_system_queueFontWeight}},"external_system_queueFont"),containerFont:s(function(){return{fontFamily:this.containerFontFamily,fontSize:this.containerFontSize,fontWeight:this.containerFontWeight}},"containerFont"),external_containerFont:s(function(){return{fontFamily:this.external_containerFontFamily,fontSize:this.external_containerFontSize,fontWeight:this.external_containerFontWeight}},"external_containerFont"),container_dbFont:s(function(){return{fontFamily:this.container_dbFontFamily,fontSize:this.container_dbFontSize,fontWeight:this.container_dbFontWeight}},"container_dbFont"),external_container_dbFont:s(function(){return{fontFamily:this.external_container_dbFontFamily,fontSize:this.external_container_dbFontSize,fontWeight:this.external_container_dbFontWeight}},"external_container_dbFont"),container_queueFont:s(function(){return{fontFamily:this.container_queueFontFamily,fontSize:this.container_queueFontSize,fontWeight:this.container_queueFontWeight}},"container_queueFont"),external_container_queueFont:s(function(){return{fontFamily:this.external_container_queueFontFamily,fontSize:this.external_container_queueFontSize,fontWeight:this.external_container_queueFontWeight}},"external_container_queueFont"),componentFont:s(function(){return{fontFamily:this.componentFontFamily,fontSize:this.componentFontSize,fontWeight:this.componentFontWeight}},"componentFont"),external_componentFont:s(function(){return{fontFamily:this.external_componentFontFamily,fontSize:this.external_componentFontSize,fontWeight:this.external_componentFontWeight}},"external_componentFont"),component_dbFont:s(function(){return{fontFamily:this.component_dbFontFamily,fontSize:this.component_dbFontSize,fontWeight:this.component_dbFontWeight}},"component_dbFont"),external_component_dbFont:s(function(){return{fontFamily:this.external_component_dbFontFamily,fontSize:this.external_component_dbFontSize,fontWeight:this.external_component_dbFontWeight}},"external_component_dbFont"),component_queueFont:s(function(){return{fontFamily:this.component_queueFontFamily,fontSize:this.component_queueFontSize,fontWeight:this.component_queueFontWeight}},"component_queueFont"),external_component_queueFont:s(function(){return{fontFamily:this.external_component_queueFontFamily,fontSize:this.external_component_queueFontSize,fontWeight:this.external_component_queueFontWeight}},"external_component_queueFont"),boundaryFont:s(function(){return{fontFamily:this.boundaryFontFamily,fontSize:this.boundaryFontSize,fontWeight:this.boundaryFontWeight}},"boundaryFont"),messageFont:s(function(){return{fontFamily:this.messageFontFamily,fontSize:this.messageFontSize,fontWeight:this.messageFontWeight}},"messageFont")},pie:{...Ea.pie,useWidth:984},xyChart:{...Ea.xyChart,useWidth:void 0},requirement:{...Ea.requirement,useWidth:void 0},packet:{...Ea.packet},eventmodeling:{...Ea.eventmodeling},treeView:{...Ea.treeView,useWidth:void 0},radar:{...Ea.radar},railroad:{...Ea.railroad,fontSize:void 0,fontFamily:void 0,terminalFill:void 0,terminalStroke:void 0,terminalTextColor:void 0,nonTerminalFill:void 0,nonTerminalStroke:void 0,nonTerminalTextColor:void 0,lineColor:void 0,markerFill:void 0,commentFill:void 0,commentStroke:void 0,commentTextColor:void 0,specialFill:void 0,specialStroke:void 0,ruleNameColor:void 0},ishikawa:{...Ea.ishikawa},sankey:{...Ea.sankey,nodeColors:void 0},treemap:{useMaxWidth:!0,padding:10,diagramPadding:8,showValues:!0,nodeWidth:100,nodeHeight:40,borderWidth:1,valueFontSize:12,labelFontSize:14,valueFormat:","},venn:{...Ea.venn},cynefin:{...Ea.cynefin}},JK=s((e,t="")=>Object.keys(e).reduce((r,n)=>Array.isArray(e[n])?r:typeof e[n]=="object"&&e[n]!==null?[...r,t+n,...JK(e[n],"")]:[...r,t+n],[]),"keyify"),eZ=new Set(JK(QK,"")),mr=QK});var nze,ize,G0,FD,Sw=F(()=>{"use strict";Qi();St();nze={nodeColors:/^#[\da-f]{3,8}$|^rgb\([\d\s%,.]+\)$|^hsl\([\d\s%,.]+\)$|^[a-z]+$/i,filenameIcons:/^[\w-]+(?::[\w-]+)?$/,extensionIcons:/^[\w-]+(?::[\w-]+)?$/},ize=s((e,t)=>{for(let r of Object.keys(e)){let n=e[r];(r.startsWith("__")||r.includes("proto")||r.includes("constr")||typeof n!="string"||!t.test(n))&&(se.debug("sanitize deleting dictionary entry:",r,n),delete e[r])}},"sanitizeDictionaryConfig"),G0=s(e=>{if(se.debug("sanitizeDirective called with",e),!(typeof e!="object"||e==null)){if(Array.isArray(e)){e.forEach(t=>G0(t));return}for(let t of Object.keys(e)){if(se.debug("Checking key",t),t.startsWith("__")||t.includes("proto")||t.includes("constr")||!eZ.has(t)||e[t]==null){se.debug("sanitize deleting key: ",t),delete e[t];continue}if(typeof e[t]=="object"){let n=nze[t];n?ize(e[t],n):(se.debug("sanitizing object",t),G0(e[t]));continue}let r=["themeCSS","fontFamily","altFontFamily"];for(let n of r)t.includes(n)&&(se.debug("sanitizing css option",t),e[t]=FD(e[t]))}if(e.themeVariables)for(let t of Object.keys(e.themeVariables)){let r=e.themeVariables[t];r?.match&&!r.match(/^[\d "#%(),.;A-Za-z]+$/)&&(e.themeVariables[t]="")}se.debug("After sanitization",e)}},"sanitizeDirective"),FD=s(e=>{let t=0,r=0;for(let n of e){if(t{"use strict";$0();St();Cw();Qi();Sw();wd=Object.freeze(mr),Aa=s(e=>!(e===!1||["false","null","0"].includes(String(e).trim().toLowerCase())),"evaluate"),io=ii({},wd),zp=[],$x=ii({},wd),Fx=s((e,t)=>{let r=ii({},e),n={};for(let i of t)iZ(i),n=ii(n,i);if(r=ii(r,n),n.theme&&n.theme in dl){let i=ii({},ww),a=ii(i.themeVariables||{},n.themeVariables);r.theme&&r.theme in dl&&(r.themeVariables=dl[r.theme].getThemeVariables(a))}return $x=r,sze($x),$x},"updateCurrentConfig"),GD=s(e=>(io=ii({},wd),io=ii(io,e),e.theme&&dl[e.theme]&&(io.themeVariables=dl[e.theme].getThemeVariables(e.themeVariables)),Fx(io,zp),io),"setSiteConfig"),rZ=s(e=>{ww=ii({},e)},"saveConfigFromInitialize"),nZ=s(e=>(io=ii(io,e),Fx(io,zp),io),"updateSiteConfig"),zD=s(()=>ii({},io),"getSiteConfig"),kw=s(e=>(Fx($x,[e]),Lt()),"setConfig"),Lt=s(()=>ii({},$x),"getConfig"),iZ=s(e=>{e&&(["secure",...io.secure??[]].forEach(t=>{Object.hasOwn(e,t)&&(se.debug(`Denied attempt to modify a secure key ${t}`,e[t]),delete e[t])}),Object.keys(e).forEach(t=>{t.startsWith("__")&&delete e[t]}),Object.keys(e).forEach(t=>{typeof e[t]=="string"&&(e[t].includes("<")||e[t].includes(">")||e[t].includes("url(data:"))&&delete e[t],typeof e[t]=="object"&&iZ(e[t])}))},"sanitize"),aZ=s(e=>{G0(e),e.fontFamily&&!e.themeVariables?.fontFamily&&(e.themeVariables={...e.themeVariables,fontFamily:e.fontFamily}),zp.push(e),Fx(io,zp)},"addDirective"),Gx=s((e=io)=>{zp=[],Fx(e,zp)},"reset"),aze={LAZY_LOAD_DEPRECATED:"The configuration options lazyLoadedDiagrams and loadExternalDiagramsAtStartup are deprecated. Please use registerExternalDiagrams instead.",FLOWCHART_HTML_LABELS_DEPRECATED:"flowchart.htmlLabels is deprecated. Please use global htmlLabels instead."},tZ={},sZ=s(e=>{tZ[e]||(se.warn(aze[e]),tZ[e]=!0)},"issueWarning"),sze=s(e=>{e&&(e.lazyLoadedDiagrams||e.loadExternalDiagramsAtStartup)&&sZ("LAZY_LOAD_DEPRECATED")},"checkConfig"),Ew=s(()=>{let e={};ww&&(e=ii(e,ww));for(let t of zp)e=ii(e,t);return e},"getUserDefinedConfig"),ai=s(e=>(e.flowchart?.htmlLabels!=null&&sZ("FLOWCHART_HTML_LABELS_DEPRECATED"),Aa(e.htmlLabels??e.flowchart?.htmlLabels??!0)),"getEffectiveHtmlLabels")});function oZ(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r1?r-1:0),i=1;i2&&arguments[2]!==void 0?arguments[2]:Wx;if(lZ&&lZ(e,null),!Ed(t))return e;let n=t.length;for(;n--;){let i=t[n];if(typeof i=="string"){let a=r(i);a!==i&&(dze(t)||(t[n]=a),i=a)}e[i]=!0}return e}function Tze(e){for(let t=0;t0&&arguments[0]!==void 0?arguments[0]:Bze(),t=s(Bt=>wZ(Bt),"DOMPurify");if(t.version="3.4.12",t.removed=[],!e||!e.document||e.document.nodeType!==Oo.document||!e.Element)return t.isSupported=!1,t;let r=e.document,n=r,i=n.currentScript;e.DocumentFragment;let a=e.HTMLTemplateElement,o=e.Node,l=e.Element,u=e.NodeFilter,h=e.NamedNodeMap;h===void 0&&(e.NamedNodeMap||e.MozNamedAttrMap),e.HTMLFormElement;let d=e.DOMParser,f=e.trustedTypes,p=l.prototype,m=Nc(p,"cloneNode"),g=Nc(p,"remove"),y=Nc(p,"nextSibling"),v=Nc(p,"childNodes"),x=Nc(p,"parentNode"),b=Nc(p,"shadowRoot"),T=Nc(p,"attributes"),w=o&&o.prototype?Nc(o.prototype,"nodeType"):null,C=o&&o.prototype?Nc(o.prototype,"nodeName"):null;if(typeof a=="function"){let Bt=r.createElement("template");Bt.content&&Bt.content.ownerDocument&&(r=Bt.content.ownerDocument)}let S,k="",R,M=!1,P=0,N=s(function(){if(P>0)throw Vp('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.')},"_assertNotInTrustedTypesPolicy"),_=s(function($e){N(),P++;try{return S.createHTML($e)}finally{P--}},"_createTrustedHTML"),A=s(function($e){N(),P++;try{return S.createScriptURL($e)}finally{P--}},"_createTrustedScriptURL"),I=s(function(){return M||(R=$ze(f,i),M=!0),R},"_getDefaultTrustedTypesPolicy"),D=r,L=D.implementation,B=D.createNodeIterator,O=D.createDocumentFragment,$=D.getElementsByTagName,W=n.importNode,V=TZ();t.isSupported=typeof CZ=="function"&&typeof x=="function"&&L&&L.createHTMLDocument!==void 0;let z=Eze,H=Aze,K=Rze,J=_ze,ne=Lze,Z=Dze,pe=Ize,te=Nze,fe=vZ,ce=null,de=dn({},[...pZ,...WD,...qD,...HD,...mZ]),Ee=null,we=dn({},[...gZ,...UD,...yZ,...Aw]),Fe=Object.seal(W0(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ye=null,He=null,ye=Object.seal(W0(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}})),Be=!0,Ie=!0,Ne=!1,re=!0,ue=!1,le=!0,he=!1,Te=!1,oe=null,G=null,X=!1,ee=!1,q=!1,ie=!1,j=!0,U=!1,Y="user-content-",Ae=!0,me=!1,Se={},Ce=null,Me=dn({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","selectedcontent","style","svg","template","thead","title","video","xmp"]),_e=null,ze=dn({},["audio","video","img","source","image","track"]),Le=null,qe=dn({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),ve="http://www.w3.org/1998/Math/MathML",lt="http://www.w3.org/2000/svg",Oe="http://www.w3.org/1999/xhtml",mt=Oe,At=!1,Gt=null,Ct=dn({},[ve,lt,Oe],VD),Rt=Ya(["mi","mo","mn","ms","mtext"]),gt=dn({},Rt),be=Ya(["annotation-xml"]),ct=dn({},be),xt=dn({},["title","style","font","a","script"]),Ke=null,De=["application/xhtml+xml","text/html"],tt="text/html",Ze=null,dt=null,Ut=r.createElement("form"),$t=s(function($e){return $e instanceof RegExp||$e instanceof Function},"isRegexOrFunction"),Yt=s(function(){let $e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(dt&&dt===$e)return;(!$e||typeof $e!="object")&&($e={}),$e=_s($e),Ke=De.indexOf($e.PARSER_MEDIA_TYPE)===-1?tt:$e.PARSER_MEDIA_TYPE,Ze=Ke==="application/xhtml+xml"?VD:Wx,ce=kd($e,"ALLOWED_TAGS",de,{transform:Ze}),Ee=kd($e,"ALLOWED_ATTR",we,{transform:Ze}),Gt=kd($e,"ALLOWED_NAMESPACES",Ct,{transform:VD}),Le=kd($e,"ADD_URI_SAFE_ATTR",qe,{transform:Ze,base:qe}),_e=kd($e,"ADD_DATA_URI_TAGS",ze,{transform:Ze,base:ze}),Ce=kd($e,"FORBID_CONTENTS",Me,{transform:Ze}),Ye=kd($e,"FORBID_TAGS",_s({}),{transform:Ze}),He=kd($e,"FORBID_ATTR",_s({}),{transform:Ze}),Se=_a($e,"USE_PROFILES")?$e.USE_PROFILES&&typeof $e.USE_PROFILES=="object"?_s($e.USE_PROFILES):$e.USE_PROFILES:!1,Be=$e.ALLOW_ARIA_ATTR!==!1,Ie=$e.ALLOW_DATA_ATTR!==!1,Ne=$e.ALLOW_UNKNOWN_PROTOCOLS||!1,re=$e.ALLOW_SELF_CLOSE_IN_ATTR!==!1,ue=$e.SAFE_FOR_TEMPLATES||!1,le=$e.SAFE_FOR_XML!==!1,he=$e.WHOLE_DOCUMENT||!1,ee=$e.RETURN_DOM||!1,q=$e.RETURN_DOM_FRAGMENT||!1,ie=$e.RETURN_TRUSTED_TYPE||!1,X=$e.FORCE_BODY||!1,j=$e.SANITIZE_DOM!==!1,U=$e.SANITIZE_NAMED_PROPS||!1,Ae=$e.KEEP_CONTENT!==!1,me=$e.IN_PLACE||!1,fe=Sze($e.ALLOWED_URI_REGEXP)?$e.ALLOWED_URI_REGEXP:vZ,mt=typeof $e.NAMESPACE=="string"?$e.NAMESPACE:Oe,gt=_a($e,"MATHML_TEXT_INTEGRATION_POINTS")&&$e.MATHML_TEXT_INTEGRATION_POINTS&&typeof $e.MATHML_TEXT_INTEGRATION_POINTS=="object"?_s($e.MATHML_TEXT_INTEGRATION_POINTS):dn({},Rt),ct=_a($e,"HTML_INTEGRATION_POINTS")&&$e.HTML_INTEGRATION_POINTS&&typeof $e.HTML_INTEGRATION_POINTS=="object"?_s($e.HTML_INTEGRATION_POINTS):dn({},be);let nt=_a($e,"CUSTOM_ELEMENT_HANDLING")&&$e.CUSTOM_ELEMENT_HANDLING&&typeof $e.CUSTOM_ELEMENT_HANDLING=="object"?_s($e.CUSTOM_ELEMENT_HANDLING):W0(null);if(Fe=W0(null),_a(nt,"tagNameCheck")&&$t(nt.tagNameCheck)&&(Fe.tagNameCheck=nt.tagNameCheck),_a(nt,"attributeNameCheck")&&$t(nt.attributeNameCheck)&&(Fe.attributeNameCheck=nt.attributeNameCheck),_a(nt,"allowCustomizedBuiltInElements")&&typeof nt.allowCustomizedBuiltInElements=="boolean"&&(Fe.allowCustomizedBuiltInElements=nt.allowCustomizedBuiltInElements),ja(Fe),ue&&(Ie=!1),q&&(ee=!0),Se&&(ce=dn({},mZ),Ee=W0(null),Se.html===!0&&(dn(ce,pZ),dn(Ee,gZ)),Se.svg===!0&&(dn(ce,WD),dn(Ee,UD),dn(Ee,Aw)),Se.svgFilters===!0&&(dn(ce,qD),dn(Ee,UD),dn(Ee,Aw)),Se.mathMl===!0&&(dn(ce,HD),dn(Ee,yZ),dn(Ee,Aw))),ye.tagCheck=null,ye.attributeCheck=null,_a($e,"ADD_TAGS")&&(typeof $e.ADD_TAGS=="function"?ye.tagCheck=$e.ADD_TAGS:Ed($e.ADD_TAGS)&&(ce===de&&(ce=_s(ce)),dn(ce,$e.ADD_TAGS,Ze))),_a($e,"ADD_ATTR")&&(typeof $e.ADD_ATTR=="function"?ye.attributeCheck=$e.ADD_ATTR:Ed($e.ADD_ATTR)&&(Ee===we&&(Ee=_s(Ee)),dn(Ee,$e.ADD_ATTR,Ze))),_a($e,"ADD_URI_SAFE_ATTR")&&Ed($e.ADD_URI_SAFE_ATTR)&&dn(Le,$e.ADD_URI_SAFE_ATTR,Ze),_a($e,"FORBID_CONTENTS")&&Ed($e.FORBID_CONTENTS)&&(Ce===Me&&(Ce=_s(Ce)),dn(Ce,$e.FORBID_CONTENTS,Ze)),_a($e,"ADD_FORBID_CONTENTS")&&Ed($e.ADD_FORBID_CONTENTS)&&(Ce===Me&&(Ce=_s(Ce)),dn(Ce,$e.ADD_FORBID_CONTENTS,Ze)),Ae&&(ce["#text"]=!0),he&&dn(ce,["html","head","body"]),ce.table&&(dn(ce,["tbody"]),delete Ye.tbody),$e.TRUSTED_TYPES_POLICY){if(typeof $e.TRUSTED_TYPES_POLICY.createHTML!="function")throw Vp('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof $e.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Vp('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');let bt=S;S=$e.TRUSTED_TYPES_POLICY;try{k=_("")}catch(rr){throw S=bt,rr}}else $e.TRUSTED_TYPES_POLICY===null?(S=void 0,k=""):(S===void 0&&(S=I()),S&&typeof k=="string"&&(k=_("")));Ya&&Ya($e),dt=$e},"_parseConfig"),Sr=dn({},[...WD,...qD,...wze]),lr=dn({},[...HD,...kze]),Wt=s(function($e,nt,bt){return nt.namespaceURI===Oe?$e==="svg":nt.namespaceURI===ve?$e==="svg"&&(bt==="annotation-xml"||gt[bt]):!!Sr[$e]},"_checkSvgNamespace"),ar=s(function($e,nt,bt){return nt.namespaceURI===Oe?$e==="math":nt.namespaceURI===lt?$e==="math"&&ct[bt]:!!lr[$e]},"_checkMathMlNamespace"),et=s(function($e,nt,bt){return nt.namespaceURI===lt&&!ct[bt]||nt.namespaceURI===ve&&!gt[bt]?!1:!lr[$e]&&(xt[$e]||!Sr[$e])},"_checkHtmlNamespace"),pt=s(function($e){let nt=x($e);(!nt||!nt.tagName)&&(nt={namespaceURI:mt,tagName:"template"});let bt=Wx($e.tagName),rr=Wx(nt.tagName);return Gt[$e.namespaceURI]?$e.namespaceURI===lt?Wt(bt,nt,rr):$e.namespaceURI===ve?ar(bt,nt,rr):$e.namespaceURI===Oe?et(bt,nt,rr):!!(Ke==="application/xhtml+xml"&&Gt[$e.namespaceURI]):!1},"_checkValidNamespace"),kt=s(function($e){V0(t.removed,{element:$e});try{x($e).removeChild($e)}catch{if(g($e),!x($e))throw Vp("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place")}},"_forceRemove"),vr=s(function($e){on($e);let nt=v($e);if(nt){let rr=[];z0(nt,fr=>{V0(rr,fr)}),z0(rr,fr=>{try{g(fr)}catch{}})}let bt=T($e);if(bt)for(let rr=bt.length-1;rr>=0;--rr){let fr=bt[rr],dr=fr&&fr.name;if(typeof dr=="string")try{$e.removeAttribute(dr)}catch{}}},"_neutralizeRoot"),tr=s(function($e,nt){try{V0(t.removed,{attribute:nt.getAttributeNode($e),from:nt})}catch{V0(t.removed,{attribute:null,from:nt})}if(nt.removeAttribute($e),$e==="is")if(ee||q)try{kt(nt)}catch{}else try{nt.setAttribute($e,"")}catch{}},"_removeAttribute"),Ur=s(function($e){let nt=T($e);if(nt)for(let bt=nt.length-1;bt>=0;--bt){let rr=nt[bt],fr=rr&&rr.name;if(!(typeof fr!="string"||Ee[Ze(fr)]))try{$e.removeAttribute(fr)}catch{}}},"_stripDisallowedAttributes"),on=s(function($e){let nt=[$e];for(;nt.length>0;){let bt=nt.pop();(w?w(bt):bt.nodeType)===Oo.element&&Ur(bt);let fr=v(bt);if(fr)for(let dr=fr.length-1;dr>=0;--dr)nt.push(fr[dr])}},"_neutralizeSubtree"),zr=s(function($e){if(!le)return;let nt=[$e];for(;nt.length>0;){let bt=nt.pop(),rr=w?w(bt):bt.nodeType;if(rr===Oo.processingInstruction||rr===Oo.comment&&Ra(bZ,bt.data)){try{g(bt)}catch{}continue}if(rr===Oo.element){let dr=bt,Ti=Ze(C?C(bt):bt.nodeName);try{dr.hasAttribute&&dr.hasAttribute("patchsrc")&&dr.removeAttribute("patchsrc"),dr.hasAttribute&&dr.hasAttribute("for")&&Ti!=="label"&&Ti!=="output"&&dr.removeAttribute("for")}catch{}}let fr=v(bt);if(fr)for(let dr=fr.length-1;dr>=0;--dr)nt.push(fr[dr])}},"_neutralizePatchLinkage"),Pt=s(function($e){let nt=null,bt=null;if(X)$e=""+$e;else{let dr=uZ($e,/^[\r\n\t ]+/);bt=dr&&dr[0]}Ke==="application/xhtml+xml"&&mt===Oe&&($e=''+$e+"");let rr=S?_($e):$e;if(mt===Oe)try{nt=new d().parseFromString(rr,Ke)}catch{}if(!nt||!nt.documentElement){nt=L.createDocument(mt,"template",null);try{nt.documentElement.innerHTML=At?k:rr}catch{}}let fr=nt.body||nt.documentElement;return $e&&bt&&fr.insertBefore(r.createTextNode(bt),fr.childNodes[0]||null),mt===Oe?$.call(nt,he?"html":"body")[0]:he?nt.documentElement:fr},"_initDocument"),Et=s(function($e){return B.call($e.ownerDocument||$e,$e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT|u.SHOW_PROCESSING_INSTRUCTION|u.SHOW_CDATA_SECTION,null)},"_createNodeIterator"),Ot=s(function($e){return $e=zx($e,z," "),$e=zx($e,H," "),$e=zx($e,K," "),$e},"_stripTemplateExpressions"),zt=s(function($e){var nt;$e.normalize();let bt=B.call($e.ownerDocument||$e,$e,u.SHOW_TEXT|u.SHOW_COMMENT|u.SHOW_CDATA_SECTION|u.SHOW_PROCESSING_INSTRUCTION,null),rr=bt.nextNode();for(;rr;)rr.data=Ot(rr.data),rr=bt.nextNode();let fr=(nt=$e.querySelectorAll)===null||nt===void 0?void 0:nt.call($e,"template");fr&&z0(fr,dr=>{cr(dr.content)&&zt(dr.content)})},"_scrubTemplateExpressions"),wt=s(function($e){let nt=C?C($e):null;return typeof nt!="string"||Ze(nt)!=="form"?!1:typeof $e.nodeName!="string"||typeof $e.textContent!="string"||typeof $e.removeChild!="function"||$e.attributes!==T($e)||typeof $e.removeAttribute!="function"||typeof $e.setAttribute!="function"||typeof $e.namespaceURI!="string"||typeof $e.insertBefore!="function"||typeof $e.hasChildNodes!="function"||$e.nodeType!==w($e)||$e.childNodes!==v($e)},"_isClobbered"),cr=s(function($e){if(!w||typeof $e!="object"||$e===null)return!1;try{return w($e)===Oo.documentFragment}catch{return!1}},"_isDocumentFragment"),Re=s(function($e){if(!w||typeof $e!="object"||$e===null)return!1;try{return typeof w($e)=="number"}catch{return!1}},"_isNode");function ln(Bt,$e,nt){Bt.length!==0&&z0(Bt,bt=>{bt.call(t,$e,nt,dt)})}s(ln,"_executeHooks");let Ge=s(function($e,nt){return!!(le&&$e.hasChildNodes()&&!Re($e.firstElementChild)&&Ra(xZ,$e.textContent)&&Ra(xZ,$e.innerHTML)||le&&$e.namespaceURI===Oe&&nt==="style"&&Re($e.firstElementChild)||$e.nodeType===Oo.processingInstruction||le&&$e.nodeType===Oo.comment&&Ra(bZ,$e.data))},"_isUnsafeNode"),_r=s(function($e,nt){if(!Ye[nt]&&Qu(nt)&&(Fe.tagNameCheck instanceof RegExp&&Ra(Fe.tagNameCheck,nt)||Fe.tagNameCheck instanceof Function&&Fe.tagNameCheck(nt)))return!1;if(Ae&&!Ce[nt]){let bt=x($e),rr=v($e);if(rr&&bt){let fr=rr.length;for(let dr=fr-1;dr>=0;--dr){let Ti=me?rr[dr]:m(rr[dr],!0);bt.insertBefore(Ti,y($e))}}}return kt($e),!0},"_sanitizeDisallowedNode"),Yi=s(function($e,nt){if(ln(V.beforeSanitizeElements,$e,null),$e!==nt&&x($e)===null)return!0;if(wt($e))return kt($e),!0;let bt=Ze(C?C($e):$e.nodeName);if(ln(V.uponSanitizeElement,$e,{tagName:bt,allowedTags:ce}),$e!==nt&&x($e)===null)return!0;if(Ge($e,bt))return kt($e),!0;if(Ye[bt]||!(ye.tagCheck instanceof Function&&ye.tagCheck(bt))&&!ce[bt]){let fr=_r($e,bt);return fr===!1&&ln(V.afterSanitizeElements,$e,null),fr}if((w?w($e):$e.nodeType)===Oo.element&&!pt($e)||(bt==="noscript"||bt==="noembed"||bt==="noframes")&&Ra(Pze,$e.innerHTML))return kt($e),!0;if(ue&&$e.nodeType===Oo.text){let fr=Ot($e.textContent);$e.textContent!==fr&&(V0(t.removed,{element:$e.cloneNode()}),$e.textContent=fr)}return ln(V.afterSanitizeElements,$e,null),!1},"_sanitizeElements"),ll=s(function($e,nt,bt){if(He[nt]||le&&nt==="patchsrc"||le&&nt==="for"&&$e!=="label"&&$e!=="output"||j&&(nt==="id"||nt==="name")&&(bt in r||bt in Ut))return!1;let rr=Ee[nt]||ye.attributeCheck instanceof Function&&ye.attributeCheck(nt,$e);if(!(Ie&&Ra(J,nt))){if(!(Be&&Ra(ne,nt))){if(rr){if(!Le[nt]){if(!Ra(fe,zx(bt,pe,""))){if(!((nt==="src"||nt==="xlink:href"||nt==="href")&&$e!=="script"&&hZ(bt,"data:")===0&&_e[$e])){if(!(Ne&&!Ra(Z,zx(bt,pe,"")))){if(bt)return!1}}}}}else if(!(Qu($e)&&(Fe.tagNameCheck instanceof RegExp&&Ra(Fe.tagNameCheck,$e)||Fe.tagNameCheck instanceof Function&&Fe.tagNameCheck($e))&&(Fe.attributeNameCheck instanceof RegExp&&Ra(Fe.attributeNameCheck,nt)||Fe.attributeNameCheck instanceof Function&&Fe.attributeNameCheck(nt,$e))||nt==="is"&&Fe.allowCustomizedBuiltInElements&&(Fe.tagNameCheck instanceof RegExp&&Ra(Fe.tagNameCheck,bt)||Fe.tagNameCheck instanceof Function&&Fe.tagNameCheck(bt))))return!1}}return!0},"_isValidAttribute"),Cd=dn({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),Qu=s(function($e){return!Cd[Wx($e)]&&Ra(te,$e)},"_isBasicCustomElement"),O0=s(function($e,nt,bt,rr){if(S&&typeof f=="object"&&typeof f.getAttributeType=="function"&&!bt)switch(f.getAttributeType($e,nt)){case"TrustedHTML":return _(rr);case"TrustedScriptURL":return A(rr)}return rr},"_applyTrustedTypesToAttribute"),bi=s(function($e,nt,bt,rr){try{bt?$e.setAttributeNS(bt,nt,rr):$e.setAttribute(nt,rr),wt($e)?kt($e):cZ(t.removed)}catch{tr(nt,$e)}},"_setAttributeValue"),No=s(function($e){ln(V.beforeSanitizeAttributes,$e,null);let nt=$e.attributes;if(!nt||wt($e))return;let bt={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee,forceKeepAttr:void 0},rr=nt.length,fr=Ze($e.nodeName);for(;rr--;){let dr=nt[rr],Ti=dr.name,ji=dr.namespaceURI,Sa=dr.value,ca=Ze(Ti),Vi=Sa,wa=Ti==="value"?Vi:yze(Vi);if(bt.attrName=ca,bt.attrValue=wa,bt.keepAttr=!0,bt.forceKeepAttr=void 0,ln(V.uponSanitizeAttribute,$e,bt),wa=bt.attrValue,U&&(ca==="id"||ca==="name")&&hZ(wa,Y)!==0&&(tr(Ti,$e),wa=Y+wa),le&&Ra(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,wa)){tr(Ti,$e);continue}if(ca==="attributename"&&uZ(wa,"href")){tr(Ti,$e);continue}if(!bt.forceKeepAttr){if(!bt.keepAttr){tr(Ti,$e);continue}if(!re&&Ra(Oze,wa)){tr(Ti,$e);continue}if(ue&&(wa=Ot(wa)),!ll(fr,ca,wa)){tr(Ti,$e);continue}wa=O0(fr,ca,ji,wa),wa!==Vi&&bi($e,Ti,ji,wa)}}ln(V.afterSanitizeAttributes,$e,null)},"_sanitizeAttributes"),Dc=s(function($e){let nt=null,bt=Et($e);for(ln(V.beforeSanitizeShadowDOM,$e,null);nt=bt.nextNode();)if(ln(V.uponSanitizeShadowNode,nt,null),Yi(nt,$e),No(nt),cr(nt.content)&&Dc(nt.content),(w?w(nt):nt.nodeType)===Oo.element){let fr=b(nt);cr(fr)&&(cl(fr),Dc(fr))}ln(V.afterSanitizeShadowDOM,$e,null)},"_sanitizeShadowDOM"),cl=s(function($e){let nt=[{node:$e,shadow:null}];for(;nt.length>0;){let bt=nt.pop();if(bt.shadow){Dc(bt.shadow);continue}let rr=bt.node,dr=(w?w(rr):rr.nodeType)===Oo.element,Ti=v(rr);if(Ti)for(let ji=Ti.length-1;ji>=0;--ji)nt.push({node:Ti[ji],shadow:null});if(dr){let ji=C?C(rr):null;if(typeof ji=="string"&&Ze(ji)==="template"){let Sa=rr.content;cr(Sa)&&nt.push({node:Sa,shadow:null})}}if(dr){let ji=b(rr);cr(ji)&&nt.push({node:null,shadow:ji},{node:ji,shadow:null})}}},"_sanitizeAttachedShadowRoots");return t.sanitize=function(Bt){let $e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},nt=null,bt=null,rr=null,fr=null;if(At=!Bt,At&&(Bt=""),typeof Bt!="string"&&!Re(Bt)&&(Bt=Cze(Bt),typeof Bt!="string"))throw Vp("dirty is not a string, aborting");if(!t.isSupported)return Bt;Te?(ce=oe,Ee=G):Yt($e),(V.uponSanitizeElement.length>0||V.uponSanitizeAttribute.length>0)&&(ce=_s(ce)),V.uponSanitizeAttribute.length>0&&(Ee=_s(Ee)),t.removed=[];let dr=me&&typeof Bt!="string"&&Re(Bt);if(dr){zr(Bt);let ca=C?C(Bt):Bt.nodeName;if(typeof ca=="string"){let Vi=Ze(ca);if(!ce[Vi]||Ye[Vi])throw vr(Bt),Vp("root node is forbidden and cannot be sanitized in-place")}if(wt(Bt))throw vr(Bt),Vp("root node is clobbered and cannot be sanitized in-place");try{cl(Bt)}catch(Vi){throw vr(Bt),Vi}}else if(Re(Bt))nt=Pt(""),bt=nt.ownerDocument.importNode(Bt,!0),bt.nodeType===Oo.element&&bt.nodeName==="BODY"||bt.nodeName==="HTML"?nt=bt:nt.appendChild(bt),cl(bt);else{if(!ee&&!ue&&!he&&Bt.indexOf("<")===-1)return S&&ie?_(Bt):Bt;if(nt=Pt(Bt),!nt)return ee?null:ie?k:""}nt&&X&&kt(nt.firstChild);let Ti=dr?Bt:nt,ji=Et(Ti);try{for(;rr=ji.nextNode();)Yi(rr,Ti),No(rr),cr(rr.content)&&Dc(rr.content)}catch(ca){throw dr&&(vr(Bt),z0(t.removed,Vi=>{Vi.element&&on(Vi.element)})),ca}if(dr)return z0(t.removed,ca=>{ca.element&&on(ca.element)}),ue&&zt(Bt),Bt;if(ee){if(ue&&zt(nt),q)for(fr=O.call(nt.ownerDocument);nt.firstChild;)fr.appendChild(nt.firstChild);else fr=nt;return(Ee.shadowroot||Ee.shadowrootmode)&&(fr=W.call(n,fr,!0)),fr}let Sa=he?nt.outerHTML:nt.innerHTML;return he&&ce["!doctype"]&&nt.ownerDocument&&nt.ownerDocument.doctype&&nt.ownerDocument.doctype.name&&Ra(Mze,nt.ownerDocument.doctype.name)&&(Sa=" +`+Sa),ue&&(Sa=Ot(Sa)),S&&ie?_(Sa):Sa},t.setConfig=function(){let Bt=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};Yt(Bt),Te=!0,oe=ce,G=Ee},t.clearConfig=function(){dt=null,Te=!1,oe=null,G=null,S=R,k=""},t.isValidAttribute=function(Bt,$e,nt){dt||Yt({});let bt=Ze(Bt),rr=Ze($e);return ll(bt,rr,nt)},t.addHook=function(Bt,$e){typeof $e=="function"&&_a(V,Bt)&&V0(V[Bt],$e)},t.removeHook=function(Bt,$e){if(_a(V,Bt)){if($e!==void 0){let nt=mze(V[Bt],$e);return nt===-1?void 0:gze(V[Bt],nt,1)[0]}return cZ(V[Bt])}},t.removeHooks=function(Bt){_a(V,Bt)&&(V[Bt]=[])},t.removeAllHooks=function(){V=TZ()},t}var CZ,lZ,dze,fze,pze,Ya,ja,W0,SZ,YD,jD,z0,mze,cZ,V0,gze,Ed,Wx,VD,uZ,zx,hZ,yze,vze,xze,dZ,fZ,_a,Vx,Ra,Vp,pZ,WD,qD,wze,HD,kze,mZ,gZ,UD,yZ,Aw,Eze,Aze,Rze,_ze,Lze,vZ,Dze,Ize,Mze,Nze,xZ,bZ,Pze,Oze,Oo,Bze,$ze,TZ,kd,ao,q0=F(()=>{"use strict";s(oZ,"_arrayLikeToArray");s(oze,"_arrayWithHoles");s(lze,"_iterableToArrayLimit");s(cze,"_nonIterableRest");s(uze,"_slicedToArray");s(hze,"_unsupportedIterableToArray");CZ=Object.entries,lZ=Object.setPrototypeOf,dze=Object.isFrozen,fze=Object.getPrototypeOf,pze=Object.getOwnPropertyDescriptor,Ya=Object.freeze,ja=Object.seal,W0=Object.create,SZ=typeof Reflect<"u"&&Reflect,YD=SZ.apply,jD=SZ.construct;Ya||(Ya=s(function(t){return t},"freeze"));ja||(ja=s(function(t){return t},"seal"));YD||(YD=s(function(t,r){for(var n=arguments.length,i=new Array(n>2?n-2:0),a=2;a1?r-1:0),i=1;i"u"?null:Ji(BigInt.prototype.toString),fZ=typeof Symbol>"u"?null:Ji(Symbol.prototype.toString),_a=Ji(Object.prototype.hasOwnProperty),Vx=Ji(Object.prototype.toString),Ra=Ji(RegExp.prototype.test),Vp=bze(TypeError);s(Ji,"unapply");s(bze,"unconstruct");s(dn,"addToSet");s(Tze,"cleanArray");s(_s,"clone");s(Cze,"stringifyValue");s(Nc,"lookupGetter");s(Sze,"isRegex");pZ=Ya(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),WD=Ya(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),qD=Ya(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),wze=Ya(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),HD=Ya(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),kze=Ya(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),mZ=Ya(["#text"]),gZ=Ya(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","command","commandfor","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),UD=Ya(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dominant-baseline","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-orientation","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),yZ=Ya(["accent","accentunder","align","bevelled","close","columnalign","columnlines","columnspacing","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lquote","lspace","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),Aw=Ya(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),Eze=ja(/{{[\w\W]*|^[\w\W]*}}/g),Aze=ja(/<%[\w\W]*|^[\w\W]*%>/g),Rze=ja(/\${[\w\W]*/g),_ze=ja(/^data-[\-\w.\u00B7-\uFFFF]+$/),Lze=ja(/^aria-[\-\w]+$/),vZ=ja(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Dze=ja(/^(?:\w+script|data):/i),Ize=ja(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Mze=ja(/^html$/i),Nze=ja(/^[a-z][.\w]*(-[.\w]+)+$/i),xZ=ja(/<[/\w!]/g),bZ=ja(/<[/\w]/g),Pze=ja(/<\/no(script|embed|frames)/i),Oze=ja(/\/>/i),Oo={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,processingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},Bze=s(function(){return typeof window>"u"?null:window},"getGlobal"),$ze=s(function(t,r){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let n=null,i="data-tt-policy-suffix";r&&r.hasAttribute(i)&&(n=r.getAttribute(i));let a="dompurify"+(n?"#"+n:"");try{return t.createPolicy(a,{createHTML(o){return o},createScriptURL(o){return o}})}catch{return console.warn("TrustedTypes policy "+a+" could not be created."),null}},"_createTrustedTypesPolicy"),TZ=s(function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},"_createHooksMap"),kd=s(function(t,r,n,i){return _a(t,r)&&Ed(t[r])?dn(i.base?_s(i.base):{},t[r],i.transform):n},"_resolveSetOption");s(wZ,"createDOMPurify");ao=wZ()});var sJ={};sr(sJ,{ParseError:()=>Nt,SETTINGS_SCHEMA:()=>Gw,__defineFunction:()=>jt,__defineMacro:()=>ke,__defineSymbol:()=>Q,__domTree:()=>aJ,__parse:()=>tJ,__renderToDomTree:()=>ok,__renderToHTMLTree:()=>nJ,__setFontMetrics:()=>hQ,default:()=>AWe,render:()=>W7,renderToString:()=>eJ,version:()=>iJ});function Hze(e){if(typeof e!="string")return e.enum[0];switch(e){case"boolean":return!1;case"string":return"";case"number":return 0;case"object":return{};default:throw new Error("Unexpected schema type; settings must declare an explicit default.")}}function Uze(e){if(e.default!==void 0)return e.default;var t=Array.isArray(e.type)?e.type[0]:e.type;return Hze(t)}function Yze(e,t,r,n){var i=r[t];e[t]=i!==void 0?n.processor?n.processor(i):i:Uze(n)}function eVe(e){for(var t=0;t=i[0]&&e<=i[1])return r.name}return null}function sQ(e){for(var t=0;t=Fw[t]&&e<=Fw[t+1])return!0;return!1}function hVe(e){return"toText"in e}function mVe(e){if(e instanceof Is)return e;throw new Error("Expected symbolNode but got "+String(e)+".")}function gVe(e){if(e instanceof Dd)return e;throw new Error("Expected span but got "+String(e)+".")}function hQ(e,t){Bc[e]=t}function D7(e,t,r){if(!Bc[t])throw new Error("Font metrics not found for font: "+t+".");var n=e.charCodeAt(0),i=Bc[t][n];if(!i&&e[0]in EZ&&(n=EZ[e[0]].charCodeAt(0),i=Bc[t][n]),!i&&r==="text"&&sQ(n)&&(i=Bc[t][77]),i)return{depth:i[0],height:i[1],italic:i[2],skew:i[3],width:i[4]}}function vVe(e){var t;if(e>=5?t=0:e>=3?t=1:t=2,!XD[t]){var r=XD[t]={cssEmPerMu:Rw.quad[t]/18};for(var n in Rw)Rw.hasOwnProperty(n)&&(r[n]=Rw[n][t])}return XD[t]}function Q(e,t,r,n,i,a){pi[e][i]={font:t,group:r,replace:n},a&&n&&(pi[e][n]=pi[e][i])}function jt(e){for(var{type:t,names:r,props:n,handler:i,htmlBuilder:a,mathmlBuilder:o}=e,l={type:t,numArgs:n.numArgs,argTypes:n.argTypes,allowedInArgument:!!n.allowedInArgument,allowedInText:!!n.allowedInText,allowedInMath:n.allowedInMath===void 0?!0:n.allowedInMath,numOptionalArgs:n.numOptionalArgs||0,infix:!!n.infix,primitive:!!n.primitive,handler:i},u=0;u0&&(a.push(Nw(o,t)),o=[]),a.push(n[l]));o.length>0&&a.push(Nw(o,t));var h;r?(h=Nw(fa(r,t,!0),t),h.classes=["tag"],a.push(h)):i&&a.push(i);var d=It(["katex-html"],a);if(d.setAttribute("aria-hidden","true"),h){var f=h.children[0];f.style.height=Ft(d.height+d.depth),d.depth&&(f.style.verticalAlign=Ft(-d.depth))}return d}function xQ(e){return new _d(e)}function QD(e){if(!e)return!1;if(e.type==="mi"&&e.children.length===1){var t=e.children[0];return t instanceof Di&&t.text==="."}else if(e.type==="mo"&&e.children.length===1&&e.getAttribute("separator")==="true"&&e.getAttribute("lspace")==="0em"&&e.getAttribute("rspace")==="0em"){var r=e.children[0];return r instanceof Di&&r.text===","}else return!1}function OZ(e,t,r,n,i){var a=Fo(e,r),o;a.length===1&&a[0]instanceof Mt&&MVe.has(a[0].type)?o=a[0]:o=new Mt("mrow",a);var l=new Mt("annotation",[new Di(t)]);l.setAttribute("encoding","application/x-tex");var u=new Mt("semantics",[o,l]),h=new Mt("math",[u]);h.setAttribute("xmlns","http://www.w3.org/1998/Math/MathML"),n&&h.setAttribute("display","block");var d=i?"katex":"katex-mathml";return It([d],[h])}function qVe(e){return e in VVe}function Kr(e,t){if(!e||e.type!==t)throw new Error("Expected node of type "+t+", but got "+(e?"node of type "+e.type:String(e)));return e}function ek(e){var t=tk(e);if(!t)throw new Error("Expected node of symbol group type, but got "+(e?"node of type "+e.type:String(e)));return t}function tk(e){return e&&(e.type==="atom"||WVe.hasOwnProperty(e.type))?e:null}function wQ(e,t){var r=fa(e.body,t,!0);return It([e.mclass],r,t)}function kQ(e,t){var r,n=Fo(e.body,t);return e.mclass==="minner"?r=new Mt("mpadded",n):e.mclass==="mord"?e.isCharacterBox?(r=n[0],r.type="mi"):r=new Mt("mi",n):(e.isCharacterBox?(r=n[0],r.type="mo"):r=new Mt("mo",n),e.mclass==="mbin"?(r.attributes.lspace="0.22em",r.attributes.rspace="0.22em"):e.mclass==="mpunct"?(r.attributes.lspace="0em",r.attributes.rspace="0.17em"):e.mclass==="mopen"||e.mclass==="mclose"?(r.attributes.lspace="0em",r.attributes.rspace="0em"):e.mclass==="minner"&&(r.attributes.lspace="0.0556em",r.attributes.width="+0.1111em")),r}function jVe(e,t,r){var n=UVe[e];switch(n){case"\\\\cdrightarrow":case"\\\\cdleftarrow":return r.callFunction(n,[t[0]],[t[1]]);case"\\uparrow":case"\\downarrow":{var i=r.callFunction("\\\\cdleft",[t[0]],[]),a={type:"atom",text:n,mode:"math",family:"rel"},o=r.callFunction("\\Big",[a],[]),l=r.callFunction("\\\\cdright",[t[1]],[]),u={type:"ordgroup",mode:"math",body:[i,o,l]};return r.callFunction("\\\\cdparent",[u],[])}case"\\\\cdlongequal":return r.callFunction("\\\\cdlongequal",[],[]);case"\\Vert":{var h={type:"textord",text:"\\Vert",mode:"math"};return r.callFunction("\\Big",[h],[])}default:return{type:"textord",text:" ",mode:"math"}}}function XVe(e){var t=[];for(e.gullet.beginGroup(),e.gullet.macros.set("\\cr","\\\\\\relax"),e.gullet.beginGroup();;){t.push(e.parseExpression(!1,"\\\\")),e.gullet.endGroup(),e.gullet.beginGroup();var r=e.fetch().text;if(r==="&"||r==="\\\\")e.consume();else if(r==="\\end"){t[t.length-1].length===0&&t.pop();break}else throw new Nt("Expected \\\\ or \\cr or \\end",e.nextToken)}for(var n=[],i=[n],a=0;aAV".includes(h))for(var f=0;f<2;f++){for(var p=!0,m=u+1;mAV=|." after @',o[u]);var g=jVe(h,d,e),y={type:"styling",body:[g],mode:"math",style:"display",resetFont:!0};n.push(y),l=FZ()}a%2===0?n.push(l):n.shift(),n=[],i.push(n)}e.gullet.endGroup(),e.gullet.endGroup();var v=new Array(i[0].length).fill({type:"align",align:"c",pregap:.25,postgap:.25});return{type:"array",mode:"math",body:i,arraystretch:1,addJot:!0,rowGaps:[null],cols:v,colSeparationType:"CD",hLinesBeforeRow:new Array(i.length+1).fill([])}}function VZ(e){return"isMiddle"in e}function nk(e,t){var r=tk(e);if(r&&sWe.has(r.text))return r;throw r?new Nt("Invalid delimiter '"+r.text+"' after '"+t.funcName+"'",e):new Nt("Invalid delimiter type '"+e.type+"'",e)}function WZ(e){if(!e.body)throw new Error("Bug: The leftright ParseNode wasn't fully parsed.")}function Fc(e){for(var{type:t,names:r,props:n,handler:i,htmlBuilder:a,mathmlBuilder:o}=e,l={type:t,numArgs:n.numArgs||0,allowedInText:!1,numOptionalArgs:0,handler:i},u=0;u1||!d)&&y.pop(),x.length{"use strict";Nt=class e extends Error{static{s(this,"ParseError")}constructor(t,r){var n="KaTeX parse error: "+t,i,a,o=r&&r.loc;if(o&&o.start<=o.end){var l=o.lexer.input;i=o.start,a=o.end,i===l.length?n+=" at end of input: ":n+=" at position "+(i+1)+": ";var u=l.slice(i,a).replace(/[^]/g,"$&\u0332"),h;i>15?h="\u2026"+l.slice(i-15,i):h=l.slice(0,i);var d;a+15e.replace(Fze,"-$1").toLowerCase(),"hyphenate"),zze={"&":"&",">":">","<":"<",'"':""","'":"'"},Vze=/[&><"']/g,fs=s(e=>String(e).replace(Vze,t=>zze[t]),"escape"),$w=s(e=>e.type==="ordgroup"||e.type==="color"?e.body.length===1?$w(e.body[0]):e:e.type==="font"?$w(e.body):e,"getBaseElem"),Wze=new Set(["mathord","textord","atom"]),sh=s(e=>Wze.has($w(e).type),"isCharacterBox"),qze=s(e=>{var t=/^[\x00-\x20]*([^\\/#?]*?)(:|�*58|�*3a|&colon)/i.exec(e);return t?t[2]!==":"||!/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(t[1])?null:t[1].toLowerCase():"_relative"},"protocolFromUrl"),Gw={displayMode:{type:"boolean",description:"Render math in display mode, which puts the math in display style (so \\int and \\sum are large, for example), and centers the math on the page on its own line.",cli:"-d, --display-mode"},output:{type:{enum:["htmlAndMathml","html","mathml"]},description:"Determines the markup language of the output.",cli:"-F, --format "},leqno:{type:"boolean",description:"Render display math in leqno style (left-justified tags)."},fleqn:{type:"boolean",description:"Render display math flush left."},throwOnError:{type:"boolean",default:!0,cli:"-t, --no-throw-on-error",cliDescription:"Render errors (in the color given by --error-color) instead of throwing a ParseError exception when encountering an error."},errorColor:{type:"string",default:"#cc0000",cli:"-c, --error-color ",cliDescription:"A color string given in the format 'rgb' or 'rrggbb' (no #). This option determines the color of errors rendered by the -t option.",cliProcessor:s(e=>"#"+e,"cliProcessor")},macros:{type:"object",cli:"-m, --macro ",cliDescription:"Define custom macro of the form '\\foo:expansion' (use multiple -m arguments for multiple macros).",cliDefault:[],cliProcessor:s((e,t)=>(t.push(e),t),"cliProcessor")},minRuleThickness:{type:"number",description:"Specifies a minimum thickness, in ems, for fraction lines, `\\sqrt` top lines, `{array}` vertical lines, `\\hline`, `\\hdashline`, `\\underline`, `\\overline`, and the borders of `\\fbox`, `\\boxed`, and `\\fcolorbox`.",processor:s(e=>Math.max(0,e),"processor"),cli:"--min-rule-thickness ",cliProcessor:parseFloat},colorIsTextColor:{type:"boolean",description:"Makes \\color behave like LaTeX's 2-argument \\textcolor, instead of LaTeX's one-argument \\color mode change.",cli:"-b, --color-is-text-color"},strict:{type:[{enum:["warn","ignore","error"]},"boolean","function"],description:"Turn on strict / LaTeX faithfulness mode, which throws an error if the input uses features that are not supported by LaTeX.",cli:"-S, --strict",cliDefault:!1},trust:{type:["boolean","function"],description:"Trust the input, enabling all HTML features such as \\url.",cli:"-T, --trust"},maxSize:{type:"number",default:1/0,description:"If non-zero, all user-specified sizes, e.g. in \\rule{500em}{500em}, will be capped to maxSize ems. Otherwise, elements and spaces can be arbitrarily large",processor:s(e=>Math.max(0,e),"processor"),cli:"-s, --max-size ",cliProcessor:parseInt},maxExpand:{type:"number",default:1e3,description:"Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to Infinity, the macro expander will try to fully expand as in LaTeX.",processor:s(e=>Math.max(0,e),"processor"),cli:"-e, --max-expand ",cliProcessor:s(e=>e==="Infinity"?1/0:parseInt(e),"cliProcessor")},globalGroup:{type:"boolean",cli:!1}};s(Hze,"getImplicitDefault");s(Uze,"getDefaultValue");s(Yze,"applySetting");jx=class{static{s(this,"Settings")}constructor(t){t===void 0&&(t={}),this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0,this.errorColor=void 0,this.macros=void 0,this.minRuleThickness=void 0,this.colorIsTextColor=void 0,this.strict=void 0,this.trust=void 0,this.maxSize=void 0,this.maxExpand=void 0,this.globalGroup=void 0,t=t||{};for(var r of Object.keys(Gw)){var n=Gw[r];n&&Yze(this,r,t,n)}}reportNonstrict(t,r,n){var i=this.strict;if(typeof i=="function"&&(i=i(t,r,n)),!(!i||i==="ignore")){if(i===!0||i==="error")throw new Nt("LaTeX-incompatible input and strict mode is set to 'error': "+(r+" ["+t+"]"),n);i==="warn"?typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+(r+" ["+t+"]")):typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to "+("unrecognized '"+i+"': "+r+" ["+t+"]"))}}useStrictBehavior(t,r,n){var i=this.strict;if(typeof i=="function")try{i=i(t,r,n)}catch{i="error"}return!i||i==="ignore"?!1:i===!0||i==="error"?!0:i==="warn"?(typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+(r+" ["+t+"]")),!1):(typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to "+("unrecognized '"+i+"': "+r+" ["+t+"]")),!1)}isTrusted(t){if("url"in t&&t.url&&!t.protocol){var r=qze(t.url);if(r==null)return!1;t.protocol=r}var n=typeof this.trust=="function"?this.trust(t):this.trust;return!!n}},Pc=class{static{s(this,"Style")}constructor(t,r,n){this.id=void 0,this.size=void 0,this.cramped=void 0,this.id=t,this.size=r,this.cramped=n}sup(){return Oc[jze[this.id]]}sub(){return Oc[Xze[this.id]]}fracNum(){return Oc[Kze[this.id]]}fracDen(){return Oc[Zze[this.id]]}cramp(){return Oc[Qze[this.id]]}text(){return Oc[Jze[this.id]]}isTight(){return this.size>=2}},_7=0,zw=1,U0=2,ah=3,Xx=4,fl=5,Y0=6,Ds=7,Oc=[new Pc(_7,0,!1),new Pc(zw,0,!0),new Pc(U0,1,!1),new Pc(ah,1,!0),new Pc(Xx,2,!1),new Pc(fl,2,!0),new Pc(Y0,3,!1),new Pc(Ds,3,!0)],jze=[Xx,fl,Xx,fl,Y0,Ds,Y0,Ds],Xze=[fl,fl,fl,fl,Ds,Ds,Ds,Ds],Kze=[U0,ah,Xx,fl,Y0,Ds,Y0,Ds],Zze=[ah,ah,fl,fl,Ds,Ds,Ds,Ds],Qze=[zw,zw,ah,ah,fl,fl,Ds,Ds],Jze=[_7,zw,U0,ah,U0,ah,U0,ah],Vr={DISPLAY:Oc[_7],TEXT:Oc[U0],SCRIPT:Oc[Xx],SCRIPTSCRIPT:Oc[Y0]},c7=[{name:"latin",blocks:[[256,591],[768,879]]},{name:"cyrillic",blocks:[[1024,1279]]},{name:"armenian",blocks:[[1328,1423]]},{name:"brahmic",blocks:[[2304,4255]]},{name:"georgian",blocks:[[4256,4351]]},{name:"cjk",blocks:[[12288,12543],[19968,40879],[65280,65376]]},{name:"hangul",blocks:[[44032,55215]]}];s(eVe,"scriptFromCodepoint");Fw=[];c7.forEach(e=>e.blocks.forEach(t=>Fw.push(...t)));s(sQ,"supportedCodepoint");da=s(e=>e+" "+e,"doubleBrushStroke"),H0=80,tVe=s(function(t,r){return"M95,"+(622+t+r)+` +c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14 +c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54 +c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10 +s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429 +c69,-144,104.5,-217.7,106.5,-221 +l`+t/2.075+" -"+t+` +c5.3,-9.3,12,-14,20,-14 +H400000v`+(40+t)+`H845.2724 +s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7 +c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z +M`+(834+t)+" "+r+"h400000v"+(40+t)+"h-400000z"},"sqrtMain"),rVe=s(function(t,r){return"M263,"+(601+t+r)+`c0.7,0,18,39.7,52,119 +c34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120 +c340,-704.7,510.7,-1060.3,512,-1067 +l`+t/2.084+" -"+t+` +c4.7,-7.3,11,-11,19,-11 +H40000v`+(40+t)+`H1012.3 +s-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232 +c-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1 +s-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26 +c-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60z +M`+(1001+t)+" "+r+"h400000v"+(40+t)+"h-400000z"},"sqrtSize1"),nVe=s(function(t,r){return"M983 "+(10+t+r)+` +l`+t/3.13+" -"+t+` +c4,-6.7,10,-10,18,-10 H400000v`+(40+t)+` +H1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7 +s-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744 +c-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30 +c26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722 +c56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5 +c53.7,-170.3,84.5,-266.8,92.5,-289.5z +M`+(1001+t)+" "+r+"h400000v"+(40+t)+"h-400000z"},"sqrtSize2"),iVe=s(function(t,r){return"M424,"+(2398+t+r)+` +c-1.3,-0.7,-38.5,-172,-111.5,-514c-73,-342,-109.8,-513.3,-110.5,-514 +c0,-2,-10.7,14.3,-32,49c-4.7,7.3,-9.8,15.7,-15.5,25c-5.7,9.3,-9.8,16,-12.5,20 +s-5,7,-5,7c-4,-3.3,-8.3,-7.7,-13,-13s-13,-13,-13,-13s76,-122,76,-122s77,-121,77,-121 +s209,968,209,968c0,-2,84.7,-361.7,254,-1079c169.3,-717.3,254.7,-1077.7,256,-1081 +l`+t/4.223+" -"+t+`c4,-6.7,10,-10,18,-10 H400000 +v`+(40+t)+`H1014.6 +s-87.3,378.7,-272.6,1166c-185.3,787.3,-279.3,1182.3,-282,1185 +c-2,6,-10,9,-24,9 +c-8,0,-12,-0.7,-12,-2z M`+(1001+t)+" "+r+` +h400000v`+(40+t)+"h-400000z"},"sqrtSize3"),aVe=s(function(t,r){return"M473,"+(2713+t+r)+` +c339.3,-1799.3,509.3,-2700,510,-2702 l`+t/5.298+" -"+t+` +c3.3,-7.3,9.3,-11,18,-11 H400000v`+(40+t)+`H1017.7 +s-90.5,478,-276.2,1466c-185.7,988,-279.5,1483,-281.5,1485c-2,6,-10,9,-24,9 +c-8,0,-12,-0.7,-12,-2c0,-1.3,-5.3,-32,-16,-92c-50.7,-293.3,-119.7,-693.3,-207,-1200 +c0,-1.3,-5.3,8.7,-16,30c-10.7,21.3,-21.3,42.7,-32,64s-16,33,-16,33s-26,-26,-26,-26 +s76,-153,76,-153s77,-151,77,-151c0.7,0.7,35.7,202,105,604c67.3,400.7,102,602.7,104, +606zM`+(1001+t)+" "+r+"h400000v"+(40+t)+"H1017.7z"},"sqrtSize4"),sVe=s(function(t){var r=t/2;return"M400000 "+t+" H0 L"+r+" 0 l65 45 L145 "+(t-80)+" H400000z"},"phasePath"),oVe=s(function(t,r,n){var i=n-54-r-t;return"M702 "+(t+r)+"H400000"+(40+t)+` +H742v`+i+`l-4 4-4 4c-.667.7 -2 1.5-4 2.5s-4.167 1.833-6.5 2.5-5.5 1-9.5 1 +h-12l-28-84c-16.667-52-96.667 -294.333-240-727l-212 -643 -85 170 +c-4-3.333-8.333-7.667-13 -13l-13-13l77-155 77-156c66 199.333 139 419.667 +219 661 l218 661zM702 `+r+"H400000v"+(40+t)+"H742z"},"sqrtTall"),lVe=s(function(t,r,n){r=1e3*r;var i="";switch(t){case"sqrtMain":i=tVe(r,H0);break;case"sqrtSize1":i=rVe(r,H0);break;case"sqrtSize2":i=nVe(r,H0);break;case"sqrtSize3":i=iVe(r,H0);break;case"sqrtSize4":i=aVe(r,H0);break;case"sqrtTall":i=oVe(r,H0,n)}return i},"sqrtPath"),cVe=s(function(t,r){switch(t){case"\u239C":return da("M291 0 H417 V"+r+" H291z");case"\u2223":return da("M145 0 H188 V"+r+" H145z");case"\u2225":return da("M145 0 H188 V"+r+" H145z")+da("M367 0 H410 V"+r+" H367z");case"\u239F":return da("M457 0 H583 V"+r+" H457z");case"\u23A2":return da("M319 0 H403 V"+r+" H319z");case"\u23A5":return da("M263 0 H347 V"+r+" H263z");case"\u23AA":return da("M384 0 H504 V"+r+" H384z");case"\u23D0":return da("M312 0 H355 V"+r+" H312z");case"\u2016":return da("M257 0 H300 V"+r+" H257z")+da("M478 0 H521 V"+r+" H478z");default:return""}},"innerPath"),kZ={doubleleftarrow:`M262 157 +l10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3 + 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28 + 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5 +c2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5 + 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87 +-86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7 +-2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z +m8 0v40h399730v-40zm0 194v40h399730v-40z`,doublerightarrow:`M399738 392l +-10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5 + 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88 +-33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68 +-17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18 +-13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782 +c-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3 +-107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z`,leftarrow:`M400000 241H110l3-3c68.7-52.7 113.7-120 + 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8 +-5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247 +c-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208 + 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3 + 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202 + l-3-3h399890zM100 241v40h399900v-40z`,leftbrace:`M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117 +-45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7 + 5-6 9-10 13-.7 1-7.3 1-20 1H6z`,leftbraceunder:`M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13 + 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688 + 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7 +-331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z`,leftgroup:`M400000 80 +H435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0 + 435 0h399565z`,leftgroupunder:`M400000 262 +H435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219 + 435 219h399565z`,leftharpoon:`M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3 +-3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5 +-18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7 +-196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z`,leftharpoonplus:`M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5 + 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3 +-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7 +-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z +m0 0v40h400000v-40z`,leftharpoondown:`M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333 + 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5 + 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667 +-152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z`,leftharpoondownplus:`M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12 + 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7 +-2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0 +v40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z`,lefthook:`M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5 +-83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3 +-68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21 + 71.5 23h399859zM103 281v-40h399897v40z`,leftlinesegment:da("M40 281 V428 H0 V94 H40 V241 H400000 v40z"),leftbracketunder:da("M0 0 h120 V290 H399995 v120 H0z"),leftbracketover:da("M0 440 h120 V150 H399995 v-120 H0z"),leftmapsto:da("M40 281 V448H0V74H40V241H400000v40z"),leftToFrom:`M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23 +-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8 +c28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3 + 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z`,longequal:da("M0 50 h400000 v40H0z m0 194h40000v40H0z"),midbrace:`M200428 334 +c-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14 +-53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7 + 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11 + 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z`,midbraceunder:`M199572 214 +c100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14 + 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3 + 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0 +-5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z`,oiintSize1:`M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6 +-320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z +m368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8 +60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z`,oiintSize2:`M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8 +-451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z +m502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2 +c0 110 84 276 504 276s502.4-166 502.4-276z`,oiiintSize1:`M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6 +-480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z +m525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0 +85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z`,oiiintSize2:`M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8 +-707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z +m770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1 +c0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z`,rightarrow:`M0 241v40h399891c-47.3 35.3-84 78-110 128 +-16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 + 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 + 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85 +-40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5 +-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67 + 151.7 139 205zm0 0v40h399900v-40z`,rightbrace:`M400000 542l +-6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5 +s-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1 +c124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z`,rightbraceunder:`M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3 + 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237 +-174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z`,rightgroup:`M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0 + 3-1 3-3v-38c-76-158-257-219-435-219H0z`,rightgroupunder:`M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18 + 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z`,rightharpoon:`M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3 +-3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2 +-10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 + 69.2 92 94.5zm0 0v40h399900v-40z`,rightharpoonplus:`M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11 +-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7 + 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z +m0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z`,rightharpoondown:`M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8 + 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5 +-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95 +-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z`,rightharpoondownplus:`M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8 + 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 + 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3 +-64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z +m0-194v40h400000v-40zm0 0v40h400000v-40z`,righthook:`M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3 + 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0 +-13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21 + 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z`,rightlinesegment:da("M399960 241 V94 h40 V428 h-40 V281 H0 v-40z"),rightbracketunder:da("M399995 0 h-120 V290 H0 v120 H400000z"),rightbracketover:da("M399995 440 h-120 V150 H0 v-120 H399995z"),rightToFrom:`M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23 + 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32 +-52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142 +-167z M100 147v40h399900v-40zM0 341v40h399900v-40z`,twoheadleftarrow:`M0 167c68 40 + 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69 +-70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3 +-40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19 +-37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101 + 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z`,twoheadrightarrow:`M400000 167 +c-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3 + 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42 + 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333 +-19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70 + 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z`,tilde1:`M200 55.538c-77 0-168 73.953-177 73.953-3 0-7 +-2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0 + 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0 + 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128 +-68.267.847-113-73.952-191-73.952z`,tilde2:`M344 55.266c-142 0-300.638 81.316-311.5 86.418 +-8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9 + 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114 +c1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751 + 181.476 676 181.476c-149 0-189-126.21-332-126.21z`,tilde3:`M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457 +-11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0 + 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697 + 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696 + -338 0-409-156.573-744-156.573z`,tilde4:`M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345 +-11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409 + 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9 + 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409 + -175.236-744-175.236z`,vec:`M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 +3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 +10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 +-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 +-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 +H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 +c-16-25.333-24-45-24-59z`,widehat1:`M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22 +c-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z`,widehat2:`M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10 +-11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`,widehat3:`M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10 +-11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`,widehat4:`M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10 +-11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`,widecheck1:`M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1, +-5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z`,widecheck2:`M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10, +-11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`,widecheck3:`M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10, +-11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`,widecheck4:`M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10, +-11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`,baraboveleftarrow:`M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202 +c4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5 +c-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130 +s-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47 +121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6 +s2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11 +c0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z +M100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z`,rightarrowabovebar:`M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32 +-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0 +13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39 +-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5 +-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5 +-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67 +151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z`,baraboveshortleftharpoon:`M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11 +c1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17 +c2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21 +c-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40 +c-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z +M0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z`,rightharpoonaboveshortbar:`M0,241 l0,40c399126,0,399993,0,399993,0 +c4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199, +-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6 +c-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z +M0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z`,shortbaraboveleftharpoon:`M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11 +c1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9, +1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7, +-152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z +M93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z`,shortrightharpoonabovebar:`M53,241l0,40c398570,0,399437,0,399437,0 +c4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199, +-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6 +c-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z +M500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z`},uVe=s(function(t,r){switch(t){case"lbrack":return"M403 1759 V84 H666 V0 H319 V1759 v"+r+` v1759 v84 h347 v-84 +H403z M403 1759 V0 H319 V1759 v`+r+" v1759 v84 h84z";case"rbrack":return"M347 1759 V0 H0 V84 H263 V1759 v"+r+` v1759 H0 v84 H347z +M347 1759 V0 H263 V1759 v`+r+" v1759 h84z";case"vert":return"M145 15 v585 v"+r+` v585 c2.667,10,9.667,15,21,15 +c10,0,16.667,-5,20,-15 v-585 v`+-r+` v-585 c-2.667,-10,-9.667,-15,-21,-15 +c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v`+r+" v585 h43z";case"doublevert":return"M145 15 v585 v"+r+` v585 c2.667,10,9.667,15,21,15 +c10,0,16.667,-5,20,-15 v-585 v`+-r+` v-585 c-2.667,-10,-9.667,-15,-21,-15 +c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v`+r+` v585 h43z +M367 15 v585 v`+r+` v585 c2.667,10,9.667,15,21,15 +c10,0,16.667,-5,20,-15 v-585 v`+-r+` v-585 c-2.667,-10,-9.667,-15,-21,-15 +c-10,0,-16.667,5,-20,15z M410 15 H367 v585 v`+r+" v585 h43z";case"lfloor":return"M319 602 V0 H403 V602 v"+r+` v1715 h263 v84 H319z +MM319 602 V0 H403 V602 v`+r+" v1715 H319z";case"rfloor":return"M319 602 V0 H403 V602 v"+r+` v1799 H0 v-84 H319z +MM319 602 V0 H403 V602 v`+r+" v1715 H319z";case"lceil":return"M403 1759 V84 H666 V0 H319 V1759 v"+r+` v602 h84z +M403 1759 V0 H319 V1759 v`+r+" v602 h84z";case"rceil":return"M347 1759 V0 H0 V84 H263 V1759 v"+r+` v602 h84z +M347 1759 V0 h-84 V1759 v`+r+" v602 h84z";case"lparen":return`M863,9c0,-2,-2,-5,-6,-9c0,0,-17,0,-17,0c-12.7,0,-19.3,0.3,-20,1 +c-5.3,5.3,-10.3,11,-15,17c-242.7,294.7,-395.3,682,-458,1162c-21.3,163.3,-33.3,349, +-36,557 l0,`+(r+84)+`c0.2,6,0,26,0,60c2,159.3,10,310.7,24,454c53.3,528,210, +949.7,470,1265c4.7,6,9.7,11.7,15,17c0.7,0.7,7,1,19,1c0,0,18,0,18,0c4,-4,6,-7,6,-9 +c0,-2.7,-3.3,-8.7,-10,-18c-135.3,-192.7,-235.5,-414.3,-300.5,-665c-65,-250.7,-102.5, +-544.7,-112.5,-882c-2,-104,-3,-167,-3,-189 +l0,-`+(r+92)+`c0,-162.7,5.7,-314,17,-454c20.7,-272,63.7,-513,129,-723c65.3, +-210,155.3,-396.3,270,-559c6.7,-9.3,10,-15.3,10,-18z`;case"rparen":return`M76,0c-16.7,0,-25,3,-25,9c0,2,2,6.3,6,13c21.3,28.7,42.3,60.3, +63,95c96.7,156.7,172.8,332.5,228.5,527.5c55.7,195,92.8,416.5,111.5,664.5 +c11.3,139.3,17,290.7,17,454c0,28,1.7,43,3.3,45l0,`+(r+9)+` +c-3,4,-3.3,16.7,-3.3,38c0,162,-5.7,313.7,-17,455c-18.7,248,-55.8,469.3,-111.5,664 +c-55.7,194.7,-131.8,370.3,-228.5,527c-20.7,34.7,-41.7,66.3,-63,95c-2,3.3,-4,7,-6,11 +c0,7.3,5.7,11,17,11c0,0,11,0,11,0c9.3,0,14.3,-0.3,15,-1c5.3,-5.3,10.3,-11,15,-17 +c242.7,-294.7,395.3,-681.7,458,-1161c21.3,-164.7,33.3,-350.7,36,-558 +l0,-`+(r+144)+`c-2,-159.3,-10,-310.7,-24,-454c-53.3,-528,-210,-949.7, +-470,-1265c-4.7,-6,-9.7,-11.7,-15,-17c-0.7,-0.7,-6.7,-1,-18,-1z`;default:throw new Error("Unknown stretchy delimiter.")}},"tallDelim");s(hVe,"isMathDomNode");_d=class{static{s(this,"DocumentFragment")}constructor(t){this.children=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.children=t,this.classes=[],this.height=0,this.depth=0,this.maxFontSize=0,this.style={}}hasClass(t){return this.classes.includes(t)}toNode(){for(var t=document.createDocumentFragment(),r=0;r{if(hVe(t))return t.toText();throw new Error("Expected MathDomNode with toText, got "+t.constructor.name)}).join("")}},u7={pt:1,mm:7227/2540,cm:7227/254,in:72.27,bp:803/800,pc:12,dd:1238/1157,cc:14856/1157,nd:685/642,nc:1370/107,sp:1/65536,px:803/800},dVe={ex:!0,em:!0,mu:!0},oQ=s(function(t){return typeof t!="string"&&(t=t.unit),t in u7||t in dVe||t==="ex"},"validUnit"),wi=s(function(t,r){var n;if(t.unit in u7)n=u7[t.unit]/r.fontMetrics().ptPerEm/r.sizeMultiplier;else if(t.unit==="mu")n=r.fontMetrics().cssEmPerMu;else{var i;if(r.style.isTight()?i=r.havingStyle(r.style.text()):i=r,t.unit==="ex")n=i.fontMetrics().xHeight;else if(t.unit==="em")n=i.fontMetrics().quad;else throw new Nt("Invalid unit: '"+t.unit+"'");i!==r&&(n*=i.sizeMultiplier/r.sizeMultiplier)}return Math.min(t.number*n,r.maxSize)},"calculateSize"),Ft=s(function(t){return+t.toFixed(4)+"em"},"makeEm"),Ld=s(function(t){return t.filter(r=>r).join(" ")},"createClass"),L7=s(function(t){var r="";for(var n of Object.keys(t)){var i=t[n];i!==void 0&&(r+=Gze(n)+":"+i+";")}return r},"cssStyleToString"),lQ=s(function(t,r,n){if(this.classes=t||[],this.attributes={},this.height=0,this.depth=0,this.maxFontSize=0,this.style=n||{},r){r.style.isTight()&&this.classes.push("mtight");var i=r.getColor();i&&(this.style.color=i)}},"initNode"),cQ=s(function(t){var r=document.createElement(t);r.className=Ld(this.classes),Object.assign(r.style,this.style);for(var n of Object.keys(this.attributes))r.setAttribute(n,this.attributes[n]);for(var i=0;i/=\x00-\x1f]/,uQ=s(function(t){var r="<"+t;this.classes.length&&(r+=' class="'+fs(Ld(this.classes))+'"');var n=L7(this.style);n&&(r+=' style="'+fs(n)+'"');for(var i of Object.keys(this.attributes)){if(fVe.test(i))throw new Nt("Invalid attribute name '"+i+"'");r+=" "+i+'="'+fs(this.attributes[i])+'"'}r+=">";for(var a=0;a",r},"toMarkup"),Dd=class{static{s(this,"Span")}constructor(t,r,n,i){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.width=void 0,this.maxFontSize=void 0,this.style=void 0,this.italic=void 0,lQ.call(this,t,n,i),this.children=r||[]}setAttribute(t,r){this.attributes[t]=r}hasClass(t){return this.classes.includes(t)}toNode(){return cQ.call(this,"span")}toMarkup(){return uQ.call(this,"span")}},j0=class{static{s(this,"Anchor")}constructor(t,r,n,i){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,lQ.call(this,r,i),this.children=n||[],this.setAttribute("href",t)}setAttribute(t,r){this.attributes[t]=r}hasClass(t){return this.classes.includes(t)}toNode(){return cQ.call(this,"a")}toMarkup(){return uQ.call(this,"a")}},h7=class{static{s(this,"Img")}constructor(t,r,n){this.src=void 0,this.alt=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.alt=r,this.src=t,this.classes=["mord"],this.height=0,this.depth=0,this.maxFontSize=0,this.style=n}hasClass(t){return this.classes.includes(t)}toNode(){var t=document.createElement("img");return t.src=this.src,t.alt=this.alt,t.className="mord",Object.assign(t.style,this.style),t}toMarkup(){var t=''+fs(this.alt)+'0&&(r=document.createElement("span"),r.style.marginRight=Ft(this.italic)),this.classes.length>0&&(r=r||document.createElement("span"),r.className=Ld(this.classes)),Object.keys(this.style).length>0&&(r=r||document.createElement("span"),Object.assign(r.style,this.style)),r?(r.appendChild(t),r):t}toMarkup(){var t=!1,r="0&&(n+="margin-right:"+Ft(this.italic)+";"),n+=L7(this.style),n&&(t=!0,r+=' style="'+fs(n)+'"');var i=fs(this.text);return t?(r+=">",r+=i,r+="",r):i}},ql=class{static{s(this,"SvgNode")}constructor(t,r){this.children=void 0,this.attributes=void 0,this.children=t||[],this.attributes=r||{}}toNode(){var t="http://www.w3.org/2000/svg",r=document.createElementNS(t,"svg");for(var n of Object.keys(this.attributes))r.setAttribute(n,this.attributes[n]);for(var i=0;i':''}},Kx=class{static{s(this,"LineNode")}constructor(t){this.attributes=void 0,this.attributes=t||{}}toNode(){var t="http://www.w3.org/2000/svg",r=document.createElementNS(t,"line");for(var n of Object.keys(this.attributes))r.setAttribute(n,this.attributes[n]);return r}toMarkup(){var t="e instanceof Dd||e instanceof j0||e instanceof _d,"hasHtmlDomChildren"),Bc={"AMS-Regular":{32:[0,0,0,0,.25],65:[0,.68889,0,0,.72222],66:[0,.68889,0,0,.66667],67:[0,.68889,0,0,.72222],68:[0,.68889,0,0,.72222],69:[0,.68889,0,0,.66667],70:[0,.68889,0,0,.61111],71:[0,.68889,0,0,.77778],72:[0,.68889,0,0,.77778],73:[0,.68889,0,0,.38889],74:[.16667,.68889,0,0,.5],75:[0,.68889,0,0,.77778],76:[0,.68889,0,0,.66667],77:[0,.68889,0,0,.94445],78:[0,.68889,0,0,.72222],79:[.16667,.68889,0,0,.77778],80:[0,.68889,0,0,.61111],81:[.16667,.68889,0,0,.77778],82:[0,.68889,0,0,.72222],83:[0,.68889,0,0,.55556],84:[0,.68889,0,0,.66667],85:[0,.68889,0,0,.72222],86:[0,.68889,0,0,.72222],87:[0,.68889,0,0,1],88:[0,.68889,0,0,.72222],89:[0,.68889,0,0,.72222],90:[0,.68889,0,0,.66667],107:[0,.68889,0,0,.55556],160:[0,0,0,0,.25],165:[0,.675,.025,0,.75],174:[.15559,.69224,0,0,.94666],240:[0,.68889,0,0,.55556],295:[0,.68889,0,0,.54028],710:[0,.825,0,0,2.33334],732:[0,.9,0,0,2.33334],770:[0,.825,0,0,2.33334],771:[0,.9,0,0,2.33334],989:[.08167,.58167,0,0,.77778],1008:[0,.43056,.04028,0,.66667],8245:[0,.54986,0,0,.275],8463:[0,.68889,0,0,.54028],8487:[0,.68889,0,0,.72222],8498:[0,.68889,0,0,.55556],8502:[0,.68889,0,0,.66667],8503:[0,.68889,0,0,.44445],8504:[0,.68889,0,0,.66667],8513:[0,.68889,0,0,.63889],8592:[-.03598,.46402,0,0,.5],8594:[-.03598,.46402,0,0,.5],8602:[-.13313,.36687,0,0,1],8603:[-.13313,.36687,0,0,1],8606:[.01354,.52239,0,0,1],8608:[.01354,.52239,0,0,1],8610:[.01354,.52239,0,0,1.11111],8611:[.01354,.52239,0,0,1.11111],8619:[0,.54986,0,0,1],8620:[0,.54986,0,0,1],8621:[-.13313,.37788,0,0,1.38889],8622:[-.13313,.36687,0,0,1],8624:[0,.69224,0,0,.5],8625:[0,.69224,0,0,.5],8630:[0,.43056,0,0,1],8631:[0,.43056,0,0,1],8634:[.08198,.58198,0,0,.77778],8635:[.08198,.58198,0,0,.77778],8638:[.19444,.69224,0,0,.41667],8639:[.19444,.69224,0,0,.41667],8642:[.19444,.69224,0,0,.41667],8643:[.19444,.69224,0,0,.41667],8644:[.1808,.675,0,0,1],8646:[.1808,.675,0,0,1],8647:[.1808,.675,0,0,1],8648:[.19444,.69224,0,0,.83334],8649:[.1808,.675,0,0,1],8650:[.19444,.69224,0,0,.83334],8651:[.01354,.52239,0,0,1],8652:[.01354,.52239,0,0,1],8653:[-.13313,.36687,0,0,1],8654:[-.13313,.36687,0,0,1],8655:[-.13313,.36687,0,0,1],8666:[.13667,.63667,0,0,1],8667:[.13667,.63667,0,0,1],8669:[-.13313,.37788,0,0,1],8672:[-.064,.437,0,0,1.334],8674:[-.064,.437,0,0,1.334],8705:[0,.825,0,0,.5],8708:[0,.68889,0,0,.55556],8709:[.08167,.58167,0,0,.77778],8717:[0,.43056,0,0,.42917],8722:[-.03598,.46402,0,0,.5],8724:[.08198,.69224,0,0,.77778],8726:[.08167,.58167,0,0,.77778],8733:[0,.69224,0,0,.77778],8736:[0,.69224,0,0,.72222],8737:[0,.69224,0,0,.72222],8738:[.03517,.52239,0,0,.72222],8739:[.08167,.58167,0,0,.22222],8740:[.25142,.74111,0,0,.27778],8741:[.08167,.58167,0,0,.38889],8742:[.25142,.74111,0,0,.5],8756:[0,.69224,0,0,.66667],8757:[0,.69224,0,0,.66667],8764:[-.13313,.36687,0,0,.77778],8765:[-.13313,.37788,0,0,.77778],8769:[-.13313,.36687,0,0,.77778],8770:[-.03625,.46375,0,0,.77778],8774:[.30274,.79383,0,0,.77778],8776:[-.01688,.48312,0,0,.77778],8778:[.08167,.58167,0,0,.77778],8782:[.06062,.54986,0,0,.77778],8783:[.06062,.54986,0,0,.77778],8785:[.08198,.58198,0,0,.77778],8786:[.08198,.58198,0,0,.77778],8787:[.08198,.58198,0,0,.77778],8790:[0,.69224,0,0,.77778],8791:[.22958,.72958,0,0,.77778],8796:[.08198,.91667,0,0,.77778],8806:[.25583,.75583,0,0,.77778],8807:[.25583,.75583,0,0,.77778],8808:[.25142,.75726,0,0,.77778],8809:[.25142,.75726,0,0,.77778],8812:[.25583,.75583,0,0,.5],8814:[.20576,.70576,0,0,.77778],8815:[.20576,.70576,0,0,.77778],8816:[.30274,.79383,0,0,.77778],8817:[.30274,.79383,0,0,.77778],8818:[.22958,.72958,0,0,.77778],8819:[.22958,.72958,0,0,.77778],8822:[.1808,.675,0,0,.77778],8823:[.1808,.675,0,0,.77778],8828:[.13667,.63667,0,0,.77778],8829:[.13667,.63667,0,0,.77778],8830:[.22958,.72958,0,0,.77778],8831:[.22958,.72958,0,0,.77778],8832:[.20576,.70576,0,0,.77778],8833:[.20576,.70576,0,0,.77778],8840:[.30274,.79383,0,0,.77778],8841:[.30274,.79383,0,0,.77778],8842:[.13597,.63597,0,0,.77778],8843:[.13597,.63597,0,0,.77778],8847:[.03517,.54986,0,0,.77778],8848:[.03517,.54986,0,0,.77778],8858:[.08198,.58198,0,0,.77778],8859:[.08198,.58198,0,0,.77778],8861:[.08198,.58198,0,0,.77778],8862:[0,.675,0,0,.77778],8863:[0,.675,0,0,.77778],8864:[0,.675,0,0,.77778],8865:[0,.675,0,0,.77778],8872:[0,.69224,0,0,.61111],8873:[0,.69224,0,0,.72222],8874:[0,.69224,0,0,.88889],8876:[0,.68889,0,0,.61111],8877:[0,.68889,0,0,.61111],8878:[0,.68889,0,0,.72222],8879:[0,.68889,0,0,.72222],8882:[.03517,.54986,0,0,.77778],8883:[.03517,.54986,0,0,.77778],8884:[.13667,.63667,0,0,.77778],8885:[.13667,.63667,0,0,.77778],8888:[0,.54986,0,0,1.11111],8890:[.19444,.43056,0,0,.55556],8891:[.19444,.69224,0,0,.61111],8892:[.19444,.69224,0,0,.61111],8901:[0,.54986,0,0,.27778],8903:[.08167,.58167,0,0,.77778],8905:[.08167,.58167,0,0,.77778],8906:[.08167,.58167,0,0,.77778],8907:[0,.69224,0,0,.77778],8908:[0,.69224,0,0,.77778],8909:[-.03598,.46402,0,0,.77778],8910:[0,.54986,0,0,.76042],8911:[0,.54986,0,0,.76042],8912:[.03517,.54986,0,0,.77778],8913:[.03517,.54986,0,0,.77778],8914:[0,.54986,0,0,.66667],8915:[0,.54986,0,0,.66667],8916:[0,.69224,0,0,.66667],8918:[.0391,.5391,0,0,.77778],8919:[.0391,.5391,0,0,.77778],8920:[.03517,.54986,0,0,1.33334],8921:[.03517,.54986,0,0,1.33334],8922:[.38569,.88569,0,0,.77778],8923:[.38569,.88569,0,0,.77778],8926:[.13667,.63667,0,0,.77778],8927:[.13667,.63667,0,0,.77778],8928:[.30274,.79383,0,0,.77778],8929:[.30274,.79383,0,0,.77778],8934:[.23222,.74111,0,0,.77778],8935:[.23222,.74111,0,0,.77778],8936:[.23222,.74111,0,0,.77778],8937:[.23222,.74111,0,0,.77778],8938:[.20576,.70576,0,0,.77778],8939:[.20576,.70576,0,0,.77778],8940:[.30274,.79383,0,0,.77778],8941:[.30274,.79383,0,0,.77778],8994:[.19444,.69224,0,0,.77778],8995:[.19444,.69224,0,0,.77778],9416:[.15559,.69224,0,0,.90222],9484:[0,.69224,0,0,.5],9488:[0,.69224,0,0,.5],9492:[0,.37788,0,0,.5],9496:[0,.37788,0,0,.5],9585:[.19444,.68889,0,0,.88889],9586:[.19444,.74111,0,0,.88889],9632:[0,.675,0,0,.77778],9633:[0,.675,0,0,.77778],9650:[0,.54986,0,0,.72222],9651:[0,.54986,0,0,.72222],9654:[.03517,.54986,0,0,.77778],9660:[0,.54986,0,0,.72222],9661:[0,.54986,0,0,.72222],9664:[.03517,.54986,0,0,.77778],9674:[.11111,.69224,0,0,.66667],9733:[.19444,.69224,0,0,.94445],10003:[0,.69224,0,0,.83334],10016:[0,.69224,0,0,.83334],10731:[.11111,.69224,0,0,.66667],10846:[.19444,.75583,0,0,.61111],10877:[.13667,.63667,0,0,.77778],10878:[.13667,.63667,0,0,.77778],10885:[.25583,.75583,0,0,.77778],10886:[.25583,.75583,0,0,.77778],10887:[.13597,.63597,0,0,.77778],10888:[.13597,.63597,0,0,.77778],10889:[.26167,.75726,0,0,.77778],10890:[.26167,.75726,0,0,.77778],10891:[.48256,.98256,0,0,.77778],10892:[.48256,.98256,0,0,.77778],10901:[.13667,.63667,0,0,.77778],10902:[.13667,.63667,0,0,.77778],10933:[.25142,.75726,0,0,.77778],10934:[.25142,.75726,0,0,.77778],10935:[.26167,.75726,0,0,.77778],10936:[.26167,.75726,0,0,.77778],10937:[.26167,.75726,0,0,.77778],10938:[.26167,.75726,0,0,.77778],10949:[.25583,.75583,0,0,.77778],10950:[.25583,.75583,0,0,.77778],10955:[.28481,.79383,0,0,.77778],10956:[.28481,.79383,0,0,.77778],57350:[.08167,.58167,0,0,.22222],57351:[.08167,.58167,0,0,.38889],57352:[.08167,.58167,0,0,.77778],57353:[0,.43056,.04028,0,.66667],57356:[.25142,.75726,0,0,.77778],57357:[.25142,.75726,0,0,.77778],57358:[.41951,.91951,0,0,.77778],57359:[.30274,.79383,0,0,.77778],57360:[.30274,.79383,0,0,.77778],57361:[.41951,.91951,0,0,.77778],57366:[.25142,.75726,0,0,.77778],57367:[.25142,.75726,0,0,.77778],57368:[.25142,.75726,0,0,.77778],57369:[.25142,.75726,0,0,.77778],57370:[.13597,.63597,0,0,.77778],57371:[.13597,.63597,0,0,.77778]},"Caligraphic-Regular":{32:[0,0,0,0,.25],65:[0,.68333,0,.19445,.79847],66:[0,.68333,.03041,.13889,.65681],67:[0,.68333,.05834,.13889,.52653],68:[0,.68333,.02778,.08334,.77139],69:[0,.68333,.08944,.11111,.52778],70:[0,.68333,.09931,.11111,.71875],71:[.09722,.68333,.0593,.11111,.59487],72:[0,.68333,.00965,.11111,.84452],73:[0,.68333,.07382,0,.54452],74:[.09722,.68333,.18472,.16667,.67778],75:[0,.68333,.01445,.05556,.76195],76:[0,.68333,0,.13889,.68972],77:[0,.68333,0,.13889,1.2009],78:[0,.68333,.14736,.08334,.82049],79:[0,.68333,.02778,.11111,.79611],80:[0,.68333,.08222,.08334,.69556],81:[.09722,.68333,0,.11111,.81667],82:[0,.68333,0,.08334,.8475],83:[0,.68333,.075,.13889,.60556],84:[0,.68333,.25417,0,.54464],85:[0,.68333,.09931,.08334,.62583],86:[0,.68333,.08222,0,.61278],87:[0,.68333,.08222,.08334,.98778],88:[0,.68333,.14643,.13889,.7133],89:[.09722,.68333,.08222,.08334,.66834],90:[0,.68333,.07944,.13889,.72473],160:[0,0,0,0,.25]},"Fraktur-Regular":{32:[0,0,0,0,.25],33:[0,.69141,0,0,.29574],34:[0,.69141,0,0,.21471],38:[0,.69141,0,0,.73786],39:[0,.69141,0,0,.21201],40:[.24982,.74947,0,0,.38865],41:[.24982,.74947,0,0,.38865],42:[0,.62119,0,0,.27764],43:[.08319,.58283,0,0,.75623],44:[0,.10803,0,0,.27764],45:[.08319,.58283,0,0,.75623],46:[0,.10803,0,0,.27764],47:[.24982,.74947,0,0,.50181],48:[0,.47534,0,0,.50181],49:[0,.47534,0,0,.50181],50:[0,.47534,0,0,.50181],51:[.18906,.47534,0,0,.50181],52:[.18906,.47534,0,0,.50181],53:[.18906,.47534,0,0,.50181],54:[0,.69141,0,0,.50181],55:[.18906,.47534,0,0,.50181],56:[0,.69141,0,0,.50181],57:[.18906,.47534,0,0,.50181],58:[0,.47534,0,0,.21606],59:[.12604,.47534,0,0,.21606],61:[-.13099,.36866,0,0,.75623],63:[0,.69141,0,0,.36245],65:[0,.69141,0,0,.7176],66:[0,.69141,0,0,.88397],67:[0,.69141,0,0,.61254],68:[0,.69141,0,0,.83158],69:[0,.69141,0,0,.66278],70:[.12604,.69141,0,0,.61119],71:[0,.69141,0,0,.78539],72:[.06302,.69141,0,0,.7203],73:[0,.69141,0,0,.55448],74:[.12604,.69141,0,0,.55231],75:[0,.69141,0,0,.66845],76:[0,.69141,0,0,.66602],77:[0,.69141,0,0,1.04953],78:[0,.69141,0,0,.83212],79:[0,.69141,0,0,.82699],80:[.18906,.69141,0,0,.82753],81:[.03781,.69141,0,0,.82699],82:[0,.69141,0,0,.82807],83:[0,.69141,0,0,.82861],84:[0,.69141,0,0,.66899],85:[0,.69141,0,0,.64576],86:[0,.69141,0,0,.83131],87:[0,.69141,0,0,1.04602],88:[0,.69141,0,0,.71922],89:[.18906,.69141,0,0,.83293],90:[.12604,.69141,0,0,.60201],91:[.24982,.74947,0,0,.27764],93:[.24982,.74947,0,0,.27764],94:[0,.69141,0,0,.49965],97:[0,.47534,0,0,.50046],98:[0,.69141,0,0,.51315],99:[0,.47534,0,0,.38946],100:[0,.62119,0,0,.49857],101:[0,.47534,0,0,.40053],102:[.18906,.69141,0,0,.32626],103:[.18906,.47534,0,0,.5037],104:[.18906,.69141,0,0,.52126],105:[0,.69141,0,0,.27899],106:[0,.69141,0,0,.28088],107:[0,.69141,0,0,.38946],108:[0,.69141,0,0,.27953],109:[0,.47534,0,0,.76676],110:[0,.47534,0,0,.52666],111:[0,.47534,0,0,.48885],112:[.18906,.52396,0,0,.50046],113:[.18906,.47534,0,0,.48912],114:[0,.47534,0,0,.38919],115:[0,.47534,0,0,.44266],116:[0,.62119,0,0,.33301],117:[0,.47534,0,0,.5172],118:[0,.52396,0,0,.5118],119:[0,.52396,0,0,.77351],120:[.18906,.47534,0,0,.38865],121:[.18906,.47534,0,0,.49884],122:[.18906,.47534,0,0,.39054],160:[0,0,0,0,.25],8216:[0,.69141,0,0,.21471],8217:[0,.69141,0,0,.21471],58112:[0,.62119,0,0,.49749],58113:[0,.62119,0,0,.4983],58114:[.18906,.69141,0,0,.33328],58115:[.18906,.69141,0,0,.32923],58116:[.18906,.47534,0,0,.50343],58117:[0,.69141,0,0,.33301],58118:[0,.62119,0,0,.33409],58119:[0,.47534,0,0,.50073]},"Main-Bold":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.35],34:[0,.69444,0,0,.60278],35:[.19444,.69444,0,0,.95833],36:[.05556,.75,0,0,.575],37:[.05556,.75,0,0,.95833],38:[0,.69444,0,0,.89444],39:[0,.69444,0,0,.31944],40:[.25,.75,0,0,.44722],41:[.25,.75,0,0,.44722],42:[0,.75,0,0,.575],43:[.13333,.63333,0,0,.89444],44:[.19444,.15556,0,0,.31944],45:[0,.44444,0,0,.38333],46:[0,.15556,0,0,.31944],47:[.25,.75,0,0,.575],48:[0,.64444,0,0,.575],49:[0,.64444,0,0,.575],50:[0,.64444,0,0,.575],51:[0,.64444,0,0,.575],52:[0,.64444,0,0,.575],53:[0,.64444,0,0,.575],54:[0,.64444,0,0,.575],55:[0,.64444,0,0,.575],56:[0,.64444,0,0,.575],57:[0,.64444,0,0,.575],58:[0,.44444,0,0,.31944],59:[.19444,.44444,0,0,.31944],60:[.08556,.58556,0,0,.89444],61:[-.10889,.39111,0,0,.89444],62:[.08556,.58556,0,0,.89444],63:[0,.69444,0,0,.54305],64:[0,.69444,0,0,.89444],65:[0,.68611,0,0,.86944],66:[0,.68611,0,0,.81805],67:[0,.68611,0,0,.83055],68:[0,.68611,0,0,.88194],69:[0,.68611,0,0,.75555],70:[0,.68611,0,0,.72361],71:[0,.68611,0,0,.90416],72:[0,.68611,0,0,.9],73:[0,.68611,0,0,.43611],74:[0,.68611,0,0,.59444],75:[0,.68611,0,0,.90138],76:[0,.68611,0,0,.69166],77:[0,.68611,0,0,1.09166],78:[0,.68611,0,0,.9],79:[0,.68611,0,0,.86388],80:[0,.68611,0,0,.78611],81:[.19444,.68611,0,0,.86388],82:[0,.68611,0,0,.8625],83:[0,.68611,0,0,.63889],84:[0,.68611,0,0,.8],85:[0,.68611,0,0,.88472],86:[0,.68611,.01597,0,.86944],87:[0,.68611,.01597,0,1.18888],88:[0,.68611,0,0,.86944],89:[0,.68611,.02875,0,.86944],90:[0,.68611,0,0,.70277],91:[.25,.75,0,0,.31944],92:[.25,.75,0,0,.575],93:[.25,.75,0,0,.31944],94:[0,.69444,0,0,.575],95:[.31,.13444,.03194,0,.575],97:[0,.44444,0,0,.55902],98:[0,.69444,0,0,.63889],99:[0,.44444,0,0,.51111],100:[0,.69444,0,0,.63889],101:[0,.44444,0,0,.52708],102:[0,.69444,.10903,0,.35139],103:[.19444,.44444,.01597,0,.575],104:[0,.69444,0,0,.63889],105:[0,.69444,0,0,.31944],106:[.19444,.69444,0,0,.35139],107:[0,.69444,0,0,.60694],108:[0,.69444,0,0,.31944],109:[0,.44444,0,0,.95833],110:[0,.44444,0,0,.63889],111:[0,.44444,0,0,.575],112:[.19444,.44444,0,0,.63889],113:[.19444,.44444,0,0,.60694],114:[0,.44444,0,0,.47361],115:[0,.44444,0,0,.45361],116:[0,.63492,0,0,.44722],117:[0,.44444,0,0,.63889],118:[0,.44444,.01597,0,.60694],119:[0,.44444,.01597,0,.83055],120:[0,.44444,0,0,.60694],121:[.19444,.44444,.01597,0,.60694],122:[0,.44444,0,0,.51111],123:[.25,.75,0,0,.575],124:[.25,.75,0,0,.31944],125:[.25,.75,0,0,.575],126:[.35,.34444,0,0,.575],160:[0,0,0,0,.25],163:[0,.69444,0,0,.86853],168:[0,.69444,0,0,.575],172:[0,.44444,0,0,.76666],176:[0,.69444,0,0,.86944],177:[.13333,.63333,0,0,.89444],184:[.17014,0,0,0,.51111],198:[0,.68611,0,0,1.04166],215:[.13333,.63333,0,0,.89444],216:[.04861,.73472,0,0,.89444],223:[0,.69444,0,0,.59722],230:[0,.44444,0,0,.83055],247:[.13333,.63333,0,0,.89444],248:[.09722,.54167,0,0,.575],305:[0,.44444,0,0,.31944],338:[0,.68611,0,0,1.16944],339:[0,.44444,0,0,.89444],567:[.19444,.44444,0,0,.35139],710:[0,.69444,0,0,.575],711:[0,.63194,0,0,.575],713:[0,.59611,0,0,.575],714:[0,.69444,0,0,.575],715:[0,.69444,0,0,.575],728:[0,.69444,0,0,.575],729:[0,.69444,0,0,.31944],730:[0,.69444,0,0,.86944],732:[0,.69444,0,0,.575],733:[0,.69444,0,0,.575],915:[0,.68611,0,0,.69166],916:[0,.68611,0,0,.95833],920:[0,.68611,0,0,.89444],923:[0,.68611,0,0,.80555],926:[0,.68611,0,0,.76666],928:[0,.68611,0,0,.9],931:[0,.68611,0,0,.83055],933:[0,.68611,0,0,.89444],934:[0,.68611,0,0,.83055],936:[0,.68611,0,0,.89444],937:[0,.68611,0,0,.83055],8211:[0,.44444,.03194,0,.575],8212:[0,.44444,.03194,0,1.14999],8216:[0,.69444,0,0,.31944],8217:[0,.69444,0,0,.31944],8220:[0,.69444,0,0,.60278],8221:[0,.69444,0,0,.60278],8224:[.19444,.69444,0,0,.51111],8225:[.19444,.69444,0,0,.51111],8242:[0,.55556,0,0,.34444],8407:[0,.72444,.15486,0,.575],8463:[0,.69444,0,0,.66759],8465:[0,.69444,0,0,.83055],8467:[0,.69444,0,0,.47361],8472:[.19444,.44444,0,0,.74027],8476:[0,.69444,0,0,.83055],8501:[0,.69444,0,0,.70277],8592:[-.10889,.39111,0,0,1.14999],8593:[.19444,.69444,0,0,.575],8594:[-.10889,.39111,0,0,1.14999],8595:[.19444,.69444,0,0,.575],8596:[-.10889,.39111,0,0,1.14999],8597:[.25,.75,0,0,.575],8598:[.19444,.69444,0,0,1.14999],8599:[.19444,.69444,0,0,1.14999],8600:[.19444,.69444,0,0,1.14999],8601:[.19444,.69444,0,0,1.14999],8636:[-.10889,.39111,0,0,1.14999],8637:[-.10889,.39111,0,0,1.14999],8640:[-.10889,.39111,0,0,1.14999],8641:[-.10889,.39111,0,0,1.14999],8656:[-.10889,.39111,0,0,1.14999],8657:[.19444,.69444,0,0,.70277],8658:[-.10889,.39111,0,0,1.14999],8659:[.19444,.69444,0,0,.70277],8660:[-.10889,.39111,0,0,1.14999],8661:[.25,.75,0,0,.70277],8704:[0,.69444,0,0,.63889],8706:[0,.69444,.06389,0,.62847],8707:[0,.69444,0,0,.63889],8709:[.05556,.75,0,0,.575],8711:[0,.68611,0,0,.95833],8712:[.08556,.58556,0,0,.76666],8715:[.08556,.58556,0,0,.76666],8722:[.13333,.63333,0,0,.89444],8723:[.13333,.63333,0,0,.89444],8725:[.25,.75,0,0,.575],8726:[.25,.75,0,0,.575],8727:[-.02778,.47222,0,0,.575],8728:[-.02639,.47361,0,0,.575],8729:[-.02639,.47361,0,0,.575],8730:[.18,.82,0,0,.95833],8733:[0,.44444,0,0,.89444],8734:[0,.44444,0,0,1.14999],8736:[0,.69224,0,0,.72222],8739:[.25,.75,0,0,.31944],8741:[.25,.75,0,0,.575],8743:[0,.55556,0,0,.76666],8744:[0,.55556,0,0,.76666],8745:[0,.55556,0,0,.76666],8746:[0,.55556,0,0,.76666],8747:[.19444,.69444,.12778,0,.56875],8764:[-.10889,.39111,0,0,.89444],8768:[.19444,.69444,0,0,.31944],8771:[.00222,.50222,0,0,.89444],8773:[.027,.638,0,0,.894],8776:[.02444,.52444,0,0,.89444],8781:[.00222,.50222,0,0,.89444],8801:[.00222,.50222,0,0,.89444],8804:[.19667,.69667,0,0,.89444],8805:[.19667,.69667,0,0,.89444],8810:[.08556,.58556,0,0,1.14999],8811:[.08556,.58556,0,0,1.14999],8826:[.08556,.58556,0,0,.89444],8827:[.08556,.58556,0,0,.89444],8834:[.08556,.58556,0,0,.89444],8835:[.08556,.58556,0,0,.89444],8838:[.19667,.69667,0,0,.89444],8839:[.19667,.69667,0,0,.89444],8846:[0,.55556,0,0,.76666],8849:[.19667,.69667,0,0,.89444],8850:[.19667,.69667,0,0,.89444],8851:[0,.55556,0,0,.76666],8852:[0,.55556,0,0,.76666],8853:[.13333,.63333,0,0,.89444],8854:[.13333,.63333,0,0,.89444],8855:[.13333,.63333,0,0,.89444],8856:[.13333,.63333,0,0,.89444],8857:[.13333,.63333,0,0,.89444],8866:[0,.69444,0,0,.70277],8867:[0,.69444,0,0,.70277],8868:[0,.69444,0,0,.89444],8869:[0,.69444,0,0,.89444],8900:[-.02639,.47361,0,0,.575],8901:[-.02639,.47361,0,0,.31944],8902:[-.02778,.47222,0,0,.575],8968:[.25,.75,0,0,.51111],8969:[.25,.75,0,0,.51111],8970:[.25,.75,0,0,.51111],8971:[.25,.75,0,0,.51111],8994:[-.13889,.36111,0,0,1.14999],8995:[-.13889,.36111,0,0,1.14999],9651:[.19444,.69444,0,0,1.02222],9657:[-.02778,.47222,0,0,.575],9661:[.19444,.69444,0,0,1.02222],9667:[-.02778,.47222,0,0,.575],9711:[.19444,.69444,0,0,1.14999],9824:[.12963,.69444,0,0,.89444],9825:[.12963,.69444,0,0,.89444],9826:[.12963,.69444,0,0,.89444],9827:[.12963,.69444,0,0,.89444],9837:[0,.75,0,0,.44722],9838:[.19444,.69444,0,0,.44722],9839:[.19444,.69444,0,0,.44722],10216:[.25,.75,0,0,.44722],10217:[.25,.75,0,0,.44722],10815:[0,.68611,0,0,.9],10927:[.19667,.69667,0,0,.89444],10928:[.19667,.69667,0,0,.89444],57376:[.19444,.69444,0,0,0]},"Main-BoldItalic":{32:[0,0,0,0,.25],33:[0,.69444,.11417,0,.38611],34:[0,.69444,.07939,0,.62055],35:[.19444,.69444,.06833,0,.94444],37:[.05556,.75,.12861,0,.94444],38:[0,.69444,.08528,0,.88555],39:[0,.69444,.12945,0,.35555],40:[.25,.75,.15806,0,.47333],41:[.25,.75,.03306,0,.47333],42:[0,.75,.14333,0,.59111],43:[.10333,.60333,.03306,0,.88555],44:[.19444,.14722,0,0,.35555],45:[0,.44444,.02611,0,.41444],46:[0,.14722,0,0,.35555],47:[.25,.75,.15806,0,.59111],48:[0,.64444,.13167,0,.59111],49:[0,.64444,.13167,0,.59111],50:[0,.64444,.13167,0,.59111],51:[0,.64444,.13167,0,.59111],52:[.19444,.64444,.13167,0,.59111],53:[0,.64444,.13167,0,.59111],54:[0,.64444,.13167,0,.59111],55:[.19444,.64444,.13167,0,.59111],56:[0,.64444,.13167,0,.59111],57:[0,.64444,.13167,0,.59111],58:[0,.44444,.06695,0,.35555],59:[.19444,.44444,.06695,0,.35555],61:[-.10889,.39111,.06833,0,.88555],63:[0,.69444,.11472,0,.59111],64:[0,.69444,.09208,0,.88555],65:[0,.68611,0,0,.86555],66:[0,.68611,.0992,0,.81666],67:[0,.68611,.14208,0,.82666],68:[0,.68611,.09062,0,.87555],69:[0,.68611,.11431,0,.75666],70:[0,.68611,.12903,0,.72722],71:[0,.68611,.07347,0,.89527],72:[0,.68611,.17208,0,.8961],73:[0,.68611,.15681,0,.47166],74:[0,.68611,.145,0,.61055],75:[0,.68611,.14208,0,.89499],76:[0,.68611,0,0,.69777],77:[0,.68611,.17208,0,1.07277],78:[0,.68611,.17208,0,.8961],79:[0,.68611,.09062,0,.85499],80:[0,.68611,.0992,0,.78721],81:[.19444,.68611,.09062,0,.85499],82:[0,.68611,.02559,0,.85944],83:[0,.68611,.11264,0,.64999],84:[0,.68611,.12903,0,.7961],85:[0,.68611,.17208,0,.88083],86:[0,.68611,.18625,0,.86555],87:[0,.68611,.18625,0,1.15999],88:[0,.68611,.15681,0,.86555],89:[0,.68611,.19803,0,.86555],90:[0,.68611,.14208,0,.70888],91:[.25,.75,.1875,0,.35611],93:[.25,.75,.09972,0,.35611],94:[0,.69444,.06709,0,.59111],95:[.31,.13444,.09811,0,.59111],97:[0,.44444,.09426,0,.59111],98:[0,.69444,.07861,0,.53222],99:[0,.44444,.05222,0,.53222],100:[0,.69444,.10861,0,.59111],101:[0,.44444,.085,0,.53222],102:[.19444,.69444,.21778,0,.4],103:[.19444,.44444,.105,0,.53222],104:[0,.69444,.09426,0,.59111],105:[0,.69326,.11387,0,.35555],106:[.19444,.69326,.1672,0,.35555],107:[0,.69444,.11111,0,.53222],108:[0,.69444,.10861,0,.29666],109:[0,.44444,.09426,0,.94444],110:[0,.44444,.09426,0,.64999],111:[0,.44444,.07861,0,.59111],112:[.19444,.44444,.07861,0,.59111],113:[.19444,.44444,.105,0,.53222],114:[0,.44444,.11111,0,.50167],115:[0,.44444,.08167,0,.48694],116:[0,.63492,.09639,0,.385],117:[0,.44444,.09426,0,.62055],118:[0,.44444,.11111,0,.53222],119:[0,.44444,.11111,0,.76777],120:[0,.44444,.12583,0,.56055],121:[.19444,.44444,.105,0,.56166],122:[0,.44444,.13889,0,.49055],126:[.35,.34444,.11472,0,.59111],160:[0,0,0,0,.25],168:[0,.69444,.11473,0,.59111],176:[0,.69444,0,0,.94888],184:[.17014,0,0,0,.53222],198:[0,.68611,.11431,0,1.02277],216:[.04861,.73472,.09062,0,.88555],223:[.19444,.69444,.09736,0,.665],230:[0,.44444,.085,0,.82666],248:[.09722,.54167,.09458,0,.59111],305:[0,.44444,.09426,0,.35555],338:[0,.68611,.11431,0,1.14054],339:[0,.44444,.085,0,.82666],567:[.19444,.44444,.04611,0,.385],710:[0,.69444,.06709,0,.59111],711:[0,.63194,.08271,0,.59111],713:[0,.59444,.10444,0,.59111],714:[0,.69444,.08528,0,.59111],715:[0,.69444,0,0,.59111],728:[0,.69444,.10333,0,.59111],729:[0,.69444,.12945,0,.35555],730:[0,.69444,0,0,.94888],732:[0,.69444,.11472,0,.59111],733:[0,.69444,.11472,0,.59111],915:[0,.68611,.12903,0,.69777],916:[0,.68611,0,0,.94444],920:[0,.68611,.09062,0,.88555],923:[0,.68611,0,0,.80666],926:[0,.68611,.15092,0,.76777],928:[0,.68611,.17208,0,.8961],931:[0,.68611,.11431,0,.82666],933:[0,.68611,.10778,0,.88555],934:[0,.68611,.05632,0,.82666],936:[0,.68611,.10778,0,.88555],937:[0,.68611,.0992,0,.82666],8211:[0,.44444,.09811,0,.59111],8212:[0,.44444,.09811,0,1.18221],8216:[0,.69444,.12945,0,.35555],8217:[0,.69444,.12945,0,.35555],8220:[0,.69444,.16772,0,.62055],8221:[0,.69444,.07939,0,.62055]},"Main-Italic":{32:[0,0,0,0,.25],33:[0,.69444,.12417,0,.30667],34:[0,.69444,.06961,0,.51444],35:[.19444,.69444,.06616,0,.81777],37:[.05556,.75,.13639,0,.81777],38:[0,.69444,.09694,0,.76666],39:[0,.69444,.12417,0,.30667],40:[.25,.75,.16194,0,.40889],41:[.25,.75,.03694,0,.40889],42:[0,.75,.14917,0,.51111],43:[.05667,.56167,.03694,0,.76666],44:[.19444,.10556,0,0,.30667],45:[0,.43056,.02826,0,.35778],46:[0,.10556,0,0,.30667],47:[.25,.75,.16194,0,.51111],48:[0,.64444,.13556,0,.51111],49:[0,.64444,.13556,0,.51111],50:[0,.64444,.13556,0,.51111],51:[0,.64444,.13556,0,.51111],52:[.19444,.64444,.13556,0,.51111],53:[0,.64444,.13556,0,.51111],54:[0,.64444,.13556,0,.51111],55:[.19444,.64444,.13556,0,.51111],56:[0,.64444,.13556,0,.51111],57:[0,.64444,.13556,0,.51111],58:[0,.43056,.0582,0,.30667],59:[.19444,.43056,.0582,0,.30667],61:[-.13313,.36687,.06616,0,.76666],63:[0,.69444,.1225,0,.51111],64:[0,.69444,.09597,0,.76666],65:[0,.68333,0,0,.74333],66:[0,.68333,.10257,0,.70389],67:[0,.68333,.14528,0,.71555],68:[0,.68333,.09403,0,.755],69:[0,.68333,.12028,0,.67833],70:[0,.68333,.13305,0,.65277],71:[0,.68333,.08722,0,.77361],72:[0,.68333,.16389,0,.74333],73:[0,.68333,.15806,0,.38555],74:[0,.68333,.14028,0,.525],75:[0,.68333,.14528,0,.76888],76:[0,.68333,0,0,.62722],77:[0,.68333,.16389,0,.89666],78:[0,.68333,.16389,0,.74333],79:[0,.68333,.09403,0,.76666],80:[0,.68333,.10257,0,.67833],81:[.19444,.68333,.09403,0,.76666],82:[0,.68333,.03868,0,.72944],83:[0,.68333,.11972,0,.56222],84:[0,.68333,.13305,0,.71555],85:[0,.68333,.16389,0,.74333],86:[0,.68333,.18361,0,.74333],87:[0,.68333,.18361,0,.99888],88:[0,.68333,.15806,0,.74333],89:[0,.68333,.19383,0,.74333],90:[0,.68333,.14528,0,.61333],91:[.25,.75,.1875,0,.30667],93:[.25,.75,.10528,0,.30667],94:[0,.69444,.06646,0,.51111],95:[.31,.12056,.09208,0,.51111],97:[0,.43056,.07671,0,.51111],98:[0,.69444,.06312,0,.46],99:[0,.43056,.05653,0,.46],100:[0,.69444,.10333,0,.51111],101:[0,.43056,.07514,0,.46],102:[.19444,.69444,.21194,0,.30667],103:[.19444,.43056,.08847,0,.46],104:[0,.69444,.07671,0,.51111],105:[0,.65536,.1019,0,.30667],106:[.19444,.65536,.14467,0,.30667],107:[0,.69444,.10764,0,.46],108:[0,.69444,.10333,0,.25555],109:[0,.43056,.07671,0,.81777],110:[0,.43056,.07671,0,.56222],111:[0,.43056,.06312,0,.51111],112:[.19444,.43056,.06312,0,.51111],113:[.19444,.43056,.08847,0,.46],114:[0,.43056,.10764,0,.42166],115:[0,.43056,.08208,0,.40889],116:[0,.61508,.09486,0,.33222],117:[0,.43056,.07671,0,.53666],118:[0,.43056,.10764,0,.46],119:[0,.43056,.10764,0,.66444],120:[0,.43056,.12042,0,.46389],121:[.19444,.43056,.08847,0,.48555],122:[0,.43056,.12292,0,.40889],126:[.35,.31786,.11585,0,.51111],160:[0,0,0,0,.25],168:[0,.66786,.10474,0,.51111],176:[0,.69444,0,0,.83129],184:[.17014,0,0,0,.46],198:[0,.68333,.12028,0,.88277],216:[.04861,.73194,.09403,0,.76666],223:[.19444,.69444,.10514,0,.53666],230:[0,.43056,.07514,0,.71555],248:[.09722,.52778,.09194,0,.51111],338:[0,.68333,.12028,0,.98499],339:[0,.43056,.07514,0,.71555],710:[0,.69444,.06646,0,.51111],711:[0,.62847,.08295,0,.51111],713:[0,.56167,.10333,0,.51111],714:[0,.69444,.09694,0,.51111],715:[0,.69444,0,0,.51111],728:[0,.69444,.10806,0,.51111],729:[0,.66786,.11752,0,.30667],730:[0,.69444,0,0,.83129],732:[0,.66786,.11585,0,.51111],733:[0,.69444,.1225,0,.51111],915:[0,.68333,.13305,0,.62722],916:[0,.68333,0,0,.81777],920:[0,.68333,.09403,0,.76666],923:[0,.68333,0,0,.69222],926:[0,.68333,.15294,0,.66444],928:[0,.68333,.16389,0,.74333],931:[0,.68333,.12028,0,.71555],933:[0,.68333,.11111,0,.76666],934:[0,.68333,.05986,0,.71555],936:[0,.68333,.11111,0,.76666],937:[0,.68333,.10257,0,.71555],8211:[0,.43056,.09208,0,.51111],8212:[0,.43056,.09208,0,1.02222],8216:[0,.69444,.12417,0,.30667],8217:[0,.69444,.12417,0,.30667],8220:[0,.69444,.1685,0,.51444],8221:[0,.69444,.06961,0,.51444],8463:[0,.68889,0,0,.54028]},"Main-Regular":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.27778],34:[0,.69444,0,0,.5],35:[.19444,.69444,0,0,.83334],36:[.05556,.75,0,0,.5],37:[.05556,.75,0,0,.83334],38:[0,.69444,0,0,.77778],39:[0,.69444,0,0,.27778],40:[.25,.75,0,0,.38889],41:[.25,.75,0,0,.38889],42:[0,.75,0,0,.5],43:[.08333,.58333,0,0,.77778],44:[.19444,.10556,0,0,.27778],45:[0,.43056,0,0,.33333],46:[0,.10556,0,0,.27778],47:[.25,.75,0,0,.5],48:[0,.64444,0,0,.5],49:[0,.64444,0,0,.5],50:[0,.64444,0,0,.5],51:[0,.64444,0,0,.5],52:[0,.64444,0,0,.5],53:[0,.64444,0,0,.5],54:[0,.64444,0,0,.5],55:[0,.64444,0,0,.5],56:[0,.64444,0,0,.5],57:[0,.64444,0,0,.5],58:[0,.43056,0,0,.27778],59:[.19444,.43056,0,0,.27778],60:[.0391,.5391,0,0,.77778],61:[-.13313,.36687,0,0,.77778],62:[.0391,.5391,0,0,.77778],63:[0,.69444,0,0,.47222],64:[0,.69444,0,0,.77778],65:[0,.68333,0,0,.75],66:[0,.68333,0,0,.70834],67:[0,.68333,0,0,.72222],68:[0,.68333,0,0,.76389],69:[0,.68333,0,0,.68056],70:[0,.68333,0,0,.65278],71:[0,.68333,0,0,.78472],72:[0,.68333,0,0,.75],73:[0,.68333,0,0,.36111],74:[0,.68333,0,0,.51389],75:[0,.68333,0,0,.77778],76:[0,.68333,0,0,.625],77:[0,.68333,0,0,.91667],78:[0,.68333,0,0,.75],79:[0,.68333,0,0,.77778],80:[0,.68333,0,0,.68056],81:[.19444,.68333,0,0,.77778],82:[0,.68333,0,0,.73611],83:[0,.68333,0,0,.55556],84:[0,.68333,0,0,.72222],85:[0,.68333,0,0,.75],86:[0,.68333,.01389,0,.75],87:[0,.68333,.01389,0,1.02778],88:[0,.68333,0,0,.75],89:[0,.68333,.025,0,.75],90:[0,.68333,0,0,.61111],91:[.25,.75,0,0,.27778],92:[.25,.75,0,0,.5],93:[.25,.75,0,0,.27778],94:[0,.69444,0,0,.5],95:[.31,.12056,.02778,0,.5],97:[0,.43056,0,0,.5],98:[0,.69444,0,0,.55556],99:[0,.43056,0,0,.44445],100:[0,.69444,0,0,.55556],101:[0,.43056,0,0,.44445],102:[0,.69444,.07778,0,.30556],103:[.19444,.43056,.01389,0,.5],104:[0,.69444,0,0,.55556],105:[0,.66786,0,0,.27778],106:[.19444,.66786,0,0,.30556],107:[0,.69444,0,0,.52778],108:[0,.69444,0,0,.27778],109:[0,.43056,0,0,.83334],110:[0,.43056,0,0,.55556],111:[0,.43056,0,0,.5],112:[.19444,.43056,0,0,.55556],113:[.19444,.43056,0,0,.52778],114:[0,.43056,0,0,.39167],115:[0,.43056,0,0,.39445],116:[0,.61508,0,0,.38889],117:[0,.43056,0,0,.55556],118:[0,.43056,.01389,0,.52778],119:[0,.43056,.01389,0,.72222],120:[0,.43056,0,0,.52778],121:[.19444,.43056,.01389,0,.52778],122:[0,.43056,0,0,.44445],123:[.25,.75,0,0,.5],124:[.25,.75,0,0,.27778],125:[.25,.75,0,0,.5],126:[.35,.31786,0,0,.5],160:[0,0,0,0,.25],163:[0,.69444,0,0,.76909],167:[.19444,.69444,0,0,.44445],168:[0,.66786,0,0,.5],172:[0,.43056,0,0,.66667],176:[0,.69444,0,0,.75],177:[.08333,.58333,0,0,.77778],182:[.19444,.69444,0,0,.61111],184:[.17014,0,0,0,.44445],198:[0,.68333,0,0,.90278],215:[.08333,.58333,0,0,.77778],216:[.04861,.73194,0,0,.77778],223:[0,.69444,0,0,.5],230:[0,.43056,0,0,.72222],247:[.08333,.58333,0,0,.77778],248:[.09722,.52778,0,0,.5],305:[0,.43056,0,0,.27778],338:[0,.68333,0,0,1.01389],339:[0,.43056,0,0,.77778],567:[.19444,.43056,0,0,.30556],710:[0,.69444,0,0,.5],711:[0,.62847,0,0,.5],713:[0,.56778,0,0,.5],714:[0,.69444,0,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,0,0,.5],729:[0,.66786,0,0,.27778],730:[0,.69444,0,0,.75],732:[0,.66786,0,0,.5],733:[0,.69444,0,0,.5],915:[0,.68333,0,0,.625],916:[0,.68333,0,0,.83334],920:[0,.68333,0,0,.77778],923:[0,.68333,0,0,.69445],926:[0,.68333,0,0,.66667],928:[0,.68333,0,0,.75],931:[0,.68333,0,0,.72222],933:[0,.68333,0,0,.77778],934:[0,.68333,0,0,.72222],936:[0,.68333,0,0,.77778],937:[0,.68333,0,0,.72222],8211:[0,.43056,.02778,0,.5],8212:[0,.43056,.02778,0,1],8216:[0,.69444,0,0,.27778],8217:[0,.69444,0,0,.27778],8220:[0,.69444,0,0,.5],8221:[0,.69444,0,0,.5],8224:[.19444,.69444,0,0,.44445],8225:[.19444,.69444,0,0,.44445],8230:[0,.123,0,0,1.172],8242:[0,.55556,0,0,.275],8407:[0,.71444,.15382,0,.5],8463:[0,.68889,0,0,.54028],8465:[0,.69444,0,0,.72222],8467:[0,.69444,0,.11111,.41667],8472:[.19444,.43056,0,.11111,.63646],8476:[0,.69444,0,0,.72222],8501:[0,.69444,0,0,.61111],8592:[-.13313,.36687,0,0,1],8593:[.19444,.69444,0,0,.5],8594:[-.13313,.36687,0,0,1],8595:[.19444,.69444,0,0,.5],8596:[-.13313,.36687,0,0,1],8597:[.25,.75,0,0,.5],8598:[.19444,.69444,0,0,1],8599:[.19444,.69444,0,0,1],8600:[.19444,.69444,0,0,1],8601:[.19444,.69444,0,0,1],8614:[.011,.511,0,0,1],8617:[.011,.511,0,0,1.126],8618:[.011,.511,0,0,1.126],8636:[-.13313,.36687,0,0,1],8637:[-.13313,.36687,0,0,1],8640:[-.13313,.36687,0,0,1],8641:[-.13313,.36687,0,0,1],8652:[.011,.671,0,0,1],8656:[-.13313,.36687,0,0,1],8657:[.19444,.69444,0,0,.61111],8658:[-.13313,.36687,0,0,1],8659:[.19444,.69444,0,0,.61111],8660:[-.13313,.36687,0,0,1],8661:[.25,.75,0,0,.61111],8704:[0,.69444,0,0,.55556],8706:[0,.69444,.05556,.08334,.5309],8707:[0,.69444,0,0,.55556],8709:[.05556,.75,0,0,.5],8711:[0,.68333,0,0,.83334],8712:[.0391,.5391,0,0,.66667],8715:[.0391,.5391,0,0,.66667],8722:[.08333,.58333,0,0,.77778],8723:[.08333,.58333,0,0,.77778],8725:[.25,.75,0,0,.5],8726:[.25,.75,0,0,.5],8727:[-.03472,.46528,0,0,.5],8728:[-.05555,.44445,0,0,.5],8729:[-.05555,.44445,0,0,.5],8730:[.2,.8,0,0,.83334],8733:[0,.43056,0,0,.77778],8734:[0,.43056,0,0,1],8736:[0,.69224,0,0,.72222],8739:[.25,.75,0,0,.27778],8741:[.25,.75,0,0,.5],8743:[0,.55556,0,0,.66667],8744:[0,.55556,0,0,.66667],8745:[0,.55556,0,0,.66667],8746:[0,.55556,0,0,.66667],8747:[.19444,.69444,.11111,0,.41667],8764:[-.13313,.36687,0,0,.77778],8768:[.19444,.69444,0,0,.27778],8771:[-.03625,.46375,0,0,.77778],8773:[-.022,.589,0,0,.778],8776:[-.01688,.48312,0,0,.77778],8781:[-.03625,.46375,0,0,.77778],8784:[-.133,.673,0,0,.778],8801:[-.03625,.46375,0,0,.77778],8804:[.13597,.63597,0,0,.77778],8805:[.13597,.63597,0,0,.77778],8810:[.0391,.5391,0,0,1],8811:[.0391,.5391,0,0,1],8826:[.0391,.5391,0,0,.77778],8827:[.0391,.5391,0,0,.77778],8834:[.0391,.5391,0,0,.77778],8835:[.0391,.5391,0,0,.77778],8838:[.13597,.63597,0,0,.77778],8839:[.13597,.63597,0,0,.77778],8846:[0,.55556,0,0,.66667],8849:[.13597,.63597,0,0,.77778],8850:[.13597,.63597,0,0,.77778],8851:[0,.55556,0,0,.66667],8852:[0,.55556,0,0,.66667],8853:[.08333,.58333,0,0,.77778],8854:[.08333,.58333,0,0,.77778],8855:[.08333,.58333,0,0,.77778],8856:[.08333,.58333,0,0,.77778],8857:[.08333,.58333,0,0,.77778],8866:[0,.69444,0,0,.61111],8867:[0,.69444,0,0,.61111],8868:[0,.69444,0,0,.77778],8869:[0,.69444,0,0,.77778],8872:[.249,.75,0,0,.867],8900:[-.05555,.44445,0,0,.5],8901:[-.05555,.44445,0,0,.27778],8902:[-.03472,.46528,0,0,.5],8904:[.005,.505,0,0,.9],8942:[.03,.903,0,0,.278],8943:[-.19,.313,0,0,1.172],8945:[-.1,.823,0,0,1.282],8968:[.25,.75,0,0,.44445],8969:[.25,.75,0,0,.44445],8970:[.25,.75,0,0,.44445],8971:[.25,.75,0,0,.44445],8994:[-.14236,.35764,0,0,1],8995:[-.14236,.35764,0,0,1],9136:[.244,.744,0,0,.412],9137:[.244,.745,0,0,.412],9651:[.19444,.69444,0,0,.88889],9657:[-.03472,.46528,0,0,.5],9661:[.19444,.69444,0,0,.88889],9667:[-.03472,.46528,0,0,.5],9711:[.19444,.69444,0,0,1],9824:[.12963,.69444,0,0,.77778],9825:[.12963,.69444,0,0,.77778],9826:[.12963,.69444,0,0,.77778],9827:[.12963,.69444,0,0,.77778],9837:[0,.75,0,0,.38889],9838:[.19444,.69444,0,0,.38889],9839:[.19444,.69444,0,0,.38889],10216:[.25,.75,0,0,.38889],10217:[.25,.75,0,0,.38889],10222:[.244,.744,0,0,.412],10223:[.244,.745,0,0,.412],10229:[.011,.511,0,0,1.609],10230:[.011,.511,0,0,1.638],10231:[.011,.511,0,0,1.859],10232:[.024,.525,0,0,1.609],10233:[.024,.525,0,0,1.638],10234:[.024,.525,0,0,1.858],10236:[.011,.511,0,0,1.638],10815:[0,.68333,0,0,.75],10927:[.13597,.63597,0,0,.77778],10928:[.13597,.63597,0,0,.77778],57376:[.19444,.69444,0,0,0]},"Math-BoldItalic":{32:[0,0,0,0,.25],48:[0,.44444,0,0,.575],49:[0,.44444,0,0,.575],50:[0,.44444,0,0,.575],51:[.19444,.44444,0,0,.575],52:[.19444,.44444,0,0,.575],53:[.19444,.44444,0,0,.575],54:[0,.64444,0,0,.575],55:[.19444,.44444,0,0,.575],56:[0,.64444,0,0,.575],57:[.19444,.44444,0,0,.575],65:[0,.68611,0,0,.86944],66:[0,.68611,.04835,0,.8664],67:[0,.68611,.06979,0,.81694],68:[0,.68611,.03194,0,.93812],69:[0,.68611,.05451,0,.81007],70:[0,.68611,.15972,0,.68889],71:[0,.68611,0,0,.88673],72:[0,.68611,.08229,0,.98229],73:[0,.68611,.07778,0,.51111],74:[0,.68611,.10069,0,.63125],75:[0,.68611,.06979,0,.97118],76:[0,.68611,0,0,.75555],77:[0,.68611,.11424,0,1.14201],78:[0,.68611,.11424,0,.95034],79:[0,.68611,.03194,0,.83666],80:[0,.68611,.15972,0,.72309],81:[.19444,.68611,0,0,.86861],82:[0,.68611,.00421,0,.87235],83:[0,.68611,.05382,0,.69271],84:[0,.68611,.15972,0,.63663],85:[0,.68611,.11424,0,.80027],86:[0,.68611,.25555,0,.67778],87:[0,.68611,.15972,0,1.09305],88:[0,.68611,.07778,0,.94722],89:[0,.68611,.25555,0,.67458],90:[0,.68611,.06979,0,.77257],97:[0,.44444,0,0,.63287],98:[0,.69444,0,0,.52083],99:[0,.44444,0,0,.51342],100:[0,.69444,0,0,.60972],101:[0,.44444,0,0,.55361],102:[.19444,.69444,.11042,0,.56806],103:[.19444,.44444,.03704,0,.5449],104:[0,.69444,0,0,.66759],105:[0,.69326,0,0,.4048],106:[.19444,.69326,.0622,0,.47083],107:[0,.69444,.01852,0,.6037],108:[0,.69444,.0088,0,.34815],109:[0,.44444,0,0,1.0324],110:[0,.44444,0,0,.71296],111:[0,.44444,0,0,.58472],112:[.19444,.44444,0,0,.60092],113:[.19444,.44444,.03704,0,.54213],114:[0,.44444,.03194,0,.5287],115:[0,.44444,0,0,.53125],116:[0,.63492,0,0,.41528],117:[0,.44444,0,0,.68102],118:[0,.44444,.03704,0,.56666],119:[0,.44444,.02778,0,.83148],120:[0,.44444,0,0,.65903],121:[.19444,.44444,.03704,0,.59028],122:[0,.44444,.04213,0,.55509],160:[0,0,0,0,.25],915:[0,.68611,.15972,0,.65694],916:[0,.68611,0,0,.95833],920:[0,.68611,.03194,0,.86722],923:[0,.68611,0,0,.80555],926:[0,.68611,.07458,0,.84125],928:[0,.68611,.08229,0,.98229],931:[0,.68611,.05451,0,.88507],933:[0,.68611,.15972,0,.67083],934:[0,.68611,0,0,.76666],936:[0,.68611,.11653,0,.71402],937:[0,.68611,.04835,0,.8789],945:[0,.44444,0,0,.76064],946:[.19444,.69444,.03403,0,.65972],947:[.19444,.44444,.06389,0,.59003],948:[0,.69444,.03819,0,.52222],949:[0,.44444,0,0,.52882],950:[.19444,.69444,.06215,0,.50833],951:[.19444,.44444,.03704,0,.6],952:[0,.69444,.03194,0,.5618],953:[0,.44444,0,0,.41204],954:[0,.44444,0,0,.66759],955:[0,.69444,0,0,.67083],956:[.19444,.44444,0,0,.70787],957:[0,.44444,.06898,0,.57685],958:[.19444,.69444,.03021,0,.50833],959:[0,.44444,0,0,.58472],960:[0,.44444,.03704,0,.68241],961:[.19444,.44444,0,0,.6118],962:[.09722,.44444,.07917,0,.42361],963:[0,.44444,.03704,0,.68588],964:[0,.44444,.13472,0,.52083],965:[0,.44444,.03704,0,.63055],966:[.19444,.44444,0,0,.74722],967:[.19444,.44444,0,0,.71805],968:[.19444,.69444,.03704,0,.75833],969:[0,.44444,.03704,0,.71782],977:[0,.69444,0,0,.69155],981:[.19444,.69444,0,0,.7125],982:[0,.44444,.03194,0,.975],1009:[.19444,.44444,0,0,.6118],1013:[0,.44444,0,0,.48333],57649:[0,.44444,0,0,.39352],57911:[.19444,.44444,0,0,.43889]},"Math-Italic":{32:[0,0,0,0,.25],48:[0,.43056,0,0,.5],49:[0,.43056,0,0,.5],50:[0,.43056,0,0,.5],51:[.19444,.43056,0,0,.5],52:[.19444,.43056,0,0,.5],53:[.19444,.43056,0,0,.5],54:[0,.64444,0,0,.5],55:[.19444,.43056,0,0,.5],56:[0,.64444,0,0,.5],57:[.19444,.43056,0,0,.5],65:[0,.68333,0,.13889,.75],66:[0,.68333,.05017,.08334,.75851],67:[0,.68333,.07153,.08334,.71472],68:[0,.68333,.02778,.05556,.82792],69:[0,.68333,.05764,.08334,.7382],70:[0,.68333,.13889,.08334,.64306],71:[0,.68333,0,.08334,.78625],72:[0,.68333,.08125,.05556,.83125],73:[0,.68333,.07847,.11111,.43958],74:[0,.68333,.09618,.16667,.55451],75:[0,.68333,.07153,.05556,.84931],76:[0,.68333,0,.02778,.68056],77:[0,.68333,.10903,.08334,.97014],78:[0,.68333,.10903,.08334,.80347],79:[0,.68333,.02778,.08334,.76278],80:[0,.68333,.13889,.08334,.64201],81:[.19444,.68333,0,.08334,.79056],82:[0,.68333,.00773,.08334,.75929],83:[0,.68333,.05764,.08334,.6132],84:[0,.68333,.13889,.08334,.58438],85:[0,.68333,.10903,.02778,.68278],86:[0,.68333,.22222,0,.58333],87:[0,.68333,.13889,0,.94445],88:[0,.68333,.07847,.08334,.82847],89:[0,.68333,.22222,0,.58056],90:[0,.68333,.07153,.08334,.68264],97:[0,.43056,0,0,.52859],98:[0,.69444,0,0,.42917],99:[0,.43056,0,.05556,.43276],100:[0,.69444,0,.16667,.52049],101:[0,.43056,0,.05556,.46563],102:[.19444,.69444,.10764,.16667,.48959],103:[.19444,.43056,.03588,.02778,.47697],104:[0,.69444,0,0,.57616],105:[0,.65952,0,0,.34451],106:[.19444,.65952,.05724,0,.41181],107:[0,.69444,.03148,0,.5206],108:[0,.69444,.01968,.08334,.29838],109:[0,.43056,0,0,.87801],110:[0,.43056,0,0,.60023],111:[0,.43056,0,.05556,.48472],112:[.19444,.43056,0,.08334,.50313],113:[.19444,.43056,.03588,.08334,.44641],114:[0,.43056,.02778,.05556,.45116],115:[0,.43056,0,.05556,.46875],116:[0,.61508,0,.08334,.36111],117:[0,.43056,0,.02778,.57246],118:[0,.43056,.03588,.02778,.48472],119:[0,.43056,.02691,.08334,.71592],120:[0,.43056,0,.02778,.57153],121:[.19444,.43056,.03588,.05556,.49028],122:[0,.43056,.04398,.05556,.46505],160:[0,0,0,0,.25],915:[0,.68333,.13889,.08334,.61528],916:[0,.68333,0,.16667,.83334],920:[0,.68333,.02778,.08334,.76278],923:[0,.68333,0,.16667,.69445],926:[0,.68333,.07569,.08334,.74236],928:[0,.68333,.08125,.05556,.83125],931:[0,.68333,.05764,.08334,.77986],933:[0,.68333,.13889,.05556,.58333],934:[0,.68333,0,.08334,.66667],936:[0,.68333,.11,.05556,.61222],937:[0,.68333,.05017,.08334,.7724],945:[0,.43056,.0037,.02778,.6397],946:[.19444,.69444,.05278,.08334,.56563],947:[.19444,.43056,.05556,0,.51773],948:[0,.69444,.03785,.05556,.44444],949:[0,.43056,0,.08334,.46632],950:[.19444,.69444,.07378,.08334,.4375],951:[.19444,.43056,.03588,.05556,.49653],952:[0,.69444,.02778,.08334,.46944],953:[0,.43056,0,.05556,.35394],954:[0,.43056,0,0,.57616],955:[0,.69444,0,0,.58334],956:[.19444,.43056,0,.02778,.60255],957:[0,.43056,.06366,.02778,.49398],958:[.19444,.69444,.04601,.11111,.4375],959:[0,.43056,0,.05556,.48472],960:[0,.43056,.03588,0,.57003],961:[.19444,.43056,0,.08334,.51702],962:[.09722,.43056,.07986,.08334,.36285],963:[0,.43056,.03588,0,.57141],964:[0,.43056,.1132,.02778,.43715],965:[0,.43056,.03588,.02778,.54028],966:[.19444,.43056,0,.08334,.65417],967:[.19444,.43056,0,.05556,.62569],968:[.19444,.69444,.03588,.11111,.65139],969:[0,.43056,.03588,0,.62245],977:[0,.69444,0,.08334,.59144],981:[.19444,.69444,0,.08334,.59583],982:[0,.43056,.02778,0,.82813],1009:[.19444,.43056,0,.08334,.51702],1013:[0,.43056,0,.05556,.4059],57649:[0,.43056,0,.02778,.32246],57911:[.19444,.43056,0,.08334,.38403]},"SansSerif-Bold":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.36667],34:[0,.69444,0,0,.55834],35:[.19444,.69444,0,0,.91667],36:[.05556,.75,0,0,.55],37:[.05556,.75,0,0,1.02912],38:[0,.69444,0,0,.83056],39:[0,.69444,0,0,.30556],40:[.25,.75,0,0,.42778],41:[.25,.75,0,0,.42778],42:[0,.75,0,0,.55],43:[.11667,.61667,0,0,.85556],44:[.10556,.13056,0,0,.30556],45:[0,.45833,0,0,.36667],46:[0,.13056,0,0,.30556],47:[.25,.75,0,0,.55],48:[0,.69444,0,0,.55],49:[0,.69444,0,0,.55],50:[0,.69444,0,0,.55],51:[0,.69444,0,0,.55],52:[0,.69444,0,0,.55],53:[0,.69444,0,0,.55],54:[0,.69444,0,0,.55],55:[0,.69444,0,0,.55],56:[0,.69444,0,0,.55],57:[0,.69444,0,0,.55],58:[0,.45833,0,0,.30556],59:[.10556,.45833,0,0,.30556],61:[-.09375,.40625,0,0,.85556],63:[0,.69444,0,0,.51945],64:[0,.69444,0,0,.73334],65:[0,.69444,0,0,.73334],66:[0,.69444,0,0,.73334],67:[0,.69444,0,0,.70278],68:[0,.69444,0,0,.79445],69:[0,.69444,0,0,.64167],70:[0,.69444,0,0,.61111],71:[0,.69444,0,0,.73334],72:[0,.69444,0,0,.79445],73:[0,.69444,0,0,.33056],74:[0,.69444,0,0,.51945],75:[0,.69444,0,0,.76389],76:[0,.69444,0,0,.58056],77:[0,.69444,0,0,.97778],78:[0,.69444,0,0,.79445],79:[0,.69444,0,0,.79445],80:[0,.69444,0,0,.70278],81:[.10556,.69444,0,0,.79445],82:[0,.69444,0,0,.70278],83:[0,.69444,0,0,.61111],84:[0,.69444,0,0,.73334],85:[0,.69444,0,0,.76389],86:[0,.69444,.01528,0,.73334],87:[0,.69444,.01528,0,1.03889],88:[0,.69444,0,0,.73334],89:[0,.69444,.0275,0,.73334],90:[0,.69444,0,0,.67223],91:[.25,.75,0,0,.34306],93:[.25,.75,0,0,.34306],94:[0,.69444,0,0,.55],95:[.35,.10833,.03056,0,.55],97:[0,.45833,0,0,.525],98:[0,.69444,0,0,.56111],99:[0,.45833,0,0,.48889],100:[0,.69444,0,0,.56111],101:[0,.45833,0,0,.51111],102:[0,.69444,.07639,0,.33611],103:[.19444,.45833,.01528,0,.55],104:[0,.69444,0,0,.56111],105:[0,.69444,0,0,.25556],106:[.19444,.69444,0,0,.28611],107:[0,.69444,0,0,.53056],108:[0,.69444,0,0,.25556],109:[0,.45833,0,0,.86667],110:[0,.45833,0,0,.56111],111:[0,.45833,0,0,.55],112:[.19444,.45833,0,0,.56111],113:[.19444,.45833,0,0,.56111],114:[0,.45833,.01528,0,.37222],115:[0,.45833,0,0,.42167],116:[0,.58929,0,0,.40417],117:[0,.45833,0,0,.56111],118:[0,.45833,.01528,0,.5],119:[0,.45833,.01528,0,.74445],120:[0,.45833,0,0,.5],121:[.19444,.45833,.01528,0,.5],122:[0,.45833,0,0,.47639],126:[.35,.34444,0,0,.55],160:[0,0,0,0,.25],168:[0,.69444,0,0,.55],176:[0,.69444,0,0,.73334],180:[0,.69444,0,0,.55],184:[.17014,0,0,0,.48889],305:[0,.45833,0,0,.25556],567:[.19444,.45833,0,0,.28611],710:[0,.69444,0,0,.55],711:[0,.63542,0,0,.55],713:[0,.63778,0,0,.55],728:[0,.69444,0,0,.55],729:[0,.69444,0,0,.30556],730:[0,.69444,0,0,.73334],732:[0,.69444,0,0,.55],733:[0,.69444,0,0,.55],915:[0,.69444,0,0,.58056],916:[0,.69444,0,0,.91667],920:[0,.69444,0,0,.85556],923:[0,.69444,0,0,.67223],926:[0,.69444,0,0,.73334],928:[0,.69444,0,0,.79445],931:[0,.69444,0,0,.79445],933:[0,.69444,0,0,.85556],934:[0,.69444,0,0,.79445],936:[0,.69444,0,0,.85556],937:[0,.69444,0,0,.79445],8211:[0,.45833,.03056,0,.55],8212:[0,.45833,.03056,0,1.10001],8216:[0,.69444,0,0,.30556],8217:[0,.69444,0,0,.30556],8220:[0,.69444,0,0,.55834],8221:[0,.69444,0,0,.55834]},"SansSerif-Italic":{32:[0,0,0,0,.25],33:[0,.69444,.05733,0,.31945],34:[0,.69444,.00316,0,.5],35:[.19444,.69444,.05087,0,.83334],36:[.05556,.75,.11156,0,.5],37:[.05556,.75,.03126,0,.83334],38:[0,.69444,.03058,0,.75834],39:[0,.69444,.07816,0,.27778],40:[.25,.75,.13164,0,.38889],41:[.25,.75,.02536,0,.38889],42:[0,.75,.11775,0,.5],43:[.08333,.58333,.02536,0,.77778],44:[.125,.08333,0,0,.27778],45:[0,.44444,.01946,0,.33333],46:[0,.08333,0,0,.27778],47:[.25,.75,.13164,0,.5],48:[0,.65556,.11156,0,.5],49:[0,.65556,.11156,0,.5],50:[0,.65556,.11156,0,.5],51:[0,.65556,.11156,0,.5],52:[0,.65556,.11156,0,.5],53:[0,.65556,.11156,0,.5],54:[0,.65556,.11156,0,.5],55:[0,.65556,.11156,0,.5],56:[0,.65556,.11156,0,.5],57:[0,.65556,.11156,0,.5],58:[0,.44444,.02502,0,.27778],59:[.125,.44444,.02502,0,.27778],61:[-.13,.37,.05087,0,.77778],63:[0,.69444,.11809,0,.47222],64:[0,.69444,.07555,0,.66667],65:[0,.69444,0,0,.66667],66:[0,.69444,.08293,0,.66667],67:[0,.69444,.11983,0,.63889],68:[0,.69444,.07555,0,.72223],69:[0,.69444,.11983,0,.59722],70:[0,.69444,.13372,0,.56945],71:[0,.69444,.11983,0,.66667],72:[0,.69444,.08094,0,.70834],73:[0,.69444,.13372,0,.27778],74:[0,.69444,.08094,0,.47222],75:[0,.69444,.11983,0,.69445],76:[0,.69444,0,0,.54167],77:[0,.69444,.08094,0,.875],78:[0,.69444,.08094,0,.70834],79:[0,.69444,.07555,0,.73611],80:[0,.69444,.08293,0,.63889],81:[.125,.69444,.07555,0,.73611],82:[0,.69444,.08293,0,.64584],83:[0,.69444,.09205,0,.55556],84:[0,.69444,.13372,0,.68056],85:[0,.69444,.08094,0,.6875],86:[0,.69444,.1615,0,.66667],87:[0,.69444,.1615,0,.94445],88:[0,.69444,.13372,0,.66667],89:[0,.69444,.17261,0,.66667],90:[0,.69444,.11983,0,.61111],91:[.25,.75,.15942,0,.28889],93:[.25,.75,.08719,0,.28889],94:[0,.69444,.0799,0,.5],95:[.35,.09444,.08616,0,.5],97:[0,.44444,.00981,0,.48056],98:[0,.69444,.03057,0,.51667],99:[0,.44444,.08336,0,.44445],100:[0,.69444,.09483,0,.51667],101:[0,.44444,.06778,0,.44445],102:[0,.69444,.21705,0,.30556],103:[.19444,.44444,.10836,0,.5],104:[0,.69444,.01778,0,.51667],105:[0,.67937,.09718,0,.23889],106:[.19444,.67937,.09162,0,.26667],107:[0,.69444,.08336,0,.48889],108:[0,.69444,.09483,0,.23889],109:[0,.44444,.01778,0,.79445],110:[0,.44444,.01778,0,.51667],111:[0,.44444,.06613,0,.5],112:[.19444,.44444,.0389,0,.51667],113:[.19444,.44444,.04169,0,.51667],114:[0,.44444,.10836,0,.34167],115:[0,.44444,.0778,0,.38333],116:[0,.57143,.07225,0,.36111],117:[0,.44444,.04169,0,.51667],118:[0,.44444,.10836,0,.46111],119:[0,.44444,.10836,0,.68334],120:[0,.44444,.09169,0,.46111],121:[.19444,.44444,.10836,0,.46111],122:[0,.44444,.08752,0,.43472],126:[.35,.32659,.08826,0,.5],160:[0,0,0,0,.25],168:[0,.67937,.06385,0,.5],176:[0,.69444,0,0,.73752],184:[.17014,0,0,0,.44445],305:[0,.44444,.04169,0,.23889],567:[.19444,.44444,.04169,0,.26667],710:[0,.69444,.0799,0,.5],711:[0,.63194,.08432,0,.5],713:[0,.60889,.08776,0,.5],714:[0,.69444,.09205,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,.09483,0,.5],729:[0,.67937,.07774,0,.27778],730:[0,.69444,0,0,.73752],732:[0,.67659,.08826,0,.5],733:[0,.69444,.09205,0,.5],915:[0,.69444,.13372,0,.54167],916:[0,.69444,0,0,.83334],920:[0,.69444,.07555,0,.77778],923:[0,.69444,0,0,.61111],926:[0,.69444,.12816,0,.66667],928:[0,.69444,.08094,0,.70834],931:[0,.69444,.11983,0,.72222],933:[0,.69444,.09031,0,.77778],934:[0,.69444,.04603,0,.72222],936:[0,.69444,.09031,0,.77778],937:[0,.69444,.08293,0,.72222],8211:[0,.44444,.08616,0,.5],8212:[0,.44444,.08616,0,1],8216:[0,.69444,.07816,0,.27778],8217:[0,.69444,.07816,0,.27778],8220:[0,.69444,.14205,0,.5],8221:[0,.69444,.00316,0,.5]},"SansSerif-Regular":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.31945],34:[0,.69444,0,0,.5],35:[.19444,.69444,0,0,.83334],36:[.05556,.75,0,0,.5],37:[.05556,.75,0,0,.83334],38:[0,.69444,0,0,.75834],39:[0,.69444,0,0,.27778],40:[.25,.75,0,0,.38889],41:[.25,.75,0,0,.38889],42:[0,.75,0,0,.5],43:[.08333,.58333,0,0,.77778],44:[.125,.08333,0,0,.27778],45:[0,.44444,0,0,.33333],46:[0,.08333,0,0,.27778],47:[.25,.75,0,0,.5],48:[0,.65556,0,0,.5],49:[0,.65556,0,0,.5],50:[0,.65556,0,0,.5],51:[0,.65556,0,0,.5],52:[0,.65556,0,0,.5],53:[0,.65556,0,0,.5],54:[0,.65556,0,0,.5],55:[0,.65556,0,0,.5],56:[0,.65556,0,0,.5],57:[0,.65556,0,0,.5],58:[0,.44444,0,0,.27778],59:[.125,.44444,0,0,.27778],61:[-.13,.37,0,0,.77778],63:[0,.69444,0,0,.47222],64:[0,.69444,0,0,.66667],65:[0,.69444,0,0,.66667],66:[0,.69444,0,0,.66667],67:[0,.69444,0,0,.63889],68:[0,.69444,0,0,.72223],69:[0,.69444,0,0,.59722],70:[0,.69444,0,0,.56945],71:[0,.69444,0,0,.66667],72:[0,.69444,0,0,.70834],73:[0,.69444,0,0,.27778],74:[0,.69444,0,0,.47222],75:[0,.69444,0,0,.69445],76:[0,.69444,0,0,.54167],77:[0,.69444,0,0,.875],78:[0,.69444,0,0,.70834],79:[0,.69444,0,0,.73611],80:[0,.69444,0,0,.63889],81:[.125,.69444,0,0,.73611],82:[0,.69444,0,0,.64584],83:[0,.69444,0,0,.55556],84:[0,.69444,0,0,.68056],85:[0,.69444,0,0,.6875],86:[0,.69444,.01389,0,.66667],87:[0,.69444,.01389,0,.94445],88:[0,.69444,0,0,.66667],89:[0,.69444,.025,0,.66667],90:[0,.69444,0,0,.61111],91:[.25,.75,0,0,.28889],93:[.25,.75,0,0,.28889],94:[0,.69444,0,0,.5],95:[.35,.09444,.02778,0,.5],97:[0,.44444,0,0,.48056],98:[0,.69444,0,0,.51667],99:[0,.44444,0,0,.44445],100:[0,.69444,0,0,.51667],101:[0,.44444,0,0,.44445],102:[0,.69444,.06944,0,.30556],103:[.19444,.44444,.01389,0,.5],104:[0,.69444,0,0,.51667],105:[0,.67937,0,0,.23889],106:[.19444,.67937,0,0,.26667],107:[0,.69444,0,0,.48889],108:[0,.69444,0,0,.23889],109:[0,.44444,0,0,.79445],110:[0,.44444,0,0,.51667],111:[0,.44444,0,0,.5],112:[.19444,.44444,0,0,.51667],113:[.19444,.44444,0,0,.51667],114:[0,.44444,.01389,0,.34167],115:[0,.44444,0,0,.38333],116:[0,.57143,0,0,.36111],117:[0,.44444,0,0,.51667],118:[0,.44444,.01389,0,.46111],119:[0,.44444,.01389,0,.68334],120:[0,.44444,0,0,.46111],121:[.19444,.44444,.01389,0,.46111],122:[0,.44444,0,0,.43472],126:[.35,.32659,0,0,.5],160:[0,0,0,0,.25],168:[0,.67937,0,0,.5],176:[0,.69444,0,0,.66667],184:[.17014,0,0,0,.44445],305:[0,.44444,0,0,.23889],567:[.19444,.44444,0,0,.26667],710:[0,.69444,0,0,.5],711:[0,.63194,0,0,.5],713:[0,.60889,0,0,.5],714:[0,.69444,0,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,0,0,.5],729:[0,.67937,0,0,.27778],730:[0,.69444,0,0,.66667],732:[0,.67659,0,0,.5],733:[0,.69444,0,0,.5],915:[0,.69444,0,0,.54167],916:[0,.69444,0,0,.83334],920:[0,.69444,0,0,.77778],923:[0,.69444,0,0,.61111],926:[0,.69444,0,0,.66667],928:[0,.69444,0,0,.70834],931:[0,.69444,0,0,.72222],933:[0,.69444,0,0,.77778],934:[0,.69444,0,0,.72222],936:[0,.69444,0,0,.77778],937:[0,.69444,0,0,.72222],8211:[0,.44444,.02778,0,.5],8212:[0,.44444,.02778,0,1],8216:[0,.69444,0,0,.27778],8217:[0,.69444,0,0,.27778],8220:[0,.69444,0,0,.5],8221:[0,.69444,0,0,.5]},"Script-Regular":{32:[0,0,0,0,.25],65:[0,.7,.22925,0,.80253],66:[0,.7,.04087,0,.90757],67:[0,.7,.1689,0,.66619],68:[0,.7,.09371,0,.77443],69:[0,.7,.18583,0,.56162],70:[0,.7,.13634,0,.89544],71:[0,.7,.17322,0,.60961],72:[0,.7,.29694,0,.96919],73:[0,.7,.19189,0,.80907],74:[.27778,.7,.19189,0,1.05159],75:[0,.7,.31259,0,.91364],76:[0,.7,.19189,0,.87373],77:[0,.7,.15981,0,1.08031],78:[0,.7,.3525,0,.9015],79:[0,.7,.08078,0,.73787],80:[0,.7,.08078,0,1.01262],81:[0,.7,.03305,0,.88282],82:[0,.7,.06259,0,.85],83:[0,.7,.19189,0,.86767],84:[0,.7,.29087,0,.74697],85:[0,.7,.25815,0,.79996],86:[0,.7,.27523,0,.62204],87:[0,.7,.27523,0,.80532],88:[0,.7,.26006,0,.94445],89:[0,.7,.2939,0,.70961],90:[0,.7,.24037,0,.8212],160:[0,0,0,0,.25]},"Size1-Regular":{32:[0,0,0,0,.25],40:[.35001,.85,0,0,.45834],41:[.35001,.85,0,0,.45834],47:[.35001,.85,0,0,.57778],91:[.35001,.85,0,0,.41667],92:[.35001,.85,0,0,.57778],93:[.35001,.85,0,0,.41667],123:[.35001,.85,0,0,.58334],125:[.35001,.85,0,0,.58334],160:[0,0,0,0,.25],710:[0,.72222,0,0,.55556],732:[0,.72222,0,0,.55556],770:[0,.72222,0,0,.55556],771:[0,.72222,0,0,.55556],8214:[-99e-5,.601,0,0,.77778],8593:[1e-5,.6,0,0,.66667],8595:[1e-5,.6,0,0,.66667],8657:[1e-5,.6,0,0,.77778],8659:[1e-5,.6,0,0,.77778],8719:[.25001,.75,0,0,.94445],8720:[.25001,.75,0,0,.94445],8721:[.25001,.75,0,0,1.05556],8730:[.35001,.85,0,0,1],8739:[-.00599,.606,0,0,.33333],8741:[-.00599,.606,0,0,.55556],8747:[.30612,.805,.19445,0,.47222],8748:[.306,.805,.19445,0,.47222],8749:[.306,.805,.19445,0,.47222],8750:[.30612,.805,.19445,0,.47222],8896:[.25001,.75,0,0,.83334],8897:[.25001,.75,0,0,.83334],8898:[.25001,.75,0,0,.83334],8899:[.25001,.75,0,0,.83334],8968:[.35001,.85,0,0,.47222],8969:[.35001,.85,0,0,.47222],8970:[.35001,.85,0,0,.47222],8971:[.35001,.85,0,0,.47222],9168:[-99e-5,.601,0,0,.66667],10216:[.35001,.85,0,0,.47222],10217:[.35001,.85,0,0,.47222],10752:[.25001,.75,0,0,1.11111],10753:[.25001,.75,0,0,1.11111],10754:[.25001,.75,0,0,1.11111],10756:[.25001,.75,0,0,.83334],10758:[.25001,.75,0,0,.83334]},"Size2-Regular":{32:[0,0,0,0,.25],40:[.65002,1.15,0,0,.59722],41:[.65002,1.15,0,0,.59722],47:[.65002,1.15,0,0,.81111],91:[.65002,1.15,0,0,.47222],92:[.65002,1.15,0,0,.81111],93:[.65002,1.15,0,0,.47222],123:[.65002,1.15,0,0,.66667],125:[.65002,1.15,0,0,.66667],160:[0,0,0,0,.25],710:[0,.75,0,0,1],732:[0,.75,0,0,1],770:[0,.75,0,0,1],771:[0,.75,0,0,1],8719:[.55001,1.05,0,0,1.27778],8720:[.55001,1.05,0,0,1.27778],8721:[.55001,1.05,0,0,1.44445],8730:[.65002,1.15,0,0,1],8747:[.86225,1.36,.44445,0,.55556],8748:[.862,1.36,.44445,0,.55556],8749:[.862,1.36,.44445,0,.55556],8750:[.86225,1.36,.44445,0,.55556],8896:[.55001,1.05,0,0,1.11111],8897:[.55001,1.05,0,0,1.11111],8898:[.55001,1.05,0,0,1.11111],8899:[.55001,1.05,0,0,1.11111],8968:[.65002,1.15,0,0,.52778],8969:[.65002,1.15,0,0,.52778],8970:[.65002,1.15,0,0,.52778],8971:[.65002,1.15,0,0,.52778],10216:[.65002,1.15,0,0,.61111],10217:[.65002,1.15,0,0,.61111],10752:[.55001,1.05,0,0,1.51112],10753:[.55001,1.05,0,0,1.51112],10754:[.55001,1.05,0,0,1.51112],10756:[.55001,1.05,0,0,1.11111],10758:[.55001,1.05,0,0,1.11111]},"Size3-Regular":{32:[0,0,0,0,.25],40:[.95003,1.45,0,0,.73611],41:[.95003,1.45,0,0,.73611],47:[.95003,1.45,0,0,1.04445],91:[.95003,1.45,0,0,.52778],92:[.95003,1.45,0,0,1.04445],93:[.95003,1.45,0,0,.52778],123:[.95003,1.45,0,0,.75],125:[.95003,1.45,0,0,.75],160:[0,0,0,0,.25],710:[0,.75,0,0,1.44445],732:[0,.75,0,0,1.44445],770:[0,.75,0,0,1.44445],771:[0,.75,0,0,1.44445],8730:[.95003,1.45,0,0,1],8968:[.95003,1.45,0,0,.58334],8969:[.95003,1.45,0,0,.58334],8970:[.95003,1.45,0,0,.58334],8971:[.95003,1.45,0,0,.58334],10216:[.95003,1.45,0,0,.75],10217:[.95003,1.45,0,0,.75]},"Size4-Regular":{32:[0,0,0,0,.25],40:[1.25003,1.75,0,0,.79167],41:[1.25003,1.75,0,0,.79167],47:[1.25003,1.75,0,0,1.27778],91:[1.25003,1.75,0,0,.58334],92:[1.25003,1.75,0,0,1.27778],93:[1.25003,1.75,0,0,.58334],123:[1.25003,1.75,0,0,.80556],125:[1.25003,1.75,0,0,.80556],160:[0,0,0,0,.25],710:[0,.825,0,0,1.8889],732:[0,.825,0,0,1.8889],770:[0,.825,0,0,1.8889],771:[0,.825,0,0,1.8889],8730:[1.25003,1.75,0,0,1],8968:[1.25003,1.75,0,0,.63889],8969:[1.25003,1.75,0,0,.63889],8970:[1.25003,1.75,0,0,.63889],8971:[1.25003,1.75,0,0,.63889],9115:[.64502,1.155,0,0,.875],9116:[1e-5,.6,0,0,.875],9117:[.64502,1.155,0,0,.875],9118:[.64502,1.155,0,0,.875],9119:[1e-5,.6,0,0,.875],9120:[.64502,1.155,0,0,.875],9121:[.64502,1.155,0,0,.66667],9122:[-99e-5,.601,0,0,.66667],9123:[.64502,1.155,0,0,.66667],9124:[.64502,1.155,0,0,.66667],9125:[-99e-5,.601,0,0,.66667],9126:[.64502,1.155,0,0,.66667],9127:[1e-5,.9,0,0,.88889],9128:[.65002,1.15,0,0,.88889],9129:[.90001,0,0,0,.88889],9130:[0,.3,0,0,.88889],9131:[1e-5,.9,0,0,.88889],9132:[.65002,1.15,0,0,.88889],9133:[.90001,0,0,0,.88889],9143:[.88502,.915,0,0,1.05556],10216:[1.25003,1.75,0,0,.80556],10217:[1.25003,1.75,0,0,.80556],57344:[-.00499,.605,0,0,1.05556],57345:[-.00499,.605,0,0,1.05556],57680:[0,.12,0,0,.45],57681:[0,.12,0,0,.45],57682:[0,.12,0,0,.45],57683:[0,.12,0,0,.45]},"Typewriter-Regular":{32:[0,0,0,0,.525],33:[0,.61111,0,0,.525],34:[0,.61111,0,0,.525],35:[0,.61111,0,0,.525],36:[.08333,.69444,0,0,.525],37:[.08333,.69444,0,0,.525],38:[0,.61111,0,0,.525],39:[0,.61111,0,0,.525],40:[.08333,.69444,0,0,.525],41:[.08333,.69444,0,0,.525],42:[0,.52083,0,0,.525],43:[-.08056,.53055,0,0,.525],44:[.13889,.125,0,0,.525],45:[-.08056,.53055,0,0,.525],46:[0,.125,0,0,.525],47:[.08333,.69444,0,0,.525],48:[0,.61111,0,0,.525],49:[0,.61111,0,0,.525],50:[0,.61111,0,0,.525],51:[0,.61111,0,0,.525],52:[0,.61111,0,0,.525],53:[0,.61111,0,0,.525],54:[0,.61111,0,0,.525],55:[0,.61111,0,0,.525],56:[0,.61111,0,0,.525],57:[0,.61111,0,0,.525],58:[0,.43056,0,0,.525],59:[.13889,.43056,0,0,.525],60:[-.05556,.55556,0,0,.525],61:[-.19549,.41562,0,0,.525],62:[-.05556,.55556,0,0,.525],63:[0,.61111,0,0,.525],64:[0,.61111,0,0,.525],65:[0,.61111,0,0,.525],66:[0,.61111,0,0,.525],67:[0,.61111,0,0,.525],68:[0,.61111,0,0,.525],69:[0,.61111,0,0,.525],70:[0,.61111,0,0,.525],71:[0,.61111,0,0,.525],72:[0,.61111,0,0,.525],73:[0,.61111,0,0,.525],74:[0,.61111,0,0,.525],75:[0,.61111,0,0,.525],76:[0,.61111,0,0,.525],77:[0,.61111,0,0,.525],78:[0,.61111,0,0,.525],79:[0,.61111,0,0,.525],80:[0,.61111,0,0,.525],81:[.13889,.61111,0,0,.525],82:[0,.61111,0,0,.525],83:[0,.61111,0,0,.525],84:[0,.61111,0,0,.525],85:[0,.61111,0,0,.525],86:[0,.61111,0,0,.525],87:[0,.61111,0,0,.525],88:[0,.61111,0,0,.525],89:[0,.61111,0,0,.525],90:[0,.61111,0,0,.525],91:[.08333,.69444,0,0,.525],92:[.08333,.69444,0,0,.525],93:[.08333,.69444,0,0,.525],94:[0,.61111,0,0,.525],95:[.09514,0,0,0,.525],96:[0,.61111,0,0,.525],97:[0,.43056,0,0,.525],98:[0,.61111,0,0,.525],99:[0,.43056,0,0,.525],100:[0,.61111,0,0,.525],101:[0,.43056,0,0,.525],102:[0,.61111,0,0,.525],103:[.22222,.43056,0,0,.525],104:[0,.61111,0,0,.525],105:[0,.61111,0,0,.525],106:[.22222,.61111,0,0,.525],107:[0,.61111,0,0,.525],108:[0,.61111,0,0,.525],109:[0,.43056,0,0,.525],110:[0,.43056,0,0,.525],111:[0,.43056,0,0,.525],112:[.22222,.43056,0,0,.525],113:[.22222,.43056,0,0,.525],114:[0,.43056,0,0,.525],115:[0,.43056,0,0,.525],116:[0,.55358,0,0,.525],117:[0,.43056,0,0,.525],118:[0,.43056,0,0,.525],119:[0,.43056,0,0,.525],120:[0,.43056,0,0,.525],121:[.22222,.43056,0,0,.525],122:[0,.43056,0,0,.525],123:[.08333,.69444,0,0,.525],124:[.08333,.69444,0,0,.525],125:[.08333,.69444,0,0,.525],126:[0,.61111,0,0,.525],127:[0,.61111,0,0,.525],160:[0,0,0,0,.525],176:[0,.61111,0,0,.525],184:[.19445,0,0,0,.525],305:[0,.43056,0,0,.525],567:[.22222,.43056,0,0,.525],711:[0,.56597,0,0,.525],713:[0,.56555,0,0,.525],714:[0,.61111,0,0,.525],715:[0,.61111,0,0,.525],728:[0,.61111,0,0,.525],730:[0,.61111,0,0,.525],770:[0,.61111,0,0,.525],771:[0,.61111,0,0,.525],776:[0,.61111,0,0,.525],915:[0,.61111,0,0,.525],916:[0,.61111,0,0,.525],920:[0,.61111,0,0,.525],923:[0,.61111,0,0,.525],926:[0,.61111,0,0,.525],928:[0,.61111,0,0,.525],931:[0,.61111,0,0,.525],933:[0,.61111,0,0,.525],934:[0,.61111,0,0,.525],936:[0,.61111,0,0,.525],937:[0,.61111,0,0,.525],8216:[0,.61111,0,0,.525],8217:[0,.61111,0,0,.525],8242:[0,.61111,0,0,.525],9251:[.11111,.21944,0,0,.525]}},Rw={slant:[.25,.25,.25],space:[0,0,0],stretch:[0,0,0],shrink:[0,0,0],xHeight:[.431,.431,.431],quad:[1,1.171,1.472],extraSpace:[0,0,0],num1:[.677,.732,.925],num2:[.394,.384,.387],num3:[.444,.471,.504],denom1:[.686,.752,1.025],denom2:[.345,.344,.532],sup1:[.413,.503,.504],sup2:[.363,.431,.404],sup3:[.289,.286,.294],sub1:[.15,.143,.2],sub2:[.247,.286,.4],supDrop:[.386,.353,.494],subDrop:[.05,.071,.1],delim1:[2.39,1.7,1.98],delim2:[1.01,1.157,1.42],axisHeight:[.25,.25,.25],defaultRuleThickness:[.04,.049,.049],bigOpSpacing1:[.111,.111,.111],bigOpSpacing2:[.166,.166,.166],bigOpSpacing3:[.2,.2,.2],bigOpSpacing4:[.6,.611,.611],bigOpSpacing5:[.1,.143,.143],sqrtRuleThickness:[.04,.04,.04],ptPerEm:[10,10,10],doubleRuleSep:[.2,.2,.2],arrayRuleWidth:[.04,.04,.04],fboxsep:[.3,.3,.3],fboxrule:[.04,.04,.04]},EZ={\u00C5:"A",\u00D0:"D",\u00DE:"o",\u00E5:"a",\u00F0:"d",\u00FE:"o",\u0410:"A",\u0411:"B",\u0412:"B",\u0413:"F",\u0414:"A",\u0415:"E",\u0416:"K",\u0417:"3",\u0418:"N",\u0419:"N",\u041A:"K",\u041B:"N",\u041C:"M",\u041D:"H",\u041E:"O",\u041F:"N",\u0420:"P",\u0421:"C",\u0422:"T",\u0423:"y",\u0424:"O",\u0425:"X",\u0426:"U",\u0427:"h",\u0428:"W",\u0429:"W",\u042A:"B",\u042B:"X",\u042C:"B",\u042D:"3",\u042E:"X",\u042F:"R",\u0430:"a",\u0431:"b",\u0432:"a",\u0433:"r",\u0434:"y",\u0435:"e",\u0436:"m",\u0437:"e",\u0438:"n",\u0439:"n",\u043A:"n",\u043B:"n",\u043C:"m",\u043D:"n",\u043E:"o",\u043F:"n",\u0440:"p",\u0441:"c",\u0442:"o",\u0443:"y",\u0444:"b",\u0445:"x",\u0446:"n",\u0447:"n",\u0448:"w",\u0449:"w",\u044A:"a",\u044B:"m",\u044C:"a",\u044D:"e",\u044E:"m",\u044F:"r"};s(hQ,"setFontMetrics");s(D7,"getCharacterMetrics");XD={};s(vVe,"getGlobalMetrics");pi={math:{},text:{}};s(Q,"defineSymbol");ae="math",Dt="text",xe="main",Ve="ams",mi="accent-token",Qt="bin",Ms="close",Z0="inner",wr="mathord",ta="op-token",$o="open",Qx="punct",We="rel",oh="spacing",Ue="textord";Q(ae,xe,We,"\u2261","\\equiv",!0);Q(ae,xe,We,"\u227A","\\prec",!0);Q(ae,xe,We,"\u227B","\\succ",!0);Q(ae,xe,We,"\u223C","\\sim",!0);Q(ae,xe,We,"\u22A5","\\perp");Q(ae,xe,We,"\u2AAF","\\preceq",!0);Q(ae,xe,We,"\u2AB0","\\succeq",!0);Q(ae,xe,We,"\u2243","\\simeq",!0);Q(ae,xe,We,"\u2223","\\mid",!0);Q(ae,xe,We,"\u226A","\\ll",!0);Q(ae,xe,We,"\u226B","\\gg",!0);Q(ae,xe,We,"\u224D","\\asymp",!0);Q(ae,xe,We,"\u2225","\\parallel");Q(ae,xe,We,"\u22C8","\\bowtie",!0);Q(ae,xe,We,"\u2323","\\smile",!0);Q(ae,xe,We,"\u2291","\\sqsubseteq",!0);Q(ae,xe,We,"\u2292","\\sqsupseteq",!0);Q(ae,xe,We,"\u2250","\\doteq",!0);Q(ae,xe,We,"\u2322","\\frown",!0);Q(ae,xe,We,"\u220B","\\ni",!0);Q(ae,xe,We,"\u221D","\\propto",!0);Q(ae,xe,We,"\u22A2","\\vdash",!0);Q(ae,xe,We,"\u22A3","\\dashv",!0);Q(ae,xe,We,"\u220B","\\owns");Q(ae,xe,Qx,".","\\ldotp");Q(ae,xe,Qx,"\u22C5","\\cdotp");Q(ae,xe,Qx,"\u22C5","\xB7");Q(Dt,xe,Ue,"\u22C5","\xB7");Q(ae,xe,Ue,"#","\\#");Q(Dt,xe,Ue,"#","\\#");Q(ae,xe,Ue,"&","\\&");Q(Dt,xe,Ue,"&","\\&");Q(ae,xe,Ue,"\u2135","\\aleph",!0);Q(ae,xe,Ue,"\u2200","\\forall",!0);Q(ae,xe,Ue,"\u210F","\\hbar",!0);Q(ae,xe,Ue,"\u2203","\\exists",!0);Q(ae,xe,Ue,"\u2207","\\nabla",!0);Q(ae,xe,Ue,"\u266D","\\flat",!0);Q(ae,xe,Ue,"\u2113","\\ell",!0);Q(ae,xe,Ue,"\u266E","\\natural",!0);Q(ae,xe,Ue,"\u2663","\\clubsuit",!0);Q(ae,xe,Ue,"\u2118","\\wp",!0);Q(ae,xe,Ue,"\u266F","\\sharp",!0);Q(ae,xe,Ue,"\u2662","\\diamondsuit",!0);Q(ae,xe,Ue,"\u211C","\\Re",!0);Q(ae,xe,Ue,"\u2661","\\heartsuit",!0);Q(ae,xe,Ue,"\u2111","\\Im",!0);Q(ae,xe,Ue,"\u2660","\\spadesuit",!0);Q(ae,xe,Ue,"\xA7","\\S",!0);Q(Dt,xe,Ue,"\xA7","\\S");Q(ae,xe,Ue,"\xB6","\\P",!0);Q(Dt,xe,Ue,"\xB6","\\P");Q(ae,xe,Ue,"\u2020","\\dag");Q(Dt,xe,Ue,"\u2020","\\dag");Q(Dt,xe,Ue,"\u2020","\\textdagger");Q(ae,xe,Ue,"\u2021","\\ddag");Q(Dt,xe,Ue,"\u2021","\\ddag");Q(Dt,xe,Ue,"\u2021","\\textdaggerdbl");Q(ae,xe,Ms,"\u23B1","\\rmoustache",!0);Q(ae,xe,$o,"\u23B0","\\lmoustache",!0);Q(ae,xe,Ms,"\u27EF","\\rgroup",!0);Q(ae,xe,$o,"\u27EE","\\lgroup",!0);Q(ae,xe,Qt,"\u2213","\\mp",!0);Q(ae,xe,Qt,"\u2296","\\ominus",!0);Q(ae,xe,Qt,"\u228E","\\uplus",!0);Q(ae,xe,Qt,"\u2293","\\sqcap",!0);Q(ae,xe,Qt,"\u2217","\\ast");Q(ae,xe,Qt,"\u2294","\\sqcup",!0);Q(ae,xe,Qt,"\u25EF","\\bigcirc",!0);Q(ae,xe,Qt,"\u2219","\\bullet",!0);Q(ae,xe,Qt,"\u2021","\\ddagger");Q(ae,xe,Qt,"\u2240","\\wr",!0);Q(ae,xe,Qt,"\u2A3F","\\amalg");Q(ae,xe,Qt,"&","\\And");Q(ae,xe,We,"\u27F5","\\longleftarrow",!0);Q(ae,xe,We,"\u21D0","\\Leftarrow",!0);Q(ae,xe,We,"\u27F8","\\Longleftarrow",!0);Q(ae,xe,We,"\u27F6","\\longrightarrow",!0);Q(ae,xe,We,"\u21D2","\\Rightarrow",!0);Q(ae,xe,We,"\u27F9","\\Longrightarrow",!0);Q(ae,xe,We,"\u2194","\\leftrightarrow",!0);Q(ae,xe,We,"\u27F7","\\longleftrightarrow",!0);Q(ae,xe,We,"\u21D4","\\Leftrightarrow",!0);Q(ae,xe,We,"\u27FA","\\Longleftrightarrow",!0);Q(ae,xe,We,"\u21A6","\\mapsto",!0);Q(ae,xe,We,"\u27FC","\\longmapsto",!0);Q(ae,xe,We,"\u2197","\\nearrow",!0);Q(ae,xe,We,"\u21A9","\\hookleftarrow",!0);Q(ae,xe,We,"\u21AA","\\hookrightarrow",!0);Q(ae,xe,We,"\u2198","\\searrow",!0);Q(ae,xe,We,"\u21BC","\\leftharpoonup",!0);Q(ae,xe,We,"\u21C0","\\rightharpoonup",!0);Q(ae,xe,We,"\u2199","\\swarrow",!0);Q(ae,xe,We,"\u21BD","\\leftharpoondown",!0);Q(ae,xe,We,"\u21C1","\\rightharpoondown",!0);Q(ae,xe,We,"\u2196","\\nwarrow",!0);Q(ae,xe,We,"\u21CC","\\rightleftharpoons",!0);Q(ae,Ve,We,"\u226E","\\nless",!0);Q(ae,Ve,We,"\uE010","\\@nleqslant");Q(ae,Ve,We,"\uE011","\\@nleqq");Q(ae,Ve,We,"\u2A87","\\lneq",!0);Q(ae,Ve,We,"\u2268","\\lneqq",!0);Q(ae,Ve,We,"\uE00C","\\@lvertneqq");Q(ae,Ve,We,"\u22E6","\\lnsim",!0);Q(ae,Ve,We,"\u2A89","\\lnapprox",!0);Q(ae,Ve,We,"\u2280","\\nprec",!0);Q(ae,Ve,We,"\u22E0","\\npreceq",!0);Q(ae,Ve,We,"\u22E8","\\precnsim",!0);Q(ae,Ve,We,"\u2AB9","\\precnapprox",!0);Q(ae,Ve,We,"\u2241","\\nsim",!0);Q(ae,Ve,We,"\uE006","\\@nshortmid");Q(ae,Ve,We,"\u2224","\\nmid",!0);Q(ae,Ve,We,"\u22AC","\\nvdash",!0);Q(ae,Ve,We,"\u22AD","\\nvDash",!0);Q(ae,Ve,We,"\u22EA","\\ntriangleleft");Q(ae,Ve,We,"\u22EC","\\ntrianglelefteq",!0);Q(ae,Ve,We,"\u228A","\\subsetneq",!0);Q(ae,Ve,We,"\uE01A","\\@varsubsetneq");Q(ae,Ve,We,"\u2ACB","\\subsetneqq",!0);Q(ae,Ve,We,"\uE017","\\@varsubsetneqq");Q(ae,Ve,We,"\u226F","\\ngtr",!0);Q(ae,Ve,We,"\uE00F","\\@ngeqslant");Q(ae,Ve,We,"\uE00E","\\@ngeqq");Q(ae,Ve,We,"\u2A88","\\gneq",!0);Q(ae,Ve,We,"\u2269","\\gneqq",!0);Q(ae,Ve,We,"\uE00D","\\@gvertneqq");Q(ae,Ve,We,"\u22E7","\\gnsim",!0);Q(ae,Ve,We,"\u2A8A","\\gnapprox",!0);Q(ae,Ve,We,"\u2281","\\nsucc",!0);Q(ae,Ve,We,"\u22E1","\\nsucceq",!0);Q(ae,Ve,We,"\u22E9","\\succnsim",!0);Q(ae,Ve,We,"\u2ABA","\\succnapprox",!0);Q(ae,Ve,We,"\u2246","\\ncong",!0);Q(ae,Ve,We,"\uE007","\\@nshortparallel");Q(ae,Ve,We,"\u2226","\\nparallel",!0);Q(ae,Ve,We,"\u22AF","\\nVDash",!0);Q(ae,Ve,We,"\u22EB","\\ntriangleright");Q(ae,Ve,We,"\u22ED","\\ntrianglerighteq",!0);Q(ae,Ve,We,"\uE018","\\@nsupseteqq");Q(ae,Ve,We,"\u228B","\\supsetneq",!0);Q(ae,Ve,We,"\uE01B","\\@varsupsetneq");Q(ae,Ve,We,"\u2ACC","\\supsetneqq",!0);Q(ae,Ve,We,"\uE019","\\@varsupsetneqq");Q(ae,Ve,We,"\u22AE","\\nVdash",!0);Q(ae,Ve,We,"\u2AB5","\\precneqq",!0);Q(ae,Ve,We,"\u2AB6","\\succneqq",!0);Q(ae,Ve,We,"\uE016","\\@nsubseteqq");Q(ae,Ve,Qt,"\u22B4","\\unlhd");Q(ae,Ve,Qt,"\u22B5","\\unrhd");Q(ae,Ve,We,"\u219A","\\nleftarrow",!0);Q(ae,Ve,We,"\u219B","\\nrightarrow",!0);Q(ae,Ve,We,"\u21CD","\\nLeftarrow",!0);Q(ae,Ve,We,"\u21CF","\\nRightarrow",!0);Q(ae,Ve,We,"\u21AE","\\nleftrightarrow",!0);Q(ae,Ve,We,"\u21CE","\\nLeftrightarrow",!0);Q(ae,Ve,We,"\u25B3","\\vartriangle");Q(ae,Ve,Ue,"\u210F","\\hslash");Q(ae,Ve,Ue,"\u25BD","\\triangledown");Q(ae,Ve,Ue,"\u25CA","\\lozenge");Q(ae,Ve,Ue,"\u24C8","\\circledS");Q(ae,Ve,Ue,"\xAE","\\circledR");Q(Dt,Ve,Ue,"\xAE","\\circledR");Q(ae,Ve,Ue,"\u2221","\\measuredangle",!0);Q(ae,Ve,Ue,"\u2204","\\nexists");Q(ae,Ve,Ue,"\u2127","\\mho");Q(ae,Ve,Ue,"\u2132","\\Finv",!0);Q(ae,Ve,Ue,"\u2141","\\Game",!0);Q(ae,Ve,Ue,"\u2035","\\backprime");Q(ae,Ve,Ue,"\u25B2","\\blacktriangle");Q(ae,Ve,Ue,"\u25BC","\\blacktriangledown");Q(ae,Ve,Ue,"\u25A0","\\blacksquare");Q(ae,Ve,Ue,"\u29EB","\\blacklozenge");Q(ae,Ve,Ue,"\u2605","\\bigstar");Q(ae,Ve,Ue,"\u2222","\\sphericalangle",!0);Q(ae,Ve,Ue,"\u2201","\\complement",!0);Q(ae,Ve,Ue,"\xF0","\\eth",!0);Q(Dt,xe,Ue,"\xF0","\xF0");Q(ae,Ve,Ue,"\u2571","\\diagup");Q(ae,Ve,Ue,"\u2572","\\diagdown");Q(ae,Ve,Ue,"\u25A1","\\square");Q(ae,Ve,Ue,"\u25A1","\\Box");Q(ae,Ve,Ue,"\u25CA","\\Diamond");Q(ae,Ve,Ue,"\xA5","\\yen",!0);Q(Dt,Ve,Ue,"\xA5","\\yen",!0);Q(ae,Ve,Ue,"\u2713","\\checkmark",!0);Q(Dt,Ve,Ue,"\u2713","\\checkmark");Q(ae,Ve,Ue,"\u2136","\\beth",!0);Q(ae,Ve,Ue,"\u2138","\\daleth",!0);Q(ae,Ve,Ue,"\u2137","\\gimel",!0);Q(ae,Ve,Ue,"\u03DD","\\digamma",!0);Q(ae,Ve,Ue,"\u03F0","\\varkappa");Q(ae,Ve,$o,"\u250C","\\@ulcorner",!0);Q(ae,Ve,Ms,"\u2510","\\@urcorner",!0);Q(ae,Ve,$o,"\u2514","\\@llcorner",!0);Q(ae,Ve,Ms,"\u2518","\\@lrcorner",!0);Q(ae,Ve,We,"\u2266","\\leqq",!0);Q(ae,Ve,We,"\u2A7D","\\leqslant",!0);Q(ae,Ve,We,"\u2A95","\\eqslantless",!0);Q(ae,Ve,We,"\u2272","\\lesssim",!0);Q(ae,Ve,We,"\u2A85","\\lessapprox",!0);Q(ae,Ve,We,"\u224A","\\approxeq",!0);Q(ae,Ve,Qt,"\u22D6","\\lessdot");Q(ae,Ve,We,"\u22D8","\\lll",!0);Q(ae,Ve,We,"\u2276","\\lessgtr",!0);Q(ae,Ve,We,"\u22DA","\\lesseqgtr",!0);Q(ae,Ve,We,"\u2A8B","\\lesseqqgtr",!0);Q(ae,Ve,We,"\u2251","\\doteqdot");Q(ae,Ve,We,"\u2253","\\risingdotseq",!0);Q(ae,Ve,We,"\u2252","\\fallingdotseq",!0);Q(ae,Ve,We,"\u223D","\\backsim",!0);Q(ae,Ve,We,"\u22CD","\\backsimeq",!0);Q(ae,Ve,We,"\u2AC5","\\subseteqq",!0);Q(ae,Ve,We,"\u22D0","\\Subset",!0);Q(ae,Ve,We,"\u228F","\\sqsubset",!0);Q(ae,Ve,We,"\u227C","\\preccurlyeq",!0);Q(ae,Ve,We,"\u22DE","\\curlyeqprec",!0);Q(ae,Ve,We,"\u227E","\\precsim",!0);Q(ae,Ve,We,"\u2AB7","\\precapprox",!0);Q(ae,Ve,We,"\u22B2","\\vartriangleleft");Q(ae,Ve,We,"\u22B4","\\trianglelefteq");Q(ae,Ve,We,"\u22A8","\\vDash",!0);Q(ae,Ve,We,"\u22AA","\\Vvdash",!0);Q(ae,Ve,We,"\u2323","\\smallsmile");Q(ae,Ve,We,"\u2322","\\smallfrown");Q(ae,Ve,We,"\u224F","\\bumpeq",!0);Q(ae,Ve,We,"\u224E","\\Bumpeq",!0);Q(ae,Ve,We,"\u2267","\\geqq",!0);Q(ae,Ve,We,"\u2A7E","\\geqslant",!0);Q(ae,Ve,We,"\u2A96","\\eqslantgtr",!0);Q(ae,Ve,We,"\u2273","\\gtrsim",!0);Q(ae,Ve,We,"\u2A86","\\gtrapprox",!0);Q(ae,Ve,Qt,"\u22D7","\\gtrdot");Q(ae,Ve,We,"\u22D9","\\ggg",!0);Q(ae,Ve,We,"\u2277","\\gtrless",!0);Q(ae,Ve,We,"\u22DB","\\gtreqless",!0);Q(ae,Ve,We,"\u2A8C","\\gtreqqless",!0);Q(ae,Ve,We,"\u2256","\\eqcirc",!0);Q(ae,Ve,We,"\u2257","\\circeq",!0);Q(ae,Ve,We,"\u225C","\\triangleq",!0);Q(ae,Ve,We,"\u223C","\\thicksim");Q(ae,Ve,We,"\u2248","\\thickapprox");Q(ae,Ve,We,"\u2AC6","\\supseteqq",!0);Q(ae,Ve,We,"\u22D1","\\Supset",!0);Q(ae,Ve,We,"\u2290","\\sqsupset",!0);Q(ae,Ve,We,"\u227D","\\succcurlyeq",!0);Q(ae,Ve,We,"\u22DF","\\curlyeqsucc",!0);Q(ae,Ve,We,"\u227F","\\succsim",!0);Q(ae,Ve,We,"\u2AB8","\\succapprox",!0);Q(ae,Ve,We,"\u22B3","\\vartriangleright");Q(ae,Ve,We,"\u22B5","\\trianglerighteq");Q(ae,Ve,We,"\u22A9","\\Vdash",!0);Q(ae,Ve,We,"\u2223","\\shortmid");Q(ae,Ve,We,"\u2225","\\shortparallel");Q(ae,Ve,We,"\u226C","\\between",!0);Q(ae,Ve,We,"\u22D4","\\pitchfork",!0);Q(ae,Ve,We,"\u221D","\\varpropto");Q(ae,Ve,We,"\u25C0","\\blacktriangleleft");Q(ae,Ve,We,"\u2234","\\therefore",!0);Q(ae,Ve,We,"\u220D","\\backepsilon");Q(ae,Ve,We,"\u25B6","\\blacktriangleright");Q(ae,Ve,We,"\u2235","\\because",!0);Q(ae,Ve,We,"\u22D8","\\llless");Q(ae,Ve,We,"\u22D9","\\gggtr");Q(ae,Ve,Qt,"\u22B2","\\lhd");Q(ae,Ve,Qt,"\u22B3","\\rhd");Q(ae,Ve,We,"\u2242","\\eqsim",!0);Q(ae,xe,We,"\u22C8","\\Join");Q(ae,Ve,We,"\u2251","\\Doteq",!0);Q(ae,Ve,Qt,"\u2214","\\dotplus",!0);Q(ae,Ve,Qt,"\u2216","\\smallsetminus");Q(ae,Ve,Qt,"\u22D2","\\Cap",!0);Q(ae,Ve,Qt,"\u22D3","\\Cup",!0);Q(ae,Ve,Qt,"\u2A5E","\\doublebarwedge",!0);Q(ae,Ve,Qt,"\u229F","\\boxminus",!0);Q(ae,Ve,Qt,"\u229E","\\boxplus",!0);Q(ae,Ve,Qt,"\u22C7","\\divideontimes",!0);Q(ae,Ve,Qt,"\u22C9","\\ltimes",!0);Q(ae,Ve,Qt,"\u22CA","\\rtimes",!0);Q(ae,Ve,Qt,"\u22CB","\\leftthreetimes",!0);Q(ae,Ve,Qt,"\u22CC","\\rightthreetimes",!0);Q(ae,Ve,Qt,"\u22CF","\\curlywedge",!0);Q(ae,Ve,Qt,"\u22CE","\\curlyvee",!0);Q(ae,Ve,Qt,"\u229D","\\circleddash",!0);Q(ae,Ve,Qt,"\u229B","\\circledast",!0);Q(ae,Ve,Qt,"\u22C5","\\centerdot");Q(ae,Ve,Qt,"\u22BA","\\intercal",!0);Q(ae,Ve,Qt,"\u22D2","\\doublecap");Q(ae,Ve,Qt,"\u22D3","\\doublecup");Q(ae,Ve,Qt,"\u22A0","\\boxtimes",!0);Q(ae,Ve,We,"\u21E2","\\dashrightarrow",!0);Q(ae,Ve,We,"\u21E0","\\dashleftarrow",!0);Q(ae,Ve,We,"\u21C7","\\leftleftarrows",!0);Q(ae,Ve,We,"\u21C6","\\leftrightarrows",!0);Q(ae,Ve,We,"\u21DA","\\Lleftarrow",!0);Q(ae,Ve,We,"\u219E","\\twoheadleftarrow",!0);Q(ae,Ve,We,"\u21A2","\\leftarrowtail",!0);Q(ae,Ve,We,"\u21AB","\\looparrowleft",!0);Q(ae,Ve,We,"\u21CB","\\leftrightharpoons",!0);Q(ae,Ve,We,"\u21B6","\\curvearrowleft",!0);Q(ae,Ve,We,"\u21BA","\\circlearrowleft",!0);Q(ae,Ve,We,"\u21B0","\\Lsh",!0);Q(ae,Ve,We,"\u21C8","\\upuparrows",!0);Q(ae,Ve,We,"\u21BF","\\upharpoonleft",!0);Q(ae,Ve,We,"\u21C3","\\downharpoonleft",!0);Q(ae,xe,We,"\u22B6","\\origof",!0);Q(ae,xe,We,"\u22B7","\\imageof",!0);Q(ae,Ve,We,"\u22B8","\\multimap",!0);Q(ae,Ve,We,"\u21AD","\\leftrightsquigarrow",!0);Q(ae,Ve,We,"\u21C9","\\rightrightarrows",!0);Q(ae,Ve,We,"\u21C4","\\rightleftarrows",!0);Q(ae,Ve,We,"\u21A0","\\twoheadrightarrow",!0);Q(ae,Ve,We,"\u21A3","\\rightarrowtail",!0);Q(ae,Ve,We,"\u21AC","\\looparrowright",!0);Q(ae,Ve,We,"\u21B7","\\curvearrowright",!0);Q(ae,Ve,We,"\u21BB","\\circlearrowright",!0);Q(ae,Ve,We,"\u21B1","\\Rsh",!0);Q(ae,Ve,We,"\u21CA","\\downdownarrows",!0);Q(ae,Ve,We,"\u21BE","\\upharpoonright",!0);Q(ae,Ve,We,"\u21C2","\\downharpoonright",!0);Q(ae,Ve,We,"\u21DD","\\rightsquigarrow",!0);Q(ae,Ve,We,"\u21DD","\\leadsto");Q(ae,Ve,We,"\u21DB","\\Rrightarrow",!0);Q(ae,Ve,We,"\u21BE","\\restriction");Q(ae,xe,Ue,"\u2018","`");Q(ae,xe,Ue,"$","\\$");Q(Dt,xe,Ue,"$","\\$");Q(Dt,xe,Ue,"$","\\textdollar");Q(ae,xe,Ue,"%","\\%");Q(Dt,xe,Ue,"%","\\%");Q(ae,xe,Ue,"_","\\_");Q(Dt,xe,Ue,"_","\\_");Q(Dt,xe,Ue,"_","\\textunderscore");Q(ae,xe,Ue,"\u2220","\\angle",!0);Q(ae,xe,Ue,"\u221E","\\infty",!0);Q(ae,xe,Ue,"\u2032","\\prime");Q(ae,xe,Ue,"\u25B3","\\triangle");Q(ae,xe,Ue,"\u0393","\\Gamma",!0);Q(ae,xe,Ue,"\u0394","\\Delta",!0);Q(ae,xe,Ue,"\u0398","\\Theta",!0);Q(ae,xe,Ue,"\u039B","\\Lambda",!0);Q(ae,xe,Ue,"\u039E","\\Xi",!0);Q(ae,xe,Ue,"\u03A0","\\Pi",!0);Q(ae,xe,Ue,"\u03A3","\\Sigma",!0);Q(ae,xe,Ue,"\u03A5","\\Upsilon",!0);Q(ae,xe,Ue,"\u03A6","\\Phi",!0);Q(ae,xe,Ue,"\u03A8","\\Psi",!0);Q(ae,xe,Ue,"\u03A9","\\Omega",!0);Q(ae,xe,Ue,"A","\u0391");Q(ae,xe,Ue,"B","\u0392");Q(ae,xe,Ue,"E","\u0395");Q(ae,xe,Ue,"Z","\u0396");Q(ae,xe,Ue,"H","\u0397");Q(ae,xe,Ue,"I","\u0399");Q(ae,xe,Ue,"K","\u039A");Q(ae,xe,Ue,"M","\u039C");Q(ae,xe,Ue,"N","\u039D");Q(ae,xe,Ue,"O","\u039F");Q(ae,xe,Ue,"P","\u03A1");Q(ae,xe,Ue,"T","\u03A4");Q(ae,xe,Ue,"X","\u03A7");Q(ae,xe,Ue,"\xAC","\\neg",!0);Q(ae,xe,Ue,"\xAC","\\lnot");Q(ae,xe,Ue,"\u22A4","\\top");Q(ae,xe,Ue,"\u22A5","\\bot");Q(ae,xe,Ue,"\u2205","\\emptyset");Q(ae,Ve,Ue,"\u2205","\\varnothing");Q(ae,xe,wr,"\u03B1","\\alpha",!0);Q(ae,xe,wr,"\u03B2","\\beta",!0);Q(ae,xe,wr,"\u03B3","\\gamma",!0);Q(ae,xe,wr,"\u03B4","\\delta",!0);Q(ae,xe,wr,"\u03F5","\\epsilon",!0);Q(ae,xe,wr,"\u03B6","\\zeta",!0);Q(ae,xe,wr,"\u03B7","\\eta",!0);Q(ae,xe,wr,"\u03B8","\\theta",!0);Q(ae,xe,wr,"\u03B9","\\iota",!0);Q(ae,xe,wr,"\u03BA","\\kappa",!0);Q(ae,xe,wr,"\u03BB","\\lambda",!0);Q(ae,xe,wr,"\u03BC","\\mu",!0);Q(ae,xe,wr,"\u03BD","\\nu",!0);Q(ae,xe,wr,"\u03BE","\\xi",!0);Q(ae,xe,wr,"\u03BF","\\omicron",!0);Q(ae,xe,wr,"\u03C0","\\pi",!0);Q(ae,xe,wr,"\u03C1","\\rho",!0);Q(ae,xe,wr,"\u03C3","\\sigma",!0);Q(ae,xe,wr,"\u03C4","\\tau",!0);Q(ae,xe,wr,"\u03C5","\\upsilon",!0);Q(ae,xe,wr,"\u03D5","\\phi",!0);Q(ae,xe,wr,"\u03C7","\\chi",!0);Q(ae,xe,wr,"\u03C8","\\psi",!0);Q(ae,xe,wr,"\u03C9","\\omega",!0);Q(ae,xe,wr,"\u03B5","\\varepsilon",!0);Q(ae,xe,wr,"\u03D1","\\vartheta",!0);Q(ae,xe,wr,"\u03D6","\\varpi",!0);Q(ae,xe,wr,"\u03F1","\\varrho",!0);Q(ae,xe,wr,"\u03C2","\\varsigma",!0);Q(ae,xe,wr,"\u03C6","\\varphi",!0);Q(ae,xe,Qt,"\u2217","*",!0);Q(ae,xe,Qt,"+","+");Q(ae,xe,Qt,"\u2212","-",!0);Q(ae,xe,Qt,"\u22C5","\\cdot",!0);Q(ae,xe,Qt,"\u2218","\\circ",!0);Q(ae,xe,Qt,"\xF7","\\div",!0);Q(ae,xe,Qt,"\xB1","\\pm",!0);Q(ae,xe,Qt,"\xD7","\\times",!0);Q(ae,xe,Qt,"\u2229","\\cap",!0);Q(ae,xe,Qt,"\u222A","\\cup",!0);Q(ae,xe,Qt,"\u2216","\\setminus",!0);Q(ae,xe,Qt,"\u2227","\\land");Q(ae,xe,Qt,"\u2228","\\lor");Q(ae,xe,Qt,"\u2227","\\wedge",!0);Q(ae,xe,Qt,"\u2228","\\vee",!0);Q(ae,xe,Ue,"\u221A","\\surd");Q(ae,xe,$o,"\u27E8","\\langle",!0);Q(ae,xe,$o,"\u2223","\\lvert");Q(ae,xe,$o,"\u2225","\\lVert");Q(ae,xe,Ms,"?","?");Q(ae,xe,Ms,"!","!");Q(ae,xe,Ms,"\u27E9","\\rangle",!0);Q(ae,xe,Ms,"\u2223","\\rvert");Q(ae,xe,Ms,"\u2225","\\rVert");Q(ae,xe,We,"=","=");Q(ae,xe,We,":",":");Q(ae,xe,We,"\u2248","\\approx",!0);Q(ae,xe,We,"\u2245","\\cong",!0);Q(ae,xe,We,"\u2265","\\ge");Q(ae,xe,We,"\u2265","\\geq",!0);Q(ae,xe,We,"\u2190","\\gets");Q(ae,xe,We,">","\\gt",!0);Q(ae,xe,We,"\u2208","\\in",!0);Q(ae,xe,We,"\uE020","\\@not");Q(ae,xe,We,"\u2282","\\subset",!0);Q(ae,xe,We,"\u2283","\\supset",!0);Q(ae,xe,We,"\u2286","\\subseteq",!0);Q(ae,xe,We,"\u2287","\\supseteq",!0);Q(ae,Ve,We,"\u2288","\\nsubseteq",!0);Q(ae,Ve,We,"\u2289","\\nsupseteq",!0);Q(ae,xe,We,"\u22A8","\\models");Q(ae,xe,We,"\u2190","\\leftarrow",!0);Q(ae,xe,We,"\u2264","\\le");Q(ae,xe,We,"\u2264","\\leq",!0);Q(ae,xe,We,"<","\\lt",!0);Q(ae,xe,We,"\u2192","\\rightarrow",!0);Q(ae,xe,We,"\u2192","\\to");Q(ae,Ve,We,"\u2271","\\ngeq",!0);Q(ae,Ve,We,"\u2270","\\nleq",!0);Q(ae,xe,oh,"\xA0","\\ ");Q(ae,xe,oh,"\xA0","\\space");Q(ae,xe,oh,"\xA0","\\nobreakspace");Q(Dt,xe,oh,"\xA0","\\ ");Q(Dt,xe,oh,"\xA0"," ");Q(Dt,xe,oh,"\xA0","\\space");Q(Dt,xe,oh,"\xA0","\\nobreakspace");Q(ae,xe,oh,"","\\nobreak");Q(ae,xe,oh,"","\\allowbreak");Q(ae,xe,Qx,",",",");Q(ae,xe,Qx,";",";");Q(ae,Ve,Qt,"\u22BC","\\barwedge",!0);Q(ae,Ve,Qt,"\u22BB","\\veebar",!0);Q(ae,xe,Qt,"\u2299","\\odot",!0);Q(ae,xe,Qt,"\u2295","\\oplus",!0);Q(ae,xe,Qt,"\u2297","\\otimes",!0);Q(ae,xe,Ue,"\u2202","\\partial",!0);Q(ae,xe,Qt,"\u2298","\\oslash",!0);Q(ae,Ve,Qt,"\u229A","\\circledcirc",!0);Q(ae,Ve,Qt,"\u22A1","\\boxdot",!0);Q(ae,xe,Qt,"\u25B3","\\bigtriangleup");Q(ae,xe,Qt,"\u25BD","\\bigtriangledown");Q(ae,xe,Qt,"\u2020","\\dagger");Q(ae,xe,Qt,"\u22C4","\\diamond");Q(ae,xe,Qt,"\u22C6","\\star");Q(ae,xe,Qt,"\u25C3","\\triangleleft");Q(ae,xe,Qt,"\u25B9","\\triangleright");Q(ae,xe,$o,"{","\\{");Q(Dt,xe,Ue,"{","\\{");Q(Dt,xe,Ue,"{","\\textbraceleft");Q(ae,xe,Ms,"}","\\}");Q(Dt,xe,Ue,"}","\\}");Q(Dt,xe,Ue,"}","\\textbraceright");Q(ae,xe,$o,"{","\\lbrace");Q(ae,xe,Ms,"}","\\rbrace");Q(ae,xe,$o,"[","\\lbrack",!0);Q(Dt,xe,Ue,"[","\\lbrack",!0);Q(ae,xe,Ms,"]","\\rbrack",!0);Q(Dt,xe,Ue,"]","\\rbrack",!0);Q(ae,xe,$o,"(","\\lparen",!0);Q(ae,xe,Ms,")","\\rparen",!0);Q(Dt,xe,Ue,"<","\\textless",!0);Q(Dt,xe,Ue,">","\\textgreater",!0);Q(ae,xe,$o,"\u230A","\\lfloor",!0);Q(ae,xe,Ms,"\u230B","\\rfloor",!0);Q(ae,xe,$o,"\u2308","\\lceil",!0);Q(ae,xe,Ms,"\u2309","\\rceil",!0);Q(ae,xe,Ue,"\\","\\backslash");Q(ae,xe,Ue,"\u2223","|");Q(ae,xe,Ue,"\u2223","\\vert");Q(Dt,xe,Ue,"|","\\textbar",!0);Q(ae,xe,Ue,"\u2225","\\|");Q(ae,xe,Ue,"\u2225","\\Vert");Q(Dt,xe,Ue,"\u2225","\\textbardbl");Q(Dt,xe,Ue,"~","\\textasciitilde");Q(Dt,xe,Ue,"\\","\\textbackslash");Q(Dt,xe,Ue,"^","\\textasciicircum");Q(ae,xe,We,"\u2191","\\uparrow",!0);Q(ae,xe,We,"\u21D1","\\Uparrow",!0);Q(ae,xe,We,"\u2193","\\downarrow",!0);Q(ae,xe,We,"\u21D3","\\Downarrow",!0);Q(ae,xe,We,"\u2195","\\updownarrow",!0);Q(ae,xe,We,"\u21D5","\\Updownarrow",!0);Q(ae,xe,ta,"\u2210","\\coprod");Q(ae,xe,ta,"\u22C1","\\bigvee");Q(ae,xe,ta,"\u22C0","\\bigwedge");Q(ae,xe,ta,"\u2A04","\\biguplus");Q(ae,xe,ta,"\u22C2","\\bigcap");Q(ae,xe,ta,"\u22C3","\\bigcup");Q(ae,xe,ta,"\u222B","\\int");Q(ae,xe,ta,"\u222B","\\intop");Q(ae,xe,ta,"\u222C","\\iint");Q(ae,xe,ta,"\u222D","\\iiint");Q(ae,xe,ta,"\u220F","\\prod");Q(ae,xe,ta,"\u2211","\\sum");Q(ae,xe,ta,"\u2A02","\\bigotimes");Q(ae,xe,ta,"\u2A01","\\bigoplus");Q(ae,xe,ta,"\u2A00","\\bigodot");Q(ae,xe,ta,"\u222E","\\oint");Q(ae,xe,ta,"\u222F","\\oiint");Q(ae,xe,ta,"\u2230","\\oiiint");Q(ae,xe,ta,"\u2A06","\\bigsqcup");Q(ae,xe,ta,"\u222B","\\smallint");Q(Dt,xe,Z0,"\u2026","\\textellipsis");Q(ae,xe,Z0,"\u2026","\\mathellipsis");Q(Dt,xe,Z0,"\u2026","\\ldots",!0);Q(ae,xe,Z0,"\u2026","\\ldots",!0);Q(ae,xe,Z0,"\u22EF","\\@cdots",!0);Q(ae,xe,Z0,"\u22F1","\\ddots",!0);Q(ae,xe,Ue,"\u22EE","\\varvdots");Q(Dt,xe,Ue,"\u22EE","\\varvdots");Q(ae,xe,mi,"\u02CA","\\acute");Q(ae,xe,mi,"\u02CB","\\grave");Q(ae,xe,mi,"\xA8","\\ddot");Q(ae,xe,mi,"~","\\tilde");Q(ae,xe,mi,"\u02C9","\\bar");Q(ae,xe,mi,"\u02D8","\\breve");Q(ae,xe,mi,"\u02C7","\\check");Q(ae,xe,mi,"^","\\hat");Q(ae,xe,mi,"\u20D7","\\vec");Q(ae,xe,mi,"\u02D9","\\dot");Q(ae,xe,mi,"\u02DA","\\mathring");Q(ae,xe,wr,"\uE131","\\@imath");Q(ae,xe,wr,"\uE237","\\@jmath");Q(ae,xe,Ue,"\u0131","\u0131");Q(ae,xe,Ue,"\u0237","\u0237");Q(Dt,xe,Ue,"\u0131","\\i",!0);Q(Dt,xe,Ue,"\u0237","\\j",!0);Q(Dt,xe,Ue,"\xDF","\\ss",!0);Q(Dt,xe,Ue,"\xE6","\\ae",!0);Q(Dt,xe,Ue,"\u0153","\\oe",!0);Q(Dt,xe,Ue,"\xF8","\\o",!0);Q(Dt,xe,Ue,"\xC6","\\AE",!0);Q(Dt,xe,Ue,"\u0152","\\OE",!0);Q(Dt,xe,Ue,"\xD8","\\O",!0);Q(Dt,xe,mi,"\u02CA","\\'");Q(Dt,xe,mi,"\u02CB","\\`");Q(Dt,xe,mi,"\u02C6","\\^");Q(Dt,xe,mi,"\u02DC","\\~");Q(Dt,xe,mi,"\u02C9","\\=");Q(Dt,xe,mi,"\u02D8","\\u");Q(Dt,xe,mi,"\u02D9","\\.");Q(Dt,xe,mi,"\xB8","\\c");Q(Dt,xe,mi,"\u02DA","\\r");Q(Dt,xe,mi,"\u02C7","\\v");Q(Dt,xe,mi,"\xA8",'\\"');Q(Dt,xe,mi,"\u02DD","\\H");Q(Dt,xe,mi,"\u25EF","\\textcircled");dQ={"--":!0,"---":!0,"``":!0,"''":!0};Q(Dt,xe,Ue,"\u2013","--",!0);Q(Dt,xe,Ue,"\u2013","\\textendash");Q(Dt,xe,Ue,"\u2014","---",!0);Q(Dt,xe,Ue,"\u2014","\\textemdash");Q(Dt,xe,Ue,"\u2018","`",!0);Q(Dt,xe,Ue,"\u2018","\\textquoteleft");Q(Dt,xe,Ue,"\u2019","'",!0);Q(Dt,xe,Ue,"\u2019","\\textquoteright");Q(Dt,xe,Ue,"\u201C","``",!0);Q(Dt,xe,Ue,"\u201C","\\textquotedblleft");Q(Dt,xe,Ue,"\u201D","''",!0);Q(Dt,xe,Ue,"\u201D","\\textquotedblright");Q(ae,xe,Ue,"\xB0","\\degree",!0);Q(Dt,xe,Ue,"\xB0","\\degree");Q(Dt,xe,Ue,"\xB0","\\textdegree",!0);Q(ae,xe,Ue,"\xA3","\\pounds");Q(ae,xe,Ue,"\xA3","\\mathsterling",!0);Q(Dt,xe,Ue,"\xA3","\\pounds");Q(Dt,xe,Ue,"\xA3","\\textsterling",!0);Q(ae,Ve,Ue,"\u2720","\\maltese");Q(Dt,Ve,Ue,"\u2720","\\maltese");AZ='0123456789/@."';for(_w=0;_w{var t=e.charCodeAt(0),r=e.charCodeAt(1),n=(t-55296)*1024+(r-56320)+65536;if(119808<=n&&n<120484){var i=Math.floor((n-119808)/26);return PZ[i]}else if(120782<=n&&n<=120831){var a=Math.floor((n-120782)/10);return bVe[a]}else{if(n===120485||n===120486)return PZ[0];if(120486{if(Ld(e.classes)!==Ld(t.classes)||e.skew!==t.skew||e.maxFontSize!==t.maxFontSize||e.italic!==0&&e.hasClass("mathnormal"))return!1;if(e.classes.length===1){var r=e.classes[0];if(r==="mbin"||r==="mord")return!1}for(var n of Object.keys(e.style))if(e.style[n]!==t.style[n])return!1;for(var i of Object.keys(t.style))if(e.style[i]!==t.style[i])return!1;return!0},"canCombine"),fQ=s(e=>{for(var t=0;tr&&(r=o.height),o.depth>n&&(n=o.depth),o.maxFontSize>i&&(i=o.maxFontSize)}t.height=r,t.depth=n,t.maxFontSize=i},"sizeElementFromChildren"),It=s(function(t,r,n,i){var a=new Dd(t,r,n,i);return M7(a),a},"makeSpan"),Id=s((e,t,r,n)=>new Dd(e,t,r,n),"makeSvgSpan"),X0=s(function(t,r,n){var i=It([t],[],r);return i.height=Math.max(n||r.fontMetrics().defaultRuleThickness,r.minRuleThickness),i.style.borderBottomWidth=Ft(i.height),i.maxFontSize=1,i},"makeLineSpan"),wVe=s(function(t,r,n,i){var a=new j0(t,r,n,i);return M7(a),a},"makeAnchor"),lh=s(function(t){var r=new _d(t);return M7(r),r},"makeFragment"),K0=s(function(t,r){return t instanceof _d?It([],[t],r):t},"wrapFragment"),kVe=s(function(t){if(t.positionType==="individualShift"){for(var r=t.children,n=[r[0]],i=-r[0].shift-r[0].elem.depth,a=i,o=1;o{var r=It(["mspace"],[],t),n=wi(e,t);return r.style.marginRight=Ft(n),r},"makeGlue"),Mw=s((e,t,r)=>{var n,i;switch(e){case"amsrm":n="AMS";break;case"textrm":n="Main";break;case"textsf":n="SansSerif";break;case"texttt":n="Typewriter";break;default:n=e}return t==="textbf"&&r==="textit"?i="BoldItalic":t==="textbf"?i="Bold":r==="textit"?i="Italic":i="Regular",n+"-"+i},"retrieveTextFontName"),y7={mathbf:{variant:"bold",fontName:"Main-Bold"},mathrm:{variant:"normal",fontName:"Main-Regular"},textit:{variant:"italic",fontName:"Main-Italic"},mathit:{variant:"italic",fontName:"Main-Italic"},mathnormal:{variant:"italic",fontName:"Math-Italic"},mathsfit:{variant:"sans-serif-italic",fontName:"SansSerif-Italic"},mathbb:{variant:"double-struck",fontName:"AMS-Regular"},mathcal:{variant:"script",fontName:"Caligraphic-Regular"},mathfrak:{variant:"fraktur",fontName:"Fraktur-Regular"},mathscr:{variant:"script",fontName:"Script-Regular"},mathsf:{variant:"sans-serif",fontName:"SansSerif-Regular"},mathtt:{variant:"monospace",fontName:"Typewriter-Regular"}},mQ={vec:["vec",.471,.714],oiintSize1:["oiintSize1",.957,.499],oiintSize2:["oiintSize2",1.472,.659],oiiintSize1:["oiiintSize1",1.304,.499],oiiintSize2:["oiiintSize2",1.98,.659]},gQ=s(function(t,r){var[n,i,a]=mQ[t],o=new $c(n),l=new ql([o],{width:Ft(i),height:Ft(a),style:"width:"+Ft(i),viewBox:"0 0 "+1e3*i+" "+1e3*a,preserveAspectRatio:"xMinYMin"}),u=Id(["overlay"],[l],r);return u.height=a,u.style.height=Ft(a),u.style.width=Ft(i),u},"staticSvg"),Si={number:3,unit:"mu"},Wp={number:4,unit:"mu"},ih={number:5,unit:"mu"},EVe={mord:{mop:Si,mbin:Wp,mrel:ih,minner:Si},mop:{mord:Si,mop:Si,mrel:ih,minner:Si},mbin:{mord:Wp,mop:Wp,mopen:Wp,minner:Wp},mrel:{mord:ih,mop:ih,mopen:ih,minner:ih},mopen:{},mclose:{mop:Si,mbin:Wp,mrel:ih,minner:Si},mpunct:{mord:Si,mop:Si,mrel:ih,mopen:Si,mclose:Si,mpunct:Si,minner:Si},minner:{mord:Si,mop:Si,mbin:Wp,mrel:ih,mopen:Si,mpunct:Si,minner:Si}},AVe={mord:{mop:Si},mop:{mord:Si,mop:Si},mbin:{},mrel:{},mopen:{},mclose:{mop:Si},mpunct:{},minner:{mop:Si}},yQ={},Ww={},qw={};s(jt,"defineFunction");s(Hp,"defineFunctionBuilders");Hw=s(function(t){return t.type==="ordgroup"&&t.body.length===1?t.body[0]:t},"normalizeArgument"),ea=s(function(t){return t.type==="ordgroup"?t.body:[t]},"ordargument"),RVe=new Set(["leftmost","mbin","mopen","mrel","mop","mpunct"]),_Ve=new Set(["rightmost","mrel","mclose","mpunct"]),LVe={display:Vr.DISPLAY,text:Vr.TEXT,script:Vr.SCRIPT,scriptscript:Vr.SCRIPTSCRIPT},DVe={mord:"mord",mop:"mop",mbin:"mbin",mrel:"mrel",mopen:"mopen",mclose:"mclose",mpunct:"mpunct",minner:"minner"},fa=s(function(t,r,n,i){i===void 0&&(i=[null,null]);for(var a=[],o=0;o{var v=y.classes[0],x=g.classes[0];v==="mbin"&&_Ve.has(x)?y.classes[0]="mord":x==="mbin"&&RVe.has(v)&&(g.classes[0]="mord")},{node:f},p,m),v7(a,(g,y)=>{var v,x,b=b7(y),T=b7(g),w=b&&T?g.hasClass("mtight")?(v=AVe[b])==null?void 0:v[T]:(x=EVe[b])==null?void 0:x[T]:null;if(w)return pQ(w,h)},{node:f},p,m),a},"buildExpression"),v7=s(function(t,r,n,i,a){i&&t.push(i);for(var o=0;op=>{t.splice(f+1,0,p),o++})(o)}i&&t.pop()},"traverseNonSpaceNodes"),vQ=s(function(t){return t instanceof _d||t instanceof j0||t instanceof Dd&&t.hasClass("enclosing")?t:null},"checkPartialGroup"),x7=s(function(t,r){var n=vQ(t);if(n){var i=n.children;if(i.length){if(r==="right")return x7(i[i.length-1],"right");if(r==="left")return x7(i[0],"left")}}return t},"getOutermostNode"),b7=s(function(t,r){if(!t)return null;r&&(t=x7(t,r));var n=t.classes[0];return DVe[n]||null},"getTypeOfDomTree"),Zx=s(function(t,r){var n=["nulldelimiter"].concat(t.baseSizingClasses());return It(r.concat(n))},"makeNullDelimiter"),Rn=s(function(t,r,n){if(!t)return It();if(Ww[t.type]){var i=Ww[t.type](t,r);if(n&&r.size!==n.size){i=It(r.sizingClasses(n),[i],r);var a=r.sizeMultiplier/n.sizeMultiplier;i.height*=a,i.depth*=a}return i}else throw new Nt("Got group of unknown type: '"+t.type+"'")},"buildGroup");s(Nw,"buildHTMLUnbreakable");s(T7,"buildHTML");s(xQ,"newDocumentFragment");Mt=class{static{s(this,"MathNode")}constructor(t,r,n){this.type=void 0,this.attributes=void 0,this.children=void 0,this.classes=void 0,this.type=t,this.attributes={},this.children=r||[],this.classes=n||[]}setAttribute(t,r){this.attributes[t]=r}getAttribute(t){return this.attributes[t]}toNode(){var t=document.createElementNS("http://www.w3.org/1998/Math/MathML",this.type);for(var r in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,r)&&t.setAttribute(r,this.attributes[r]);this.classes.length>0&&(t.className=Ld(this.classes));for(var n=0;n0&&(t+=' class ="'+fs(Ld(this.classes))+'"'),t+=">";for(var n=0;n",t}toText(){return this.children.map(t=>t.toText()).join("")}},Di=class{static{s(this,"TextNode")}constructor(t){this.text=void 0,this.text=t}toNode(){return document.createTextNode(this.text)}toMarkup(){return fs(this.toText())}toText(){return this.text}},Uw=class{static{s(this,"SpaceNode")}constructor(t){this.width=void 0,this.character=void 0,this.width=t,t>=.05555&&t<=.05556?this.character="\u200A":t>=.1666&&t<=.1667?this.character="\u2009":t>=.2222&&t<=.2223?this.character="\u2005":t>=.2777&&t<=.2778?this.character="\u2005\u200A":t>=-.05556&&t<=-.05555?this.character="\u200A\u2063":t>=-.1667&&t<=-.1666?this.character="\u2009\u2063":t>=-.2223&&t<=-.2222?this.character="\u205F\u2063":t>=-.2778&&t<=-.2777?this.character="\u2005\u2063":this.character=null}toNode(){if(this.character)return document.createTextNode(this.character);var t=document.createElementNS("http://www.w3.org/1998/Math/MathML","mspace");return t.setAttribute("width",Ft(this.width)),t}toMarkup(){return this.character?""+this.character+"":''}toText(){return this.character?this.character:" "}},IVe=new Set(["\\imath","\\jmath"]),MVe=new Set(["mrow","mtable"]),pl=s(function(t,r,n){return pi[r][t]&&pi[r][t].replace&&t.charCodeAt(0)!==55349&&!(dQ.hasOwnProperty(t)&&n&&(n.fontFamily&&n.fontFamily.slice(4,6)==="tt"||n.font&&n.font.slice(4,6)==="tt"))&&(t=pi[r][t].replace),new Di(t)},"makeText"),N7=s(function(t){return t.length===1?t[0]:new Mt("mrow",t)},"makeRow"),NVe={mathit:"italic",boldsymbol:s(e=>e.type==="textord"?"bold":"bold-italic","boldsymbol"),mathbf:"bold",mathbb:"double-struck",mathsfit:"sans-serif-italic",mathfrak:"fraktur",mathscr:"script",mathcal:"script",mathsf:"sans-serif",mathtt:"monospace"},P7=s((e,t)=>{if(e.mode==="text"){if(t.fontFamily==="texttt")return"monospace";if(t.fontFamily==="textsf")return t.fontShape==="textit"&&t.fontWeight==="textbf"?"sans-serif-bold-italic":t.fontShape==="textit"?"sans-serif-italic":t.fontWeight==="textbf"?"bold-sans-serif":"sans-serif";if(t.fontShape==="textit"&&t.fontWeight==="textbf")return"bold-italic";if(t.fontShape==="textit")return"italic";if(t.fontWeight==="textbf")return"bold"}var r=t.font;if(!r||r==="mathnormal")return null;var n=e.mode,i=NVe[r];if(i)return typeof i=="function"?i(e):i;var a=e.text;if(IVe.has(a))return null;if(pi[n][a]){var o=pi[n][a].replace;o&&(a=o)}var l=y7[r].fontName;return D7(a,l,n)?y7[r].variant:null},"getVariant");s(QD,"isNumberPunctuation");Fo=s(function(t,r,n){if(t.length===1){var i=Yn(t[0],r);return n&&i instanceof Mt&&i.type==="mo"&&(i.setAttribute("lspace","0em"),i.setAttribute("rspace","0em")),[i]}for(var a=[],o,l=0;l=1&&(o.type==="mn"||QD(o))){var h=u.children[0];h instanceof Mt&&h.type==="mn"&&(h.children=[...o.children,...h.children],a.pop())}else if(o.type==="mi"&&o.children.length===1){var d=o.children[0];if(d instanceof Di&&d.text==="\u0338"&&(u.type==="mo"||u.type==="mi"||u.type==="mn")){var f=u.children[0];f instanceof Di&&f.text.length>0&&(f.text=f.text.slice(0,1)+"\u0338"+f.text.slice(1),a.pop())}}}a.push(u),o=u}return a},"buildExpression"),Md=s(function(t,r,n){return N7(Fo(t,r,n))},"buildExpressionRow"),Yn=s(function(t,r){if(!t)return new Mt("mrow");if(qw[t.type])return qw[t.type](t,r);throw new Nt("Got group of unknown type: '"+t.type+"'")},"buildGroup");s(OZ,"buildMathML");PVe=[[1,1,1],[2,1,1],[3,1,1],[4,2,1],[5,2,1],[6,3,1],[7,4,2],[8,6,3],[9,7,6],[10,8,7],[11,10,9]],BZ=[.5,.6,.7,.8,.9,1,1.2,1.44,1.728,2.074,2.488],$Z=s(function(t,r){return r.size<2?t:PVe[t-1][r.size-1]},"sizeAtStyle"),Yw=class e{static{s(this,"Options")}constructor(t){this.style=void 0,this.color=void 0,this.size=void 0,this.textSize=void 0,this.phantom=void 0,this.font=void 0,this.fontFamily=void 0,this.fontWeight=void 0,this.fontShape=void 0,this.sizeMultiplier=void 0,this.maxSize=void 0,this.minRuleThickness=void 0,this._fontMetrics=void 0,this.style=t.style,this.color=t.color,this.size=t.size||e.BASESIZE,this.textSize=t.textSize||this.size,this.phantom=!!t.phantom,this.font=t.font||"",this.fontFamily=t.fontFamily||"",this.fontWeight=t.fontWeight||"",this.fontShape=t.fontShape||"",this.sizeMultiplier=BZ[this.size-1],this.maxSize=t.maxSize,this.minRuleThickness=t.minRuleThickness,this._fontMetrics=void 0}extend(t){var r={style:this.style,size:this.size,textSize:this.textSize,color:this.color,phantom:this.phantom,font:this.font,fontFamily:this.fontFamily,fontWeight:this.fontWeight,fontShape:this.fontShape,maxSize:this.maxSize,minRuleThickness:this.minRuleThickness};return Object.assign(r,t),new e(r)}havingStyle(t){return this.style===t?this:this.extend({style:t,size:$Z(this.textSize,t)})}havingCrampedStyle(){return this.havingStyle(this.style.cramp())}havingSize(t){return this.size===t&&this.textSize===t?this:this.extend({style:this.style.text(),size:t,textSize:t,sizeMultiplier:BZ[t-1]})}havingBaseStyle(t){t=t||this.style.text();var r=$Z(e.BASESIZE,t);return this.size===r&&this.textSize===e.BASESIZE&&this.style===t?this:this.extend({style:t,size:r})}havingBaseSizing(){var t;switch(this.style.id){case 4:case 5:t=3;break;case 6:case 7:t=1;break;default:t=6}return this.extend({style:this.style.text(),size:t})}withColor(t){return this.extend({color:t})}withPhantom(){return this.extend({phantom:!0})}withFont(t){return this.extend({font:t})}withTextFontFamily(t){return this.extend({fontFamily:t,font:""})}withTextFontWeight(t){return this.extend({fontWeight:t,font:""})}withTextFontShape(t){return this.extend({fontShape:t,font:""})}sizingClasses(t){return t.size!==this.size?["sizing","reset-size"+t.size,"size"+this.size]:[]}baseSizingClasses(){return this.size!==e.BASESIZE?["sizing","reset-size"+this.size,"size"+e.BASESIZE]:[]}fontMetrics(){return this._fontMetrics||(this._fontMetrics=vVe(this.size)),this._fontMetrics}getColor(){return this.phantom?"transparent":this.color}};Yw.BASESIZE=6;bQ=s(function(t){return new Yw({style:t.displayMode?Vr.DISPLAY:Vr.TEXT,maxSize:t.maxSize,minRuleThickness:t.minRuleThickness})},"optionsFromSettings"),TQ=s(function(t,r){if(r.displayMode){var n=["katex-display"];r.leqno&&n.push("leqno"),r.fleqn&&n.push("fleqn"),t=It(n,[t])}return t},"displayWrap"),OVe=s(function(t,r,n){var i=bQ(n),a;if(n.output==="mathml")return OZ(t,r,i,n.displayMode,!0);if(n.output==="html"){var o=T7(t,i);a=It(["katex"],[o])}else{var l=OZ(t,r,i,n.displayMode,!1),u=T7(t,i);a=It(["katex"],[l,u])}return TQ(a,n)},"buildTree"),BVe=s(function(t,r,n){var i=bQ(n),a=T7(t,i),o=It(["katex"],[a]);return TQ(o,n)},"buildHTMLTree"),$Ve={widehat:"^",widecheck:"\u02C7",widetilde:"~",utilde:"~",overleftarrow:"\u2190",underleftarrow:"\u2190",xleftarrow:"\u2190",overrightarrow:"\u2192",underrightarrow:"\u2192",xrightarrow:"\u2192",underbrace:"\u23DF",overbrace:"\u23DE",underbracket:"\u23B5",overbracket:"\u23B4",overgroup:"\u23E0",undergroup:"\u23E1",overleftrightarrow:"\u2194",underleftrightarrow:"\u2194",xleftrightarrow:"\u2194",Overrightarrow:"\u21D2",xRightarrow:"\u21D2",overleftharpoon:"\u21BC",xleftharpoonup:"\u21BC",overrightharpoon:"\u21C0",xrightharpoonup:"\u21C0",xLeftarrow:"\u21D0",xLeftrightarrow:"\u21D4",xhookleftarrow:"\u21A9",xhookrightarrow:"\u21AA",xmapsto:"\u21A6",xrightharpoondown:"\u21C1",xleftharpoondown:"\u21BD",xrightleftharpoons:"\u21CC",xleftrightharpoons:"\u21CB",xtwoheadleftarrow:"\u219E",xtwoheadrightarrow:"\u21A0",xlongequal:"=",xtofrom:"\u21C4",xrightleftarrows:"\u21C4",xrightequilibrium:"\u21CC",xleftequilibrium:"\u21CB","\\cdrightarrow":"\u2192","\\cdleftarrow":"\u2190","\\cdlongequal":"="},Qw=s(function(t){var r=new Mt("mo",[new Di($Ve[t.replace(/^\\/,"")])]);return r.setAttribute("stretchy","true"),r},"stretchyMathML"),FVe={overrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],overleftarrow:[["leftarrow"],.888,522,"xMinYMin"],underrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],underleftarrow:[["leftarrow"],.888,522,"xMinYMin"],xrightarrow:[["rightarrow"],1.469,522,"xMaxYMin"],"\\cdrightarrow":[["rightarrow"],3,522,"xMaxYMin"],xleftarrow:[["leftarrow"],1.469,522,"xMinYMin"],"\\cdleftarrow":[["leftarrow"],3,522,"xMinYMin"],Overrightarrow:[["doublerightarrow"],.888,560,"xMaxYMin"],xRightarrow:[["doublerightarrow"],1.526,560,"xMaxYMin"],xLeftarrow:[["doubleleftarrow"],1.526,560,"xMinYMin"],overleftharpoon:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoonup:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoondown:[["leftharpoondown"],.888,522,"xMinYMin"],overrightharpoon:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoonup:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoondown:[["rightharpoondown"],.888,522,"xMaxYMin"],xlongequal:[["longequal"],.888,334,"xMinYMin"],"\\cdlongequal":[["longequal"],3,334,"xMinYMin"],xtwoheadleftarrow:[["twoheadleftarrow"],.888,334,"xMinYMin"],xtwoheadrightarrow:[["twoheadrightarrow"],.888,334,"xMaxYMin"],overleftrightarrow:[["leftarrow","rightarrow"],.888,522],overbrace:[["leftbrace","midbrace","rightbrace"],1.6,548],underbrace:[["leftbraceunder","midbraceunder","rightbraceunder"],1.6,548],underleftrightarrow:[["leftarrow","rightarrow"],.888,522],xleftrightarrow:[["leftarrow","rightarrow"],1.75,522],xLeftrightarrow:[["doubleleftarrow","doublerightarrow"],1.75,560],xrightleftharpoons:[["leftharpoondownplus","rightharpoonplus"],1.75,716],xleftrightharpoons:[["leftharpoonplus","rightharpoondownplus"],1.75,716],xhookleftarrow:[["leftarrow","righthook"],1.08,522],xhookrightarrow:[["lefthook","rightarrow"],1.08,522],overlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],underlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],overbracket:[["leftbracketover","rightbracketover"],1.6,440],underbracket:[["leftbracketunder","rightbracketunder"],1.6,410],overgroup:[["leftgroup","rightgroup"],.888,342],undergroup:[["leftgroupunder","rightgroupunder"],.888,342],xmapsto:[["leftmapsto","rightarrow"],1.5,522],xtofrom:[["leftToFrom","rightToFrom"],1.75,528],xrightleftarrows:[["baraboveleftarrow","rightarrowabovebar"],1.75,901],xrightequilibrium:[["baraboveshortleftharpoon","rightharpoonaboveshortbar"],1.75,716],xleftequilibrium:[["shortbaraboveleftharpoon","shortrightharpoonabovebar"],1.75,716]},GVe=new Set(["widehat","widecheck","widetilde","utilde"]),Jw=s(function(t,r){function n(){var l=4e5,u=t.label.slice(1);if(GVe.has(u)&&"base"in t){var h=t.base.type==="ordgroup"?t.base.body.length:1,d,f,p;if(h>5)u==="widehat"||u==="widecheck"?(d=420,l=2364,p=.42,f=u+"4"):(d=312,l=2340,p=.34,f="tilde4");else{var m=[1,1,2,2,3,3][h];u==="widehat"||u==="widecheck"?(l=[0,1062,2364,2364,2364][m],d=[0,239,300,360,420][m],p=[0,.24,.3,.3,.36,.42][m],f=u+m):(l=[0,600,1033,2339,2340][m],d=[0,260,286,306,312][m],p=[0,.26,.286,.3,.306,.34][m],f="tilde"+m)}var g=new $c(f),y=new ql([g],{width:"100%",height:Ft(p),viewBox:"0 0 "+l+" "+d,preserveAspectRatio:"none"});return{span:Id([],[y],r),minWidth:0,height:p}}else{var v=[],x=FVe[u];if(!x)throw new Error('No SVG data for "'+u+'".');var[b,T,w]=x,C=w/1e3,S=b.length,k,R;if(S===1){if(x.length!==4)throw new Error('Expected 4-tuple for single-path SVG data "'+u+'".');k=["hide-tail"],R=[x[3]]}else if(S===2)k=["halfarrow-left","halfarrow-right"],R=["xMinYMin","xMaxYMin"];else if(S===3)k=["brace-left","brace-center","brace-right"],R=["xMinYMin","xMidYMin","xMaxYMin"];else throw new Error(`Correct katexImagesData or update code here to support + `+S+" children.");for(var M=0;M0&&(i.style.minWidth=Ft(a)),i},"stretchySvg"),zVe=s(function(t,r,n,i,a){var o,l=t.height+t.depth+n+i;if(/fbox|color|angl/.test(r)){if(o=It(["stretchy",r],[],a),r==="fbox"){var u=a.color&&a.getColor();u&&(o.style.borderColor=u)}}else{var h=[];/^[bx]cancel$/.test(r)&&h.push(new Kx({x1:"0",y1:"0",x2:"100%",y2:"100%","stroke-width":"0.046em"})),/^x?cancel$/.test(r)&&h.push(new Kx({x1:"0",y1:"100%",x2:"100%",y2:"0","stroke-width":"0.046em"}));var d=new ql(h,{width:"100%",height:Ft(l)});o=Id([],[d],a)}return o.height=l,o.style.height=Ft(l),o},"stretchyEnclose"),VVe={bin:1,close:1,inner:1,open:1,punct:1,rel:1},WVe={"accent-token":1,mathord:1,"op-token":1,spacing:1,textord:1};s(qVe,"isAtom");s(Kr,"assertNodeType");s(ek,"assertSymbolNodeType");s(tk,"checkSymbolNodeType");CQ=s(e=>{if(e instanceof Is)return e;if(yVe(e)&&e.children.length===1)return CQ(e.children[0])},"getBaseSymbol"),O7=s((e,t)=>{var r,n,i;e&&e.type==="supsub"?(n=Kr(e.base,"accent"),r=n.base,e.base=r,i=gVe(Rn(e,t)),e.base=n):(n=Kr(e,"accent"),r=n.base);var a=Rn(r,t.havingCrampedStyle()),o=n.isShifty&&sh(r),l=0;if(o){var u,h;l=(u=(h=CQ(a))==null?void 0:h.skew)!=null?u:0}var d=n.label==="\\c",f=d?a.height+a.depth:Math.min(a.height,t.fontMetrics().xHeight),p;if(n.isStretchy)p=Jw(n,t),p=An({positionType:"firstBaseline",children:[{type:"elem",elem:a},{type:"elem",elem:p,wrapperClasses:["svg-align"],wrapperStyle:l>0?{width:"calc(100% - "+Ft(2*l)+")",marginLeft:Ft(2*l)}:void 0}]});else{var m,g;n.label==="\\vec"?(m=gQ("vec",t),g=mQ.vec[1]):(m=Zw({type:"textord",mode:n.mode,text:n.label},t,"textord"),m=mVe(m),m.italic=0,g=m.width,d&&(f+=m.depth)),p=It(["accent-body"],[m]);var y=n.label==="\\textcircled";y&&(p.classes.push("accent-full"),f=a.height);var v=l;y||(v-=g/2),p.style.left=Ft(v),n.label==="\\textcircled"&&(p.style.top=".2em"),p=An({positionType:"firstBaseline",children:[{type:"elem",elem:a},{type:"kern",size:-f},{type:"elem",elem:p}]})}var x=It(["mord","accent"],[p],t);return i?(i.children[0]=x,i.height=Math.max(x.height,i.height),i.classes[0]="mord",i):x},"htmlBuilder$a"),SQ=s((e,t)=>{var r=e.isStretchy?Qw(e.label):new Mt("mo",[pl(e.label,e.mode)]),n=new Mt("mover",[Yn(e.base,t),r]);return n.setAttribute("accent","true"),n},"mathmlBuilder$9"),HVe=new RegExp(["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring"].map(e=>"\\"+e).join("|"));jt({type:"accent",names:["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring","\\widecheck","\\widehat","\\widetilde","\\overrightarrow","\\overleftarrow","\\Overrightarrow","\\overleftrightarrow","\\overgroup","\\overlinesegment","\\overleftharpoon","\\overrightharpoon"],props:{numArgs:1},handler:s((e,t)=>{var r=Hw(t[0]),n=!HVe.test(e.funcName),i=!n||e.funcName==="\\widehat"||e.funcName==="\\widetilde"||e.funcName==="\\widecheck";return{type:"accent",mode:e.parser.mode,label:e.funcName,isStretchy:n,isShifty:i,base:r}},"handler"),htmlBuilder:O7,mathmlBuilder:SQ});jt({type:"accent",names:["\\'","\\`","\\^","\\~","\\=","\\u","\\.",'\\"',"\\c","\\r","\\H","\\v","\\textcircled"],props:{numArgs:1,allowedInText:!0,allowedInMath:!0,argTypes:["primitive"]},handler:s((e,t)=>{var r=t[0],n=e.parser.mode;return n==="math"&&(e.parser.settings.reportNonstrict("mathVsTextAccents","LaTeX's accent "+e.funcName+" works only in text mode"),n="text"),{type:"accent",mode:n,label:e.funcName,isStretchy:!1,isShifty:!0,base:r}},"handler"),htmlBuilder:O7,mathmlBuilder:SQ});jt({type:"accentUnder",names:["\\underleftarrow","\\underrightarrow","\\underleftrightarrow","\\undergroup","\\underlinesegment","\\utilde"],props:{numArgs:1},handler:s((e,t)=>{var{parser:r,funcName:n}=e,i=t[0];return{type:"accentUnder",mode:r.mode,label:n,base:i}},"handler"),htmlBuilder:s((e,t)=>{var r=Rn(e.base,t),n=Jw(e,t),i=e.label==="\\utilde"?.12:0,a=An({positionType:"top",positionData:r.height,children:[{type:"elem",elem:n,wrapperClasses:["svg-align"]},{type:"kern",size:i},{type:"elem",elem:r}]});return It(["mord","accentunder"],[a],t)},"htmlBuilder"),mathmlBuilder:s((e,t)=>{var r=Qw(e.label),n=new Mt("munder",[Yn(e.base,t),r]);return n.setAttribute("accentunder","true"),n},"mathmlBuilder")});Pw=s(e=>{var t=new Mt("mpadded",e?[e]:[]);return t.setAttribute("width","+0.6em"),t.setAttribute("lspace","0.3em"),t},"paddedNode");jt({type:"xArrow",names:["\\xleftarrow","\\xrightarrow","\\xLeftarrow","\\xRightarrow","\\xleftrightarrow","\\xLeftrightarrow","\\xhookleftarrow","\\xhookrightarrow","\\xmapsto","\\xrightharpoondown","\\xrightharpoonup","\\xleftharpoondown","\\xleftharpoonup","\\xrightleftharpoons","\\xleftrightharpoons","\\xlongequal","\\xtwoheadrightarrow","\\xtwoheadleftarrow","\\xtofrom","\\xrightleftarrows","\\xrightequilibrium","\\xleftequilibrium","\\\\cdrightarrow","\\\\cdleftarrow","\\\\cdlongequal"],props:{numArgs:1,numOptionalArgs:1},handler(e,t,r){var{parser:n,funcName:i}=e;return{type:"xArrow",mode:n.mode,label:i,body:t[0],below:r[0]}},htmlBuilder(e,t){var r=t.style,n=t.havingStyle(r.sup()),i=K0(Rn(e.body,n,t),t),a=e.label.slice(0,2)==="\\x"?"x":"cd";i.classes.push(a+"-arrow-pad");var o;e.below&&(n=t.havingStyle(r.sub()),o=K0(Rn(e.below,n,t),t),o.classes.push(a+"-arrow-pad"));var l=Jw(e,t),u=-t.fontMetrics().axisHeight+.5*l.height,h=-t.fontMetrics().axisHeight-.5*l.height-.111;(i.depth>.25||e.label==="\\xleftequilibrium")&&(h-=i.depth);var d;if(o){var f=-t.fontMetrics().axisHeight+o.height+.5*l.height+.111;d=An({positionType:"individualShift",children:[{type:"elem",elem:i,shift:h},{type:"elem",elem:l,shift:u,wrapperClasses:["svg-align"]},{type:"elem",elem:o,shift:f}]})}else d=An({positionType:"individualShift",children:[{type:"elem",elem:i,shift:h},{type:"elem",elem:l,shift:u,wrapperClasses:["svg-align"]}]});return It(["mrel","x-arrow"],[d],t)},mathmlBuilder(e,t){var r=Qw(e.label);r.setAttribute("minsize",e.label.charAt(0)==="x"?"1.75em":"3.0em");var n;if(e.body){var i=Pw(Yn(e.body,t));if(e.below){var a=Pw(Yn(e.below,t));n=new Mt("munderover",[r,a,i])}else n=new Mt("mover",[r,i])}else if(e.below){var o=Pw(Yn(e.below,t));n=new Mt("munder",[r,o])}else n=Pw(),n=new Mt("mover",[r,n]);return n}});s(wQ,"htmlBuilder$9");s(kQ,"mathmlBuilder$8");jt({type:"mclass",names:["\\mathord","\\mathbin","\\mathrel","\\mathopen","\\mathclose","\\mathpunct","\\mathinner"],props:{numArgs:1,primitive:!0},handler(e,t){var{parser:r,funcName:n}=e,i=t[0];return{type:"mclass",mode:r.mode,mclass:"m"+n.slice(5),body:ea(i),isCharacterBox:sh(i)}},htmlBuilder:wQ,mathmlBuilder:kQ});rk=s(e=>{var t=e.type==="ordgroup"&&e.body.length?e.body[0]:e;return t.type==="atom"&&(t.family==="bin"||t.family==="rel")?"m"+t.family:"mord"},"binrelClass");jt({type:"mclass",names:["\\@binrel"],props:{numArgs:2},handler(e,t){var{parser:r}=e;return{type:"mclass",mode:r.mode,mclass:rk(t[0]),body:ea(t[1]),isCharacterBox:sh(t[1])}}});jt({type:"mclass",names:["\\stackrel","\\overset","\\underset"],props:{numArgs:2},handler(e,t){var{parser:r,funcName:n}=e,i=t[1],a=t[0],o;n!=="\\stackrel"?o=rk(i):o="mrel";var l={type:"op",mode:i.mode,limits:!0,alwaysHandleSupSub:!0,parentIsSupSub:!1,symbol:!1,suppressBaseShift:n!=="\\stackrel",body:ea(i)},u={type:"supsub",mode:a.mode,base:l,sup:n==="\\underset"?null:a,sub:n==="\\underset"?a:null};return{type:"mclass",mode:r.mode,mclass:o,body:[u],isCharacterBox:sh(u)}},htmlBuilder:wQ,mathmlBuilder:kQ});jt({type:"pmb",names:["\\pmb"],props:{numArgs:1,allowedInText:!0},handler(e,t){var{parser:r}=e;return{type:"pmb",mode:r.mode,mclass:rk(t[0]),body:ea(t[0])}},htmlBuilder(e,t){var r=fa(e.body,t,!0),n=It([e.mclass],r,t);return n.style.textShadow="0.02em 0.01em 0.04px",n},mathmlBuilder(e,t){var r=Fo(e.body,t),n=new Mt("mstyle",r);return n.setAttribute("style","text-shadow: 0.02em 0.01em 0.04px"),n}});UVe={">":"\\\\cdrightarrow","<":"\\\\cdleftarrow","=":"\\\\cdlongequal",A:"\\uparrow",V:"\\downarrow","|":"\\Vert",".":"no arrow"},FZ=s(()=>({type:"styling",body:[],mode:"math",style:"display",resetFont:!0}),"newCell"),GZ=s(e=>e.type==="textord"&&e.text==="@","isStartOfArrow"),YVe=s((e,t)=>(e.type==="mathord"||e.type==="atom")&&e.text===t,"isLabelEnd");s(jVe,"cdArrow");s(XVe,"parseCD");jt({type:"cdlabel",names:["\\\\cdleft","\\\\cdright"],props:{numArgs:1},handler(e,t){var{parser:r,funcName:n}=e;return{type:"cdlabel",mode:r.mode,side:n.slice(4),label:t[0]}},htmlBuilder(e,t){var r=t.havingStyle(t.style.sup()),n=K0(Rn(e.label,r,t),t);return n.classes.push("cd-label-"+e.side),n.style.bottom=Ft(.8-n.depth),n.height=0,n.depth=0,n},mathmlBuilder(e,t){var r=new Mt("mrow",[Yn(e.label,t)]);return r=new Mt("mpadded",[r]),r.setAttribute("width","0"),e.side==="left"&&r.setAttribute("lspace","-1width"),r.setAttribute("voffset","0.7em"),r=new Mt("mstyle",[r]),r.setAttribute("displaystyle","false"),r.setAttribute("scriptlevel","1"),r}});jt({type:"cdlabelparent",names:["\\\\cdparent"],props:{numArgs:1},handler(e,t){var{parser:r}=e;return{type:"cdlabelparent",mode:r.mode,fragment:t[0]}},htmlBuilder(e,t){var r=K0(Rn(e.fragment,t),t);return r.classes.push("cd-vert-arrow"),r},mathmlBuilder(e,t){return new Mt("mrow",[Yn(e.fragment,t)])}});jt({type:"textord",names:["\\@char"],props:{numArgs:1,allowedInText:!0},handler(e,t){for(var{parser:r}=e,n=Kr(t[0],"ordgroup"),i=n.body,a="",o=0;o=1114111)throw new Nt("\\@char with invalid code point "+a);return u<=65535?h=String.fromCharCode(u):(u-=65536,h=String.fromCharCode((u>>10)+55296,(u&1023)+56320)),{type:"textord",mode:r.mode,text:h}}});EQ=s((e,t)=>{var r=fa(e.body,t.withColor(e.color),!1);return lh(r)},"htmlBuilder$8"),AQ=s((e,t)=>{var r=Fo(e.body,t.withColor(e.color)),n=new Mt("mstyle",r);return n.setAttribute("mathcolor",e.color),n},"mathmlBuilder$7");jt({type:"color",names:["\\textcolor"],props:{numArgs:2,allowedInText:!0,argTypes:["color","original"]},handler(e,t){var{parser:r}=e,n=Kr(t[0],"color-token").color,i=t[1];return{type:"color",mode:r.mode,color:n,body:ea(i)}},htmlBuilder:EQ,mathmlBuilder:AQ});jt({type:"color",names:["\\color"],props:{numArgs:1,allowedInText:!0,argTypes:["color"]},handler(e,t){var{parser:r,breakOnTokenText:n}=e,i=Kr(t[0],"color-token").color;r.gullet.macros.set("\\current@color",i);var a=r.parseExpression(!0,n);return{type:"color",mode:r.mode,color:i,body:a}},htmlBuilder:EQ,mathmlBuilder:AQ});jt({type:"cr",names:["\\\\"],props:{numArgs:0,numOptionalArgs:0,allowedInText:!0},handler(e,t,r){var{parser:n}=e,i=n.gullet.future().text==="["?n.parseSizeGroup(!0):null,a=!n.settings.displayMode||!n.settings.useStrictBehavior("newLineInDisplayMode","In LaTeX, \\\\ or \\newline does nothing in display mode");return{type:"cr",mode:n.mode,newLine:a,size:i&&Kr(i,"size").value}},htmlBuilder(e,t){var r=It(["mspace"],[],t);return e.newLine&&(r.classes.push("newline"),e.size&&(r.style.marginTop=Ft(wi(e.size,t)))),r},mathmlBuilder(e,t){var r=new Mt("mspace");return e.newLine&&(r.setAttribute("linebreak","newline"),e.size&&r.setAttribute("height",Ft(wi(e.size,t)))),r}});C7={"\\global":"\\global","\\long":"\\\\globallong","\\\\globallong":"\\\\globallong","\\def":"\\gdef","\\gdef":"\\gdef","\\edef":"\\xdef","\\xdef":"\\xdef","\\let":"\\\\globallet","\\futurelet":"\\\\globalfuture"},RQ=s(e=>{var t=e.text;if(/^(?:[\\{}$&#^_]|EOF)$/.test(t))throw new Nt("Expected a control sequence",e);return t},"checkControlSequence"),KVe=s(e=>{var t=e.gullet.popToken();return t.text==="="&&(t=e.gullet.popToken(),t.text===" "&&(t=e.gullet.popToken())),t},"getRHS"),_Q=s((e,t,r,n)=>{var i=e.gullet.macros.get(r.text);i==null&&(r.noexpand=!0,i={tokens:[r],numArgs:0,unexpandable:!e.gullet.isExpandable(r.text)}),e.gullet.macros.set(t,i,n)},"letCommand");jt({type:"internal",names:["\\global","\\long","\\\\globallong"],props:{numArgs:0,allowedInText:!0},handler(e){var{parser:t,funcName:r}=e;t.consumeSpaces();var n=t.fetch();if(C7[n.text])return(r==="\\global"||r==="\\\\globallong")&&(n.text=C7[n.text]),Kr(t.parseFunction(),"internal");throw new Nt("Invalid token after macro prefix",n)}});jt({type:"internal",names:["\\def","\\gdef","\\edef","\\xdef"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(e){var{parser:t,funcName:r}=e,n=t.gullet.popToken(),i=n.text;if(/^(?:[\\{}$&#^_]|EOF)$/.test(i))throw new Nt("Expected a control sequence",n);for(var a=0,o,l=[[]];t.gullet.future().text!=="{";)if(n=t.gullet.popToken(),n.text==="#"){if(t.gullet.future().text==="{"){o=t.gullet.future(),l[a].push("{");break}if(n=t.gullet.popToken(),!/^[1-9]$/.test(n.text))throw new Nt('Invalid argument number "'+n.text+'"');if(parseInt(n.text)!==a+1)throw new Nt('Argument number "'+n.text+'" out of order');a++,l.push([])}else{if(n.text==="EOF")throw new Nt("Expected a macro definition");l[a].push(n.text)}var{tokens:u}=t.gullet.consumeArg();return o&&u.unshift(o),(r==="\\edef"||r==="\\xdef")&&(u=t.gullet.expandTokens(u),u.reverse()),t.gullet.macros.set(i,{tokens:u,numArgs:a,delimiters:l},r===C7[r]),{type:"internal",mode:t.mode}}});jt({type:"internal",names:["\\let","\\\\globallet"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(e){var{parser:t,funcName:r}=e,n=RQ(t.gullet.popToken());t.gullet.consumeSpaces();var i=KVe(t);return _Q(t,n,i,r==="\\\\globallet"),{type:"internal",mode:t.mode}}});jt({type:"internal",names:["\\futurelet","\\\\globalfuture"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(e){var{parser:t,funcName:r}=e,n=RQ(t.gullet.popToken()),i=t.gullet.popToken(),a=t.gullet.popToken();return _Q(t,n,a,r==="\\\\globalfuture"),t.gullet.pushToken(a),t.gullet.pushToken(i),{type:"internal",mode:t.mode}}});Ux=s(function(t,r,n){var i=pi.math[t]&&pi.math[t].replace,a=D7(i||t,r,n);if(!a)throw new Error("Unsupported symbol "+t+" and font size "+r+".");return a},"getMetrics"),B7=s(function(t,r,n,i){var a=n.havingBaseStyle(r),o=It(i.concat(a.sizingClasses(n)),[t],n),l=a.sizeMultiplier/n.sizeMultiplier;return o.height*=l,o.depth*=l,o.maxFontSize=a.sizeMultiplier,o},"styleWrap"),LQ=s(function(t,r,n){var i=r.havingBaseStyle(n),a=(1-r.sizeMultiplier/i.sizeMultiplier)*r.fontMetrics().axisHeight;t.classes.push("delimcenter"),t.style.top=Ft(a),t.height-=a,t.depth+=a},"centerSpan"),ZVe=s(function(t,r,n,i,a,o){var l=Ls(t,"Main-Regular",a,i),u=B7(l,r,i,o);return n&&LQ(u,i,r),u},"makeSmallDelim"),QVe=s(function(t,r,n,i){return Ls(t,"Size"+r+"-Regular",n,i)},"mathrmSize"),DQ=s(function(t,r,n,i,a,o){var l=QVe(t,r,a,i),u=B7(It(["delimsizing","size"+r],[l],i),Vr.TEXT,i,o);return n&&LQ(u,i,Vr.TEXT),u},"makeLargeDelim"),JD=s(function(t,r,n){var i;r==="Size1-Regular"?i="delim-size1":i="delim-size4";var a=It(["delimsizinginner",i],[It([],[Ls(t,r,n)])]);return{type:"elem",elem:a}},"makeGlyphSpan"),e7=s(function(t,r,n){var i=Bc["Size4-Regular"][t.charCodeAt(0)]?Bc["Size4-Regular"][t.charCodeAt(0)][4]:Bc["Size1-Regular"][t.charCodeAt(0)][4],a=new $c("inner",cVe(t,Math.round(1e3*r))),o=new ql([a],{width:Ft(i),height:Ft(r),style:"width:"+Ft(i),viewBox:"0 0 "+1e3*i+" "+Math.round(1e3*r),preserveAspectRatio:"xMinYMin"}),l=Id([],[o],n);return l.height=r,l.style.height=Ft(r),l.style.width=Ft(i),{type:"elem",elem:l}},"makeInner"),S7=.008,Ow={type:"kern",size:-1*S7},JVe=new Set(["|","\\lvert","\\rvert","\\vert"]),eWe=new Set(["\\|","\\lVert","\\rVert","\\Vert"]),IQ=s(function(t,r,n,i,a,o){var l,u,h,d,f="",p=0;l=h=d=t,u=null;var m="Size1-Regular";t==="\\uparrow"?h=d="\u23D0":t==="\\Uparrow"?h=d="\u2016":t==="\\downarrow"?l=h="\u23D0":t==="\\Downarrow"?l=h="\u2016":t==="\\updownarrow"?(l="\\uparrow",h="\u23D0",d="\\downarrow"):t==="\\Updownarrow"?(l="\\Uparrow",h="\u2016",d="\\Downarrow"):JVe.has(t)?(h="\u2223",f="vert",p=333):eWe.has(t)?(h="\u2225",f="doublevert",p=556):t==="["||t==="\\lbrack"?(l="\u23A1",h="\u23A2",d="\u23A3",m="Size4-Regular",f="lbrack",p=667):t==="]"||t==="\\rbrack"?(l="\u23A4",h="\u23A5",d="\u23A6",m="Size4-Regular",f="rbrack",p=667):t==="\\lfloor"||t==="\u230A"?(h=l="\u23A2",d="\u23A3",m="Size4-Regular",f="lfloor",p=667):t==="\\lceil"||t==="\u2308"?(l="\u23A1",h=d="\u23A2",m="Size4-Regular",f="lceil",p=667):t==="\\rfloor"||t==="\u230B"?(h=l="\u23A5",d="\u23A6",m="Size4-Regular",f="rfloor",p=667):t==="\\rceil"||t==="\u2309"?(l="\u23A4",h=d="\u23A5",m="Size4-Regular",f="rceil",p=667):t==="("||t==="\\lparen"?(l="\u239B",h="\u239C",d="\u239D",m="Size4-Regular",f="lparen",p=875):t===")"||t==="\\rparen"?(l="\u239E",h="\u239F",d="\u23A0",m="Size4-Regular",f="rparen",p=875):t==="\\{"||t==="\\lbrace"?(l="\u23A7",u="\u23A8",d="\u23A9",h="\u23AA",m="Size4-Regular"):t==="\\}"||t==="\\rbrace"?(l="\u23AB",u="\u23AC",d="\u23AD",h="\u23AA",m="Size4-Regular"):t==="\\lgroup"||t==="\u27EE"?(l="\u23A7",d="\u23A9",h="\u23AA",m="Size4-Regular"):t==="\\rgroup"||t==="\u27EF"?(l="\u23AB",d="\u23AD",h="\u23AA",m="Size4-Regular"):t==="\\lmoustache"||t==="\u23B0"?(l="\u23A7",d="\u23AD",h="\u23AA",m="Size4-Regular"):(t==="\\rmoustache"||t==="\u23B1")&&(l="\u23AB",d="\u23A9",h="\u23AA",m="Size4-Regular");var g=Ux(l,m,a),y=g.height+g.depth,v=Ux(h,m,a),x=v.height+v.depth,b=Ux(d,m,a),T=b.height+b.depth,w=0,C=1;if(u!==null){var S=Ux(u,m,a);w=S.height+S.depth,C=2}var k=y+T+w,R=Math.max(0,Math.ceil((r-k)/(C*x))),M=k+R*C*x,P=i.fontMetrics().axisHeight;n&&(P*=i.sizeMultiplier);var N=M/2-P,_=[];if(f.length>0){var A=M-y-T,I=Math.round(M*1e3),D=uVe(f,Math.round(A*1e3)),L=new $c(f,D),B=Ft(p/1e3),O=Ft(I/1e3),$=new ql([L],{width:B,height:O,viewBox:"0 0 "+p+" "+I}),W=Id([],[$],i);W.height=I/1e3,W.style.width=B,W.style.height=O,_.push({type:"elem",elem:W})}else{if(_.push(JD(d,m,a)),_.push(Ow),u===null){var V=M-y-T+2*S7;_.push(e7(h,V,i))}else{var z=(M-y-T-w)/2+2*S7;_.push(e7(h,z,i)),_.push(Ow),_.push(JD(u,m,a)),_.push(Ow),_.push(e7(h,z,i))}_.push(Ow),_.push(JD(l,m,a))}var H=i.havingBaseStyle(Vr.TEXT),K=An({positionType:"bottom",positionData:N,children:_});return B7(It(["delimsizing","mult"],[K],H),Vr.TEXT,i,o)},"makeStackedDelim"),t7=80,r7=.08,n7=s(function(t,r,n,i,a){var o=lVe(t,i,n),l=new $c(t,o),u=new ql([l],{width:"400em",height:Ft(r),viewBox:"0 0 400000 "+n,preserveAspectRatio:"xMinYMin slice"});return Id(["hide-tail"],[u],a)},"sqrtSvg"),tWe=s(function(t,r){var n=r.havingBaseSizing(),i=BQ("\\surd",t*n.sizeMultiplier,OQ,n),a=n.sizeMultiplier,o=Math.max(0,r.minRuleThickness-r.fontMetrics().sqrtRuleThickness),l,u,h,d,f;return i.type==="small"?(d=1e3+1e3*o+t7,t<1?a=1:t<1.4&&(a=.7),u=(1+o+r7)/a,h=(1+o)/a,l=n7("sqrtMain",u,d,o,r),l.style.minWidth="0.853em",f=.833/a):i.type==="large"?(d=(1e3+t7)*Yx[i.size],h=(Yx[i.size]+o)/a,u=(Yx[i.size]+o+r7)/a,l=n7("sqrtSize"+i.size,u,d,o,r),l.style.minWidth="1.02em",f=1/a):(u=t+o+r7,h=t+o,d=Math.floor(1e3*t+o)+t7,l=n7("sqrtTall",u,d,o,r),l.style.minWidth="0.742em",f=1.056),l.height=h,l.style.height=Ft(u),{span:l,advanceWidth:f,ruleWidth:(r.fontMetrics().sqrtRuleThickness+o)*a}},"makeSqrtImage"),MQ=new Set(["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230A","\u230B","\\lceil","\\rceil","\u2308","\u2309","\\surd"]),rWe=new Set(["\\uparrow","\\downarrow","\\updownarrow","\\Uparrow","\\Downarrow","\\Updownarrow","|","\\|","\\vert","\\Vert","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27EE","\u27EF","\\lmoustache","\\rmoustache","\u23B0","\u23B1"]),NQ=new Set(["<",">","\\langle","\\rangle","/","\\backslash","\\lt","\\gt"]),Yx=[0,1.2,1.8,2.4,3],PQ=s(function(t,r,n,i,a){if(t==="<"||t==="\\lt"||t==="\u27E8"?t="\\langle":(t===">"||t==="\\gt"||t==="\u27E9")&&(t="\\rangle"),MQ.has(t)||NQ.has(t))return DQ(t,r,!1,n,i,a);if(rWe.has(t))return IQ(t,Yx[r],!1,n,i,a);throw new Nt("Illegal delimiter: '"+t+"'")},"makeSizedDelim"),nWe=[{type:"small",style:Vr.SCRIPTSCRIPT},{type:"small",style:Vr.SCRIPT},{type:"small",style:Vr.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4}],iWe=[{type:"small",style:Vr.SCRIPTSCRIPT},{type:"small",style:Vr.SCRIPT},{type:"small",style:Vr.TEXT},{type:"stack"}],OQ=[{type:"small",style:Vr.SCRIPTSCRIPT},{type:"small",style:Vr.SCRIPT},{type:"small",style:Vr.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4},{type:"stack"}],aWe=s(function(t){if(t.type==="small")return"Main-Regular";if(t.type==="large")return"Size"+t.size+"-Regular";if(t.type==="stack")return"Size4-Regular";var r=t.type;throw new Error("Add support for delim type '"+r+"' here.")},"delimTypeToFont"),BQ=s(function(t,r,n,i){for(var a=Math.min(2,3-i.style.size),o=a;or)return l}return n[n.length-1]},"traverseSequence"),w7=s(function(t,r,n,i,a,o){t==="<"||t==="\\lt"||t==="\u27E8"?t="\\langle":(t===">"||t==="\\gt"||t==="\u27E9")&&(t="\\rangle");var l;NQ.has(t)?l=nWe:MQ.has(t)?l=OQ:l=iWe;var u=BQ(t,r,l,i);return u.type==="small"?ZVe(t,u.style,n,i,a,o):u.type==="large"?DQ(t,u.size,n,i,a,o):IQ(t,r,n,i,a,o)},"makeCustomSizedDelim"),i7=s(function(t,r,n,i,a,o){var l=i.fontMetrics().axisHeight*i.sizeMultiplier,u=901,h=5/i.fontMetrics().ptPerEm,d=Math.max(r-l,n+l),f=Math.max(d/500*u,2*d-h);return w7(t,f,!0,i,a,o)},"makeLeftRightDelim"),zZ={"\\bigl":{mclass:"mopen",size:1},"\\Bigl":{mclass:"mopen",size:2},"\\biggl":{mclass:"mopen",size:3},"\\Biggl":{mclass:"mopen",size:4},"\\bigr":{mclass:"mclose",size:1},"\\Bigr":{mclass:"mclose",size:2},"\\biggr":{mclass:"mclose",size:3},"\\Biggr":{mclass:"mclose",size:4},"\\bigm":{mclass:"mrel",size:1},"\\Bigm":{mclass:"mrel",size:2},"\\biggm":{mclass:"mrel",size:3},"\\Biggm":{mclass:"mrel",size:4},"\\big":{mclass:"mord",size:1},"\\Big":{mclass:"mord",size:2},"\\bigg":{mclass:"mord",size:3},"\\Bigg":{mclass:"mord",size:4}},sWe=new Set(["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230A","\u230B","\\lceil","\\rceil","\u2308","\u2309","<",">","\\langle","\u27E8","\\rangle","\u27E9","\\lt","\\gt","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27EE","\u27EF","\\lmoustache","\\rmoustache","\u23B0","\u23B1","/","\\backslash","|","\\vert","\\|","\\Vert","\\uparrow","\\Uparrow","\\downarrow","\\Downarrow","\\updownarrow","\\Updownarrow","."]);s(VZ,"isMiddleDelimNode");s(nk,"checkDelimiter");jt({type:"delimsizing",names:["\\bigl","\\Bigl","\\biggl","\\Biggl","\\bigr","\\Bigr","\\biggr","\\Biggr","\\bigm","\\Bigm","\\biggm","\\Biggm","\\big","\\Big","\\bigg","\\Bigg"],props:{numArgs:1,argTypes:["primitive"]},handler:s((e,t)=>{var r=nk(t[0],e);return{type:"delimsizing",mode:e.parser.mode,size:zZ[e.funcName].size,mclass:zZ[e.funcName].mclass,delim:r.text}},"handler"),htmlBuilder:s((e,t)=>e.delim==="."?It([e.mclass]):PQ(e.delim,e.size,t,e.mode,[e.mclass]),"htmlBuilder"),mathmlBuilder:s(e=>{var t=[];e.delim!=="."&&t.push(pl(e.delim,e.mode));var r=new Mt("mo",t);e.mclass==="mopen"||e.mclass==="mclose"?r.setAttribute("fence","true"):r.setAttribute("fence","false"),r.setAttribute("stretchy","true");var n=Ft(Yx[e.size]);return r.setAttribute("minsize",n),r.setAttribute("maxsize",n),r},"mathmlBuilder")});s(WZ,"assertParsed");jt({type:"leftright-right",names:["\\right"],props:{numArgs:1,primitive:!0},handler:s((e,t)=>{var r=e.parser.gullet.macros.get("\\current@color");if(r&&typeof r!="string")throw new Nt("\\current@color set to non-string in \\right");return{type:"leftright-right",mode:e.parser.mode,delim:nk(t[0],e).text,color:r}},"handler")});jt({type:"leftright",names:["\\left"],props:{numArgs:1,primitive:!0},handler:s((e,t)=>{var r=nk(t[0],e),n=e.parser;++n.leftrightDepth;var i=n.parseExpression(!1);--n.leftrightDepth,n.expect("\\right",!1);var a=Kr(n.parseFunction(),"leftright-right");return{type:"leftright",mode:n.mode,body:i,left:r.text,right:a.delim,rightColor:a.color}},"handler"),htmlBuilder:s((e,t)=>{WZ(e);for(var r=fa(e.body,t,!0,["mopen","mclose"]),n=0,i=0,a=!1,o=0;o{WZ(e);var r=Fo(e.body,t);if(e.left!=="."){var n=new Mt("mo",[pl(e.left,e.mode)]);n.setAttribute("fence","true"),r.unshift(n)}if(e.right!=="."){var i=new Mt("mo",[pl(e.right,e.mode)]);i.setAttribute("fence","true"),e.rightColor&&i.setAttribute("mathcolor",e.rightColor),r.push(i)}return N7(r)},"mathmlBuilder")});jt({type:"middle",names:["\\middle"],props:{numArgs:1,primitive:!0},handler:s((e,t)=>{var r=nk(t[0],e);if(!e.parser.leftrightDepth)throw new Nt("\\middle without preceding \\left",r);return{type:"middle",mode:e.parser.mode,delim:r.text}},"handler"),htmlBuilder:s((e,t)=>{var r;return e.delim==="."?r=Zx(t,[]):(r=PQ(e.delim,1,t,e.mode,[]),r.isMiddle={delim:e.delim,options:t}),r},"htmlBuilder"),mathmlBuilder:s((e,t)=>{var r=e.delim==="\\vert"||e.delim==="|"?pl("|","text"):pl(e.delim,e.mode),n=new Mt("mo",[r]);return n.setAttribute("fence","true"),n.setAttribute("lspace","0.05em"),n.setAttribute("rspace","0.05em"),n},"mathmlBuilder")});ik=s((e,t)=>{var r=K0(Rn(e.body,t),t),n=e.label.slice(1),i=t.sizeMultiplier,a,o,l=sh(e.body);if(n==="sout")a=It(["stretchy","sout"]),a.height=t.fontMetrics().defaultRuleThickness/i,o=-.5*t.fontMetrics().xHeight;else if(n==="phase"){var u=wi({number:.6,unit:"pt"},t),h=wi({number:.35,unit:"ex"},t),d=t.havingBaseSizing();i=i/d.sizeMultiplier;var f=r.height+r.depth+u+h;r.style.paddingLeft=Ft(f/2+u);var p=Math.floor(1e3*f*i),m=sVe(p),g=new ql([new $c("phase",m)],{width:"400em",height:Ft(p/1e3),viewBox:"0 0 400000 "+p,preserveAspectRatio:"xMinYMin slice"});a=Id(["hide-tail"],[g],t),a.style.height=Ft(f),o=r.depth+u+h}else{/cancel/.test(n)?l||r.classes.push("cancel-pad"):n==="angl"?r.classes.push("anglpad"):r.classes.push("boxpad");var y,v,x=0;/box/.test(n)?(x=Math.max(t.fontMetrics().fboxrule,t.minRuleThickness),y=t.fontMetrics().fboxsep+(n==="colorbox"?0:x),v=y):n==="angl"?(x=Math.max(t.fontMetrics().defaultRuleThickness,t.minRuleThickness),y=4*x,v=Math.max(0,.25-r.depth)):(y=l?.2:0,v=y),a=zVe(r,n,y,v,t),/fbox|boxed|fcolorbox/.test(n)?(a.style.borderStyle="solid",a.style.borderWidth=Ft(x)):n==="angl"&&x!==.049&&(a.style.borderTopWidth=Ft(x),a.style.borderRightWidth=Ft(x)),o=r.depth+v,e.backgroundColor&&(a.style.backgroundColor=e.backgroundColor,e.borderColor&&(a.style.borderColor=e.borderColor))}var b;if(e.backgroundColor)b=An({positionType:"individualShift",children:[{type:"elem",elem:a,shift:o},{type:"elem",elem:r,shift:0}]});else{var T=/cancel|phase/.test(n)?["svg-align"]:[];b=An({positionType:"individualShift",children:[{type:"elem",elem:r,shift:0},{type:"elem",elem:a,shift:o,wrapperClasses:T}]})}return/cancel/.test(n)&&(b.height=r.height,b.depth=r.depth),/cancel/.test(n)&&!l?It(["mord","cancel-lap"],[b],t):It(["mord"],[b],t)},"htmlBuilder$7"),ak=s((e,t)=>{var r,n=new Mt(e.label.includes("colorbox")?"mpadded":"menclose",[Yn(e.body,t)]);switch(e.label){case"\\cancel":n.setAttribute("notation","updiagonalstrike");break;case"\\bcancel":n.setAttribute("notation","downdiagonalstrike");break;case"\\phase":n.setAttribute("notation","phasorangle");break;case"\\sout":n.setAttribute("notation","horizontalstrike");break;case"\\fbox":n.setAttribute("notation","box");break;case"\\angl":n.setAttribute("notation","actuarial");break;case"\\fcolorbox":case"\\colorbox":if(r=t.fontMetrics().fboxsep*t.fontMetrics().ptPerEm,n.setAttribute("width","+"+2*r+"pt"),n.setAttribute("height","+"+2*r+"pt"),n.setAttribute("lspace",r+"pt"),n.setAttribute("voffset",r+"pt"),e.label==="\\fcolorbox"){var i=Math.max(t.fontMetrics().fboxrule,t.minRuleThickness);n.setAttribute("style","border: "+Ft(i)+" solid "+e.borderColor)}break;case"\\xcancel":n.setAttribute("notation","updiagonalstrike downdiagonalstrike");break}return e.backgroundColor&&n.setAttribute("mathbackground",e.backgroundColor),n},"mathmlBuilder$6");jt({type:"enclose",names:["\\colorbox"],props:{numArgs:2,allowedInText:!0,argTypes:["color","hbox"]},handler(e,t,r){var{parser:n,funcName:i}=e,a=Kr(t[0],"color-token").color,o=t[1];return{type:"enclose",mode:n.mode,label:i,backgroundColor:a,body:o}},htmlBuilder:ik,mathmlBuilder:ak});jt({type:"enclose",names:["\\fcolorbox"],props:{numArgs:3,allowedInText:!0,argTypes:["color","color","hbox"]},handler(e,t,r){var{parser:n,funcName:i}=e,a=Kr(t[0],"color-token").color,o=Kr(t[1],"color-token").color,l=t[2];return{type:"enclose",mode:n.mode,label:i,backgroundColor:o,borderColor:a,body:l}},htmlBuilder:ik,mathmlBuilder:ak});jt({type:"enclose",names:["\\fbox"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!0},handler(e,t){var{parser:r}=e;return{type:"enclose",mode:r.mode,label:"\\fbox",body:t[0]}}});jt({type:"enclose",names:["\\cancel","\\bcancel","\\xcancel","\\phase"],props:{numArgs:1},handler(e,t){var{parser:r,funcName:n}=e,i=t[0];return{type:"enclose",mode:r.mode,label:n,body:i}},htmlBuilder:ik,mathmlBuilder:ak});jt({type:"enclose",names:["\\sout"],props:{numArgs:1,allowedInText:!0},handler(e,t){var{parser:r,funcName:n}=e;r.mode==="math"&&r.settings.reportNonstrict("mathVsSout","LaTeX's \\sout works only in text mode");var i=t[0];return{type:"enclose",mode:r.mode,label:n,body:i}},htmlBuilder:ik,mathmlBuilder:ak});jt({type:"enclose",names:["\\angl"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!1},handler(e,t){var{parser:r}=e;return{type:"enclose",mode:r.mode,label:"\\angl",body:t[0]}}});$Q={};s(Fc,"defineEnvironment");FQ={};s(ke,"defineMacro");so=class e{static{s(this,"SourceLocation")}constructor(t,r,n){this.lexer=void 0,this.start=void 0,this.end=void 0,this.lexer=t,this.start=r,this.end=n}static range(t,r){return r?!t||!t.loc||!r.loc||t.loc.lexer!==r.loc.lexer?null:new e(t.loc.lexer,t.loc.start,r.loc.end):t&&t.loc}},Bo=class e{static{s(this,"Token")}constructor(t,r){this.text=void 0,this.loc=void 0,this.noexpand=void 0,this.treatAsRelax=void 0,this.text=t,this.loc=r}range(t,r){return new e(r,so.range(this,t))}};s(qZ,"getHLines");sk=s(e=>{var t=e.parser.settings;if(!t.displayMode)throw new Nt("{"+e.envName+"} can be used only in display mode.")},"validateAmsEnvironmentContext"),oWe=new Set(["gather","gather*"]);s($7,"getAutoTag");s(Nd,"parseArray");s(F7,"dCellStyle");Gc=s(function(t,r){var n,i,a=t.body.length,o=t.hLinesBeforeRow,l=0,u=new Array(a),h=[],d=Math.max(r.fontMetrics().arrayRuleWidth,r.minRuleThickness),f=1/r.fontMetrics().ptPerEm,p=5*f;if(t.colSeparationType&&t.colSeparationType==="small"){var m=r.havingStyle(Vr.SCRIPT).sizeMultiplier;p=.2778*(m/r.sizeMultiplier)}var g=t.colSeparationType==="CD"?wi({number:3,unit:"ex"},r):12*f,y=3*f,v=t.arraystretch*g,x=.7*v,b=.3*v,T=0;function w(G){for(var X=0;X0&&(T+=.25),h.push({pos:T,isDashed:G[X]})}for(s(w,"setHLinePos"),w(o[0]),n=0;n0&&(N+=b,kG))for(n=0;n=l)){var te=void 0;if(i>0||t.hskipBeforeAndAfter){var fe,ce;te=(fe=(ce=H)==null?void 0:ce.pregap)!=null?fe:p,te!==0&&(D=It(["arraycolsep"],[]),D.style.width=Ft(te),I.push(D))}var de=[];for(n=0;n0){for(var Ne=X0("hline",r,d),re=X0("hdashline",r,d),ue=[{type:"elem",elem:Ie,shift:0}];h.length>0;){var le=h.pop(),he=le.pos-_;le.isDashed?ue.push({type:"elem",elem:re,shift:he}):ue.push({type:"elem",elem:Ne,shift:he})}Ie=An({positionType:"individualShift",children:ue})}if(B.length===0)return It(["mord"],[Ie],r);var Te=An({positionType:"individualShift",children:B}),oe=It(["tag"],[Te],r);return lh([Ie,oe])},"htmlBuilder"),lWe={c:"center ",l:"left ",r:"right "},zc=s(function(t,r){for(var n=[],i=new Mt("mtd",[],["mtr-glue"]),a=new Mt("mtd",[],["mml-eqn-num"]),o=0;o0){var g=t.cols,y="",v=!1,x=0,b=g.length;g[0].type==="separator"&&(p+="top ",x=1),g[g.length-1].type==="separator"&&(p+="bottom ",b-=1);for(var T=x;T0?"left ":"",p+=M[M.length-1].length>0?"right ":"";for(var P=1;P0&&m&&(v=1),n[g]={type:"align",align:y,pregap:v,postgap:0}}return o.colSeparationType=m?"align":"alignat",o},"alignedHandler");Fc({type:"array",names:["array","darray"],props:{numArgs:1},handler(e,t){var r=tk(t[0]),n=r?[t[0]]:Kr(t[0],"ordgroup").body,i=n.map(function(o){var l=ek(o),u=l.text;if("lcr".includes(u))return{type:"align",align:u};if(u==="|")return{type:"separator",separator:"|"};if(u===":")return{type:"separator",separator:":"};throw new Nt("Unknown column alignment: "+u,o)}),a={cols:i,hskipBeforeAndAfter:!0,maxNumCols:i.length};return Nd(e.parser,a,F7(e.envName))},htmlBuilder:Gc,mathmlBuilder:zc});Fc({type:"array",names:["matrix","pmatrix","bmatrix","Bmatrix","vmatrix","Vmatrix","matrix*","pmatrix*","bmatrix*","Bmatrix*","vmatrix*","Vmatrix*"],props:{numArgs:0},handler(e){var t={matrix:null,pmatrix:["(",")"],bmatrix:["[","]"],Bmatrix:["\\{","\\}"],vmatrix:["|","|"],Vmatrix:["\\Vert","\\Vert"]}[e.envName.replace("*","")],r="c",n={hskipBeforeAndAfter:!1,cols:[{type:"align",align:r}]};if(e.envName.charAt(e.envName.length-1)==="*"){var i=e.parser;if(i.consumeSpaces(),i.fetch().text==="["){if(i.consume(),i.consumeSpaces(),r=i.fetch().text,!"lcr".includes(r))throw new Nt("Expected l or c or r",i.nextToken);i.consume(),i.consumeSpaces(),i.expect("]"),i.consume(),n.cols=[{type:"align",align:r}]}}var a=Nd(e.parser,n,F7(e.envName)),o=Math.max(0,...a.body.map(l=>l.length));return a.cols=new Array(o).fill({type:"align",align:r}),t?{type:"leftright",mode:e.mode,body:[a],left:t[0],right:t[1],rightColor:void 0}:a},htmlBuilder:Gc,mathmlBuilder:zc});Fc({type:"array",names:["smallmatrix"],props:{numArgs:0},handler(e){var t={arraystretch:.5},r=Nd(e.parser,t,"script");return r.colSeparationType="small",r},htmlBuilder:Gc,mathmlBuilder:zc});Fc({type:"array",names:["subarray"],props:{numArgs:1},handler(e,t){var r=tk(t[0]),n=r?[t[0]]:Kr(t[0],"ordgroup").body,i=n.map(function(l){var u=ek(l),h=u.text;if("lc".includes(h))return{type:"align",align:h};throw new Nt("Unknown column alignment: "+h,l)});if(i.length>1)throw new Nt("{subarray} can contain only one column");var a={cols:i,hskipBeforeAndAfter:!1,arraystretch:.5},o=Nd(e.parser,a,"script");if(o.body.length>0&&o.body[0].length>1)throw new Nt("{subarray} can contain only one column");return o},htmlBuilder:Gc,mathmlBuilder:zc});Fc({type:"array",names:["cases","dcases","rcases","drcases"],props:{numArgs:0},handler(e){var t={arraystretch:1.2,cols:[{type:"align",align:"l",pregap:0,postgap:1},{type:"align",align:"l",pregap:0,postgap:0}]},r=Nd(e.parser,t,F7(e.envName));return{type:"leftright",mode:e.mode,body:[r],left:e.envName.includes("r")?".":"\\{",right:e.envName.includes("r")?"\\}":".",rightColor:void 0}},htmlBuilder:Gc,mathmlBuilder:zc});Fc({type:"array",names:["align","align*","aligned","split"],props:{numArgs:0},handler:GQ,htmlBuilder:Gc,mathmlBuilder:zc});Fc({type:"array",names:["gathered","gather","gather*"],props:{numArgs:0},handler(e){oWe.has(e.envName)&&sk(e);var t={cols:[{type:"align",align:"c"}],addJot:!0,colSeparationType:"gather",autoTag:$7(e.envName),emptySingleRow:!0,leqno:e.parser.settings.leqno};return Nd(e.parser,t,"display")},htmlBuilder:Gc,mathmlBuilder:zc});Fc({type:"array",names:["alignat","alignat*","alignedat"],props:{numArgs:1},handler:GQ,htmlBuilder:Gc,mathmlBuilder:zc});Fc({type:"array",names:["equation","equation*"],props:{numArgs:0},handler(e){sk(e);var t={autoTag:$7(e.envName),emptySingleRow:!0,singleRow:!0,maxNumCols:1,leqno:e.parser.settings.leqno};return Nd(e.parser,t,"display")},htmlBuilder:Gc,mathmlBuilder:zc});Fc({type:"array",names:["CD"],props:{numArgs:0},handler(e){return sk(e),XVe(e.parser)},htmlBuilder:Gc,mathmlBuilder:zc});ke("\\nonumber","\\gdef\\@eqnsw{0}");ke("\\notag","\\nonumber");jt({type:"text",names:["\\hline","\\hdashline"],props:{numArgs:0,allowedInText:!0,allowedInMath:!0},handler(e,t){throw new Nt(e.funcName+" valid only within array environment")}});HZ=$Q;jt({type:"environment",names:["\\begin","\\end"],props:{numArgs:1,argTypes:["text"]},handler(e,t){var{parser:r,funcName:n}=e,i=t[0];if(i.type!=="ordgroup")throw new Nt("Invalid environment name",i);for(var a="",o=0;o{var r=e.font,n=t.withFont(r);return Rn(e.body,n)},"htmlBuilder$5"),VQ=s((e,t)=>{var r=e.font,n=t.withFont(r);return Yn(e.body,n)},"mathmlBuilder$4"),UZ={"\\Bbb":"\\mathbb","\\bold":"\\mathbf","\\frak":"\\mathfrak"};jt({type:"font",names:["\\mathrm","\\mathit","\\mathbf","\\mathnormal","\\mathsfit","\\mathbb","\\mathcal","\\mathfrak","\\mathscr","\\mathsf","\\mathtt","\\Bbb","\\bold","\\frak"],props:{numArgs:1,allowedInArgument:!0},handler:s((e,t)=>{var{parser:r,funcName:n}=e,i=Hw(t[0]),a=n;return a in UZ&&(a=UZ[a]),{type:"font",mode:r.mode,font:a.slice(1),body:i}},"handler"),htmlBuilder:zQ,mathmlBuilder:VQ});jt({type:"mclass",names:["\\boldsymbol","\\bm"],props:{numArgs:1},handler:s((e,t)=>{var{parser:r}=e,n=t[0];return{type:"mclass",mode:r.mode,mclass:rk(n),body:[{type:"font",mode:r.mode,font:"boldsymbol",body:n}],isCharacterBox:sh(n)}},"handler")});jt({type:"font",names:["\\rm","\\sf","\\tt","\\bf","\\it","\\cal"],props:{numArgs:0,allowedInText:!0},handler:s((e,t)=>{var{parser:r,funcName:n,breakOnTokenText:i}=e,{mode:a}=r,o=r.parseExpression(!0,i);return{type:"font",mode:a,font:"math"+n.slice(1),body:{type:"ordgroup",mode:r.mode,body:o}}},"handler"),htmlBuilder:zQ,mathmlBuilder:VQ});cWe=s((e,t)=>{var r=t.style,n=r.fracNum(),i=r.fracDen(),a;a=t.havingStyle(n);var o=Rn(e.numer,a,t);if(e.continued){var l=8.5/t.fontMetrics().ptPerEm,u=3.5/t.fontMetrics().ptPerEm;o.height=o.height0?g=3*p:g=7*p,y=t.fontMetrics().denom1):(f>0?(m=t.fontMetrics().num2,g=p):(m=t.fontMetrics().num3,g=3*p),y=t.fontMetrics().denom2);var v;if(d){var b=t.fontMetrics().axisHeight;m-o.depth-(b+.5*f){var r=new Mt("mfrac",[Yn(e.numer,t),Yn(e.denom,t)]);if(!e.hasBarLine)r.setAttribute("linethickness","0px");else if(e.barSize){var n=wi(e.barSize,t);r.setAttribute("linethickness",Ft(n))}if(e.leftDelim!=null||e.rightDelim!=null){var i=[];if(e.leftDelim!=null){var a=new Mt("mo",[new Di(e.leftDelim.replace("\\",""))]);a.setAttribute("fence","true"),i.push(a)}if(i.push(r),e.rightDelim!=null){var o=new Mt("mo",[new Di(e.rightDelim.replace("\\",""))]);o.setAttribute("fence","true"),i.push(o)}return N7(i)}return r},"mathmlBuilder$3"),WQ=s((e,t)=>{if(!t)return e;var r={type:"styling",mode:e.mode,style:t,body:[e]};return r},"wrapWithStyle");jt({type:"genfrac",names:["\\cfrac","\\dfrac","\\frac","\\tfrac","\\dbinom","\\binom","\\tbinom","\\\\atopfrac","\\\\bracefrac","\\\\brackfrac"],props:{numArgs:2,allowedInArgument:!0},handler:s((e,t)=>{var{parser:r,funcName:n}=e,i=t[0],a=t[1],o,l=null,u=null;switch(n){case"\\cfrac":case"\\dfrac":case"\\frac":case"\\tfrac":o=!0;break;case"\\\\atopfrac":o=!1;break;case"\\dbinom":case"\\binom":case"\\tbinom":o=!1,l="(",u=")";break;case"\\\\bracefrac":o=!1,l="\\{",u="\\}";break;case"\\\\brackfrac":o=!1,l="[",u="]";break;default:throw new Error("Unrecognized genfrac command")}var h=n==="\\cfrac",d=null;return h||n.startsWith("\\d")?d="display":n.startsWith("\\t")&&(d="text"),WQ({type:"genfrac",mode:r.mode,numer:i,denom:a,continued:h,hasBarLine:o,leftDelim:l,rightDelim:u,barSize:null},d)},"handler"),htmlBuilder:cWe,mathmlBuilder:uWe});jt({type:"infix",names:["\\over","\\choose","\\atop","\\brace","\\brack"],props:{numArgs:0,infix:!0},handler(e){var{parser:t,funcName:r,token:n}=e,i;switch(r){case"\\over":i="\\frac";break;case"\\choose":i="\\binom";break;case"\\atop":i="\\\\atopfrac";break;case"\\brace":i="\\\\bracefrac";break;case"\\brack":i="\\\\brackfrac";break;default:throw new Error("Unrecognized infix genfrac command")}return{type:"infix",mode:t.mode,replaceWith:i,token:n}}});YZ=["display","text","script","scriptscript"],jZ=s(function(t){var r=null;return t.length>0&&(r=t,r=r==="."?null:r),r},"delimFromValue");jt({type:"genfrac",names:["\\genfrac"],props:{numArgs:6,allowedInArgument:!0,argTypes:["math","math","size","text","math","math"]},handler(e,t){var{parser:r}=e,n=t[4],i=t[5],a=Hw(t[0]),o=a.type==="atom"&&a.family==="open"?jZ(a.text):null,l=Hw(t[1]),u=l.type==="atom"&&l.family==="close"?jZ(l.text):null,h=Kr(t[2],"size"),d,f=null;h.isBlank?d=!0:(f=h.value,d=f.number>0);var p=null,m=t[3];if(m.type==="ordgroup"){if(m.body.length>0){var g=Kr(m.body[0],"textord");p=YZ[Number(g.text)]}}else m=Kr(m,"textord"),p=YZ[Number(m.text)];return WQ({type:"genfrac",mode:r.mode,numer:n,denom:i,continued:!1,hasBarLine:d,barSize:f,leftDelim:o,rightDelim:u},p)}});jt({type:"infix",names:["\\above"],props:{numArgs:1,argTypes:["size"],infix:!0},handler(e,t){var{parser:r,funcName:n,token:i}=e;return{type:"infix",mode:r.mode,replaceWith:"\\\\abovefrac",size:Kr(t[0],"size").value,token:i}}});jt({type:"genfrac",names:["\\\\abovefrac"],props:{numArgs:3,argTypes:["math","size","math"]},handler:s((e,t)=>{var{parser:r,funcName:n}=e,i=t[0],a=Kr(t[1],"infix").size;if(!a)throw new Error("\\\\abovefrac expected size, but got "+String(a));var o=t[2],l=a.number>0;return{type:"genfrac",mode:r.mode,numer:i,denom:o,continued:!1,hasBarLine:l,barSize:a,leftDelim:null,rightDelim:null}},"handler")});qQ=s((e,t)=>{var r=t.style,n,i;e.type==="supsub"?(n=e.sup?Rn(e.sup,t.havingStyle(r.sup()),t):Rn(e.sub,t.havingStyle(r.sub()),t),i=Kr(e.base,"horizBrace")):i=Kr(e,"horizBrace");var a=Rn(i.base,t.havingBaseStyle(Vr.DISPLAY)),o=Jw(i,t),l;if(i.isOver?l=An({positionType:"firstBaseline",children:[{type:"elem",elem:a},{type:"kern",size:.1},{type:"elem",elem:o,wrapperClasses:["svg-align"]}]}):l=An({positionType:"bottom",positionData:a.depth+.1+o.height,children:[{type:"elem",elem:o,wrapperClasses:["svg-align"]},{type:"kern",size:.1},{type:"elem",elem:a}]}),n){var u=It(["minner",i.isOver?"mover":"munder"],[l],t);i.isOver?l=An({positionType:"firstBaseline",children:[{type:"elem",elem:u},{type:"kern",size:.2},{type:"elem",elem:n}]}):l=An({positionType:"bottom",positionData:u.depth+.2+n.height+n.depth,children:[{type:"elem",elem:n},{type:"kern",size:.2},{type:"elem",elem:u}]})}return It(["minner",i.isOver?"mover":"munder"],[l],t)},"htmlBuilder$3"),hWe=s((e,t)=>{var r=Qw(e.label);return new Mt(e.isOver?"mover":"munder",[Yn(e.base,t),r])},"mathmlBuilder$2");jt({type:"horizBrace",names:["\\overbrace","\\underbrace","\\overbracket","\\underbracket"],props:{numArgs:1},handler(e,t){var{parser:r,funcName:n}=e;return{type:"horizBrace",mode:r.mode,label:n,isOver:n.includes("\\over"),base:t[0]}},htmlBuilder:qQ,mathmlBuilder:hWe});jt({type:"href",names:["\\href"],props:{numArgs:2,argTypes:["url","original"],allowedInText:!0},handler:s((e,t)=>{var{parser:r}=e,n=t[1],i=Kr(t[0],"url").url;return r.settings.isTrusted({command:"\\href",url:i})?{type:"href",mode:r.mode,href:i,body:ea(n)}:r.formatUnsupportedCmd("\\href")},"handler"),htmlBuilder:s((e,t)=>{var r=fa(e.body,t,!1);return wVe(e.href,[],r,t)},"htmlBuilder"),mathmlBuilder:s((e,t)=>{var r=Md(e.body,t);return r instanceof Mt||(r=new Mt("mrow",[r])),r.setAttribute("href",e.href),r},"mathmlBuilder")});jt({type:"href",names:["\\url"],props:{numArgs:1,argTypes:["url"],allowedInText:!0},handler:s((e,t)=>{var{parser:r}=e,n=Kr(t[0],"url").url;if(!r.settings.isTrusted({command:"\\url",url:n}))return r.formatUnsupportedCmd("\\url");for(var i=[],a=0;a{var{parser:r,funcName:n,token:i}=e,a=Kr(t[0],"raw").string,o=t[1];r.settings.strict&&r.settings.reportNonstrict("htmlExtension","HTML extension is disabled on strict mode");var l,u={};switch(n){case"\\htmlClass":u.class=a,l={command:"\\htmlClass",class:a};break;case"\\htmlId":u.id=a,l={command:"\\htmlId",id:a};break;case"\\htmlStyle":u.style=a,l={command:"\\htmlStyle",style:a};break;case"\\htmlData":{for(var h=a.split(","),d=0;d{var r=fa(e.body,t,!1),n=["enclosing"];e.attributes.class&&n.push(...e.attributes.class.trim().split(/\s+/));var i=It(n,r,t);for(var a in e.attributes)a!=="class"&&e.attributes.hasOwnProperty(a)&&i.setAttribute(a,e.attributes[a]);return i},"htmlBuilder"),mathmlBuilder:s((e,t)=>Md(e.body,t),"mathmlBuilder")});jt({type:"htmlmathml",names:["\\html@mathml"],props:{numArgs:2,allowedInArgument:!0,allowedInText:!0},handler:s((e,t)=>{var{parser:r}=e;return{type:"htmlmathml",mode:r.mode,html:ea(t[0]),mathml:ea(t[1])}},"handler"),htmlBuilder:s((e,t)=>{var r=fa(e.html,t,!1);return lh(r)},"htmlBuilder"),mathmlBuilder:s((e,t)=>Md(e.mathml,t),"mathmlBuilder")});a7=s(function(t){if(/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(t))return{number:+t,unit:"bp"};var r=/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(t);if(!r)throw new Nt("Invalid size: '"+t+"' in \\includegraphics");var n={number:+(r[1]+r[2]),unit:r[3]};if(!oQ(n))throw new Nt("Invalid unit: '"+n.unit+"' in \\includegraphics.");return n},"sizeData");jt({type:"includegraphics",names:["\\includegraphics"],props:{numArgs:1,numOptionalArgs:1,argTypes:["raw","url"],allowedInText:!1},handler:s((e,t,r)=>{var{parser:n}=e,i={number:0,unit:"em"},a={number:.9,unit:"em"},o={number:0,unit:"em"},l="";if(r[0])for(var u=Kr(r[0],"raw").string,h=u.split(","),d=0;d{var r=wi(e.height,t),n=0;e.totalheight.number>0&&(n=wi(e.totalheight,t)-r);var i=0;e.width.number>0&&(i=wi(e.width,t));var a={height:Ft(r+n)};i>0&&(a.width=Ft(i)),n>0&&(a.verticalAlign=Ft(-n));var o=new h7(e.src,e.alt,a);return o.height=r,o.depth=n,o},"htmlBuilder"),mathmlBuilder:s((e,t)=>{var r=new Mt("mglyph",[]);r.setAttribute("alt",e.alt);var n=wi(e.height,t),i=0;if(e.totalheight.number>0&&(i=wi(e.totalheight,t)-n,r.setAttribute("valign",Ft(-i))),r.setAttribute("height",Ft(n+i)),e.width.number>0){var a=wi(e.width,t);r.setAttribute("width",Ft(a))}return r.setAttribute("src",e.src),r},"mathmlBuilder")});jt({type:"kern",names:["\\kern","\\mkern","\\hskip","\\mskip"],props:{numArgs:1,argTypes:["size"],primitive:!0,allowedInText:!0},handler(e,t){var{parser:r,funcName:n}=e,i=Kr(t[0],"size");if(r.settings.strict){var a=n[1]==="m",o=i.value.unit==="mu";a?(o||r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" supports only mu units, "+("not "+i.value.unit+" units")),r.mode!=="math"&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" works only in math mode")):o&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" doesn't support mu units")}return{type:"kern",mode:r.mode,dimension:i.value}},htmlBuilder(e,t){return pQ(e.dimension,t)},mathmlBuilder(e,t){var r=wi(e.dimension,t);return new Uw(r)}});jt({type:"lap",names:["\\mathllap","\\mathrlap","\\mathclap"],props:{numArgs:1,allowedInText:!0},handler:s((e,t)=>{var{parser:r,funcName:n}=e,i=t[0];return{type:"lap",mode:r.mode,alignment:n.slice(5),body:i}},"handler"),htmlBuilder:s((e,t)=>{var r;e.alignment==="clap"?(r=It([],[Rn(e.body,t)]),r=It(["inner"],[r],t)):r=It(["inner"],[Rn(e.body,t)]);var n=It(["fix"],[]),i=It([e.alignment],[r,n],t),a=It(["strut"]);return a.style.height=Ft(i.height+i.depth),i.depth&&(a.style.verticalAlign=Ft(-i.depth)),i.children.unshift(a),i=It(["thinbox"],[i],t),It(["mord","vbox"],[i],t)},"htmlBuilder"),mathmlBuilder:s((e,t)=>{var r=new Mt("mpadded",[Yn(e.body,t)]);if(e.alignment!=="rlap"){var n=e.alignment==="llap"?"-1":"-0.5";r.setAttribute("lspace",n+"width")}return r.setAttribute("width","0px"),r},"mathmlBuilder")});jt({type:"styling",names:["\\(","$"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler(e,t){var{funcName:r,parser:n}=e,i=n.mode;n.switchMode("math");var a=r==="\\("?"\\)":"$",o=n.parseExpression(!1,a);return n.expect(a),n.switchMode(i),{type:"styling",mode:n.mode,style:"text",resetFont:!0,body:o}}});jt({type:"text",names:["\\)","\\]"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler(e,t){throw new Nt("Mismatched "+e.funcName)}});XZ=s((e,t)=>{switch(t.style.size){case Vr.DISPLAY.size:return e.display;case Vr.TEXT.size:return e.text;case Vr.SCRIPT.size:return e.script;case Vr.SCRIPTSCRIPT.size:return e.scriptscript;default:return e.text}},"chooseMathStyle");jt({type:"mathchoice",names:["\\mathchoice"],props:{numArgs:4,primitive:!0},handler:s((e,t)=>{var{parser:r}=e;return{type:"mathchoice",mode:r.mode,display:ea(t[0]),text:ea(t[1]),script:ea(t[2]),scriptscript:ea(t[3])}},"handler"),htmlBuilder:s((e,t)=>{var r=XZ(e,t),n=fa(r,t,!1);return lh(n)},"htmlBuilder"),mathmlBuilder:s((e,t)=>{var r=XZ(e,t);return Md(r,t)},"mathmlBuilder")});HQ=s((e,t,r,n,i,a,o)=>{e=It([],[e]);var l=r&&sh(r),u,h;if(t){var d=Rn(t,n.havingStyle(i.sup()),n);h={elem:d,kern:Math.max(n.fontMetrics().bigOpSpacing1,n.fontMetrics().bigOpSpacing3-d.depth)}}if(r){var f=Rn(r,n.havingStyle(i.sub()),n);u={elem:f,kern:Math.max(n.fontMetrics().bigOpSpacing2,n.fontMetrics().bigOpSpacing4-f.height)}}var p;if(h&&u){var m=n.fontMetrics().bigOpSpacing5+u.elem.height+u.elem.depth+u.kern+e.depth+o;p=An({positionType:"bottom",positionData:m,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:u.elem,marginLeft:Ft(-a)},{type:"kern",size:u.kern},{type:"elem",elem:e},{type:"kern",size:h.kern},{type:"elem",elem:h.elem,marginLeft:Ft(a)},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]})}else if(u){var g=e.height-o;p=An({positionType:"top",positionData:g,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:u.elem,marginLeft:Ft(-a)},{type:"kern",size:u.kern},{type:"elem",elem:e}]})}else if(h){var y=e.depth+o;p=An({positionType:"bottom",positionData:y,children:[{type:"elem",elem:e},{type:"kern",size:h.kern},{type:"elem",elem:h.elem,marginLeft:Ft(a)},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]})}else return e;var v=[p];if(u&&a!==0&&!l){var x=It(["mspace"],[],n);x.style.marginRight=Ft(a),v.unshift(x)}return It(["mop","op-limits"],v,n)},"assembleSupSub"),UQ=new Set(["\\smallint"]),Q0=s((e,t)=>{var r,n,i=!1,a;e.type==="supsub"?(r=e.sup,n=e.sub,a=Kr(e.base,"op"),i=!0):a=Kr(e,"op");var o=t.style,l=!1;o.size===Vr.DISPLAY.size&&a.symbol&&!UQ.has(a.name)&&(l=!0);var u,h;if(a.symbol){var d=l?"Size2-Regular":"Size1-Regular",f="";if((a.name==="\\oiint"||a.name==="\\oiiint")&&(f=a.name.slice(1),a.name=f==="oiint"?"\\iint":"\\iiint"),u=Ls(a.name,d,"math",t,["mop","op-symbol",l?"large-op":"small-op"]),h=u.italic,f.length>0){var p=gQ(f+"Size"+(l?"2":"1"),t);u=An({positionType:"individualShift",children:[{type:"elem",elem:u,shift:0},{type:"elem",elem:p,shift:l?.08:0}]}),a.name="\\"+f,u.classes.unshift("mop"),u.italic=h}}else if(a.body){var m=fa(a.body,t,!0);m.length===1&&m[0]instanceof Is?(u=m[0],u.classes[0]="mop"):u=It(["mop"],m,t)}else{for(var g=[],y=1;y{var r;if(e.symbol)r=new Mt("mo",[pl(e.name,e.mode)]),UQ.has(e.name)&&r.setAttribute("largeop","false");else if(e.body)r=new Mt("mo",Fo(e.body,t));else{r=new Mt("mi",[new Di(e.name.slice(1))]);var n=new Mt("mo",[pl("\u2061","text")]);e.parentIsSupSub?r=new Mt("mrow",[r,n]):r=xQ([r,n])}return r},"mathmlBuilder$1"),dWe={"\u220F":"\\prod","\u2210":"\\coprod","\u2211":"\\sum","\u22C0":"\\bigwedge","\u22C1":"\\bigvee","\u22C2":"\\bigcap","\u22C3":"\\bigcup","\u2A00":"\\bigodot","\u2A01":"\\bigoplus","\u2A02":"\\bigotimes","\u2A04":"\\biguplus","\u2A06":"\\bigsqcup"};jt({type:"op",names:["\\coprod","\\bigvee","\\bigwedge","\\biguplus","\\bigcap","\\bigcup","\\intop","\\prod","\\sum","\\bigotimes","\\bigoplus","\\bigodot","\\bigsqcup","\\smallint","\u220F","\u2210","\u2211","\u22C0","\u22C1","\u22C2","\u22C3","\u2A00","\u2A01","\u2A02","\u2A04","\u2A06"],props:{numArgs:0},handler:s((e,t)=>{var{parser:r,funcName:n}=e,i=n;return i.length===1&&(i=dWe[i]),{type:"op",mode:r.mode,limits:!0,parentIsSupSub:!1,symbol:!0,name:i}},"handler"),htmlBuilder:Q0,mathmlBuilder:Jx});jt({type:"op",names:["\\mathop"],props:{numArgs:1,primitive:!0},handler:s((e,t)=>{var{parser:r}=e,n=t[0];return{type:"op",mode:r.mode,limits:!1,parentIsSupSub:!1,symbol:!1,body:ea(n)}},"handler"),htmlBuilder:Q0,mathmlBuilder:Jx});fWe={"\u222B":"\\int","\u222C":"\\iint","\u222D":"\\iiint","\u222E":"\\oint","\u222F":"\\oiint","\u2230":"\\oiiint"};jt({type:"op",names:["\\arcsin","\\arccos","\\arctan","\\arctg","\\arcctg","\\arg","\\ch","\\cos","\\cosec","\\cosh","\\cot","\\cotg","\\coth","\\csc","\\ctg","\\cth","\\deg","\\dim","\\exp","\\hom","\\ker","\\lg","\\ln","\\log","\\sec","\\sin","\\sinh","\\sh","\\tan","\\tanh","\\tg","\\th"],props:{numArgs:0},handler(e){var{parser:t,funcName:r}=e;return{type:"op",mode:t.mode,limits:!1,parentIsSupSub:!1,symbol:!1,name:r}},htmlBuilder:Q0,mathmlBuilder:Jx});jt({type:"op",names:["\\det","\\gcd","\\inf","\\lim","\\max","\\min","\\Pr","\\sup"],props:{numArgs:0},handler(e){var{parser:t,funcName:r}=e;return{type:"op",mode:t.mode,limits:!0,parentIsSupSub:!1,symbol:!1,name:r}},htmlBuilder:Q0,mathmlBuilder:Jx});jt({type:"op",names:["\\int","\\iint","\\iiint","\\oint","\\oiint","\\oiiint","\u222B","\u222C","\u222D","\u222E","\u222F","\u2230"],props:{numArgs:0,allowedInArgument:!0},handler(e){var{parser:t,funcName:r}=e,n=r;return n.length===1&&(n=fWe[n]),{type:"op",mode:t.mode,limits:!1,parentIsSupSub:!1,symbol:!0,name:n}},htmlBuilder:Q0,mathmlBuilder:Jx});YQ=s((e,t)=>{var r,n,i=!1,a;e.type==="supsub"?(r=e.sup,n=e.sub,a=Kr(e.base,"operatorname"),i=!0):a=Kr(e,"operatorname");var o;if(a.body.length>0){for(var l=a.body.map(f=>{var p="text"in f?f.text:void 0;return typeof p=="string"?{type:"textord",mode:f.mode,text:p}:f}),u=fa(l,t.withFont("mathrm"),!0),h=0;h{for(var r=Fo(e.body,t.withFont("mathrm")),n=!0,i=0;id.toText()).join("");r=[new Di(l)]}var u=new Mt("mi",r);u.setAttribute("mathvariant","normal");var h=new Mt("mo",[pl("\u2061","text")]);return e.parentIsSupSub?new Mt("mrow",[u,h]):xQ([u,h])},"mathmlBuilder");jt({type:"operatorname",names:["\\operatorname@","\\operatornamewithlimits"],props:{numArgs:1},handler:s((e,t)=>{var{parser:r,funcName:n}=e,i=t[0];return{type:"operatorname",mode:r.mode,body:ea(i),alwaysHandleSupSub:n==="\\operatornamewithlimits",limits:!1,parentIsSupSub:!1}},"handler"),htmlBuilder:YQ,mathmlBuilder:pWe});ke("\\operatorname","\\@ifstar\\operatornamewithlimits\\operatorname@");Hp({type:"ordgroup",htmlBuilder(e,t){return e.semisimple?lh(fa(e.body,t,!1)):It(["mord"],fa(e.body,t,!0),t)},mathmlBuilder(e,t){return Md(e.body,t,!0)}});jt({type:"overline",names:["\\overline"],props:{numArgs:1},handler(e,t){var{parser:r}=e,n=t[0];return{type:"overline",mode:r.mode,body:n}},htmlBuilder(e,t){var r=Rn(e.body,t.havingCrampedStyle()),n=X0("overline-line",t),i=t.fontMetrics().defaultRuleThickness,a=An({positionType:"firstBaseline",children:[{type:"elem",elem:r},{type:"kern",size:3*i},{type:"elem",elem:n},{type:"kern",size:i}]});return It(["mord","overline"],[a],t)},mathmlBuilder(e,t){var r=new Mt("mo",[new Di("\u203E")]);r.setAttribute("stretchy","true");var n=new Mt("mover",[Yn(e.body,t),r]);return n.setAttribute("accent","true"),n}});jt({type:"phantom",names:["\\phantom"],props:{numArgs:1,allowedInText:!0},handler:s((e,t)=>{var{parser:r}=e,n=t[0];return{type:"phantom",mode:r.mode,body:ea(n)}},"handler"),htmlBuilder:s((e,t)=>{var r=fa(e.body,t.withPhantom(),!1);return lh(r)},"htmlBuilder"),mathmlBuilder:s((e,t)=>{var r=Fo(e.body,t);return new Mt("mphantom",r)},"mathmlBuilder")});ke("\\hphantom","\\smash{\\phantom{#1}}");jt({type:"vphantom",names:["\\vphantom"],props:{numArgs:1,allowedInText:!0},handler:s((e,t)=>{var{parser:r}=e,n=t[0];return{type:"vphantom",mode:r.mode,body:n}},"handler"),htmlBuilder:s((e,t)=>{var r=It(["inner"],[Rn(e.body,t.withPhantom())]),n=It(["fix"],[]);return It(["mord","rlap"],[r,n],t)},"htmlBuilder"),mathmlBuilder:s((e,t)=>{var r=Fo(ea(e.body),t),n=new Mt("mphantom",r),i=new Mt("mpadded",[n]);return i.setAttribute("width","0px"),i},"mathmlBuilder")});jt({type:"raisebox",names:["\\raisebox"],props:{numArgs:2,argTypes:["size","hbox"],allowedInText:!0},handler(e,t){var{parser:r}=e,n=Kr(t[0],"size").value,i=t[1];return{type:"raisebox",mode:r.mode,dy:n,body:i}},htmlBuilder(e,t){var r=Rn(e.body,t),n=wi(e.dy,t);return An({positionType:"shift",positionData:-n,children:[{type:"elem",elem:r}]})},mathmlBuilder(e,t){var r=new Mt("mpadded",[Yn(e.body,t)]),n=e.dy.number+e.dy.unit;return r.setAttribute("voffset",n),r}});jt({type:"internal",names:["\\relax"],props:{numArgs:0,allowedInText:!0,allowedInArgument:!0},handler(e){var{parser:t}=e;return{type:"internal",mode:t.mode}}});jt({type:"rule",names:["\\rule"],props:{numArgs:2,numOptionalArgs:1,allowedInText:!0,allowedInMath:!0,argTypes:["size","size","size"]},handler(e,t,r){var{parser:n}=e,i=r[0],a=Kr(t[0],"size"),o=Kr(t[1],"size");return{type:"rule",mode:n.mode,shift:i&&Kr(i,"size").value,width:a.value,height:o.value}},htmlBuilder(e,t){var r=It(["mord","rule"],[],t),n=wi(e.width,t),i=wi(e.height,t),a=e.shift?wi(e.shift,t):0;return r.style.borderRightWidth=Ft(n),r.style.borderTopWidth=Ft(i),r.style.bottom=Ft(a),r.width=n,r.height=i+a,r.depth=-a,r.maxFontSize=i*1.125*t.sizeMultiplier,r},mathmlBuilder(e,t){var r=wi(e.width,t),n=wi(e.height,t),i=e.shift?wi(e.shift,t):0,a=t.color&&t.getColor()||"black",o=new Mt("mspace");o.setAttribute("mathbackground",a),o.setAttribute("width",Ft(r)),o.setAttribute("height",Ft(n));var l=new Mt("mpadded",[o]);return i>=0?l.setAttribute("height",Ft(i)):(l.setAttribute("height",Ft(i)),l.setAttribute("depth",Ft(-i))),l.setAttribute("voffset",Ft(i)),l}});s(jQ,"sizingGroup");KZ=["\\tiny","\\sixptsize","\\scriptsize","\\footnotesize","\\small","\\normalsize","\\large","\\Large","\\LARGE","\\huge","\\Huge"],mWe=s((e,t)=>{var r=t.havingSize(e.size);return jQ(e.body,r,t)},"htmlBuilder");jt({type:"sizing",names:KZ,props:{numArgs:0,allowedInText:!0},handler:s((e,t)=>{var{breakOnTokenText:r,funcName:n,parser:i}=e,a=i.parseExpression(!1,r);return{type:"sizing",mode:i.mode,size:KZ.indexOf(n)+1,body:a}},"handler"),htmlBuilder:mWe,mathmlBuilder:s((e,t)=>{var r=t.havingSize(e.size),n=Fo(e.body,r),i=new Mt("mstyle",n);return i.setAttribute("mathsize",Ft(r.sizeMultiplier)),i},"mathmlBuilder")});jt({type:"smash",names:["\\smash"],props:{numArgs:1,numOptionalArgs:1,allowedInText:!0},handler:s((e,t,r)=>{var{parser:n}=e,i=!1,a=!1,o=r[0]&&Kr(r[0],"ordgroup");if(o)for(var l,u=0;u{var r=It([],[Rn(e.body,t)]);if(!e.smashHeight&&!e.smashDepth)return r;if(e.smashHeight&&(r.height=0),e.smashDepth&&(r.depth=0),e.smashHeight&&e.smashDepth)return It(["mord","smash"],[r],t);if(r.children)for(var n=0;n{var r=new Mt("mpadded",[Yn(e.body,t)]);return e.smashHeight&&r.setAttribute("height","0px"),e.smashDepth&&r.setAttribute("depth","0px"),r},"mathmlBuilder")});jt({type:"sqrt",names:["\\sqrt"],props:{numArgs:1,numOptionalArgs:1},handler(e,t,r){var{parser:n}=e,i=r[0],a=t[0];return{type:"sqrt",mode:n.mode,body:a,index:i}},htmlBuilder(e,t){var r=Rn(e.body,t.havingCrampedStyle());r.height===0&&(r.height=t.fontMetrics().xHeight),r=K0(r,t);var n=t.fontMetrics(),i=n.defaultRuleThickness,a=i;t.style.idr.height+r.depth+o&&(o=(o+f-r.height-r.depth)/2);var p=u.height-r.height-o-h;r.style.paddingLeft=Ft(d);var m=An({positionType:"firstBaseline",children:[{type:"elem",elem:r,wrapperClasses:["svg-align"]},{type:"kern",size:-(r.height+p)},{type:"elem",elem:u},{type:"kern",size:h}]});if(e.index){var g=t.havingStyle(Vr.SCRIPTSCRIPT),y=Rn(e.index,g,t),v=.6*(m.height-m.depth),x=An({positionType:"shift",positionData:-v,children:[{type:"elem",elem:y}]}),b=It(["root"],[x]);return It(["mord","sqrt"],[b,m],t)}else return It(["mord","sqrt"],[m],t)},mathmlBuilder(e,t){var{body:r,index:n}=e;return n?new Mt("mroot",[Yn(r,t),Yn(n,t)]):new Mt("msqrt",[Yn(r,t)])}});k7={display:Vr.DISPLAY,text:Vr.TEXT,script:Vr.SCRIPT,scriptscript:Vr.SCRIPTSCRIPT};s(gWe,"isStyleStr");jt({type:"styling",names:["\\displaystyle","\\textstyle","\\scriptstyle","\\scriptscriptstyle"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(e,t){var{breakOnTokenText:r,funcName:n,parser:i}=e,a=i.parseExpression(!0,r),o=n.slice(1,n.length-5);if(!gWe(o))throw new Error("Unknown style: "+o);return{type:"styling",mode:i.mode,style:o,body:a}},htmlBuilder(e,t){var r=k7[e.style],n=t.havingStyle(r);return e.resetFont&&(n=n.withFont("")),jQ(e.body,n,t)},mathmlBuilder(e,t){var r=k7[e.style],n=t.havingStyle(r);e.resetFont&&(n=n.withFont(""));var i=Fo(e.body,n),a=new Mt("mstyle",i),o={display:["0","true"],text:["0","false"],script:["1","false"],scriptscript:["2","false"]},l=o[e.style];return a.setAttribute("scriptlevel",l[0]),a.setAttribute("displaystyle",l[1]),a}});yWe=s(function(t,r){var n=t.base;if(n)if(n.type==="op"){var i=n.limits&&(r.style.size===Vr.DISPLAY.size||n.alwaysHandleSupSub);return i?Q0:null}else if(n.type==="operatorname"){var a=n.alwaysHandleSupSub&&(r.style.size===Vr.DISPLAY.size||n.limits);return a?YQ:null}else{if(n.type==="accent")return sh(n.base)?O7:null;if(n.type==="horizBrace"){var o=!t.sub;return o===n.isOver?qQ:null}else return null}else return null},"htmlBuilderDelegate");Hp({type:"supsub",htmlBuilder(e,t){var r=yWe(e,t);if(r)return r(e,t);var{base:n,sup:i,sub:a}=e,o=Rn(n,t),l,u,h=t.fontMetrics(),d=0,f=0,p=n&&sh(n);if(i){var m=t.havingStyle(t.style.sup());l=Rn(i,m,t),p||(d=o.height-m.fontMetrics().supDrop*m.sizeMultiplier/t.sizeMultiplier)}if(a){var g=t.havingStyle(t.style.sub());u=Rn(a,g,t),p||(f=o.depth+g.fontMetrics().subDrop*g.sizeMultiplier/t.sizeMultiplier)}var y;t.style===Vr.DISPLAY?y=h.sup1:t.style.cramped?y=h.sup3:y=h.sup2;var v=t.sizeMultiplier,x=Ft(.5/h.ptPerEm/v),b=null;if(u){var T=e.base&&e.base.type==="op"&&e.base.name&&(e.base.name==="\\oiint"||e.base.name==="\\oiiint");if(o instanceof Is||T){var w;b=Ft(-((w=o.italic)!=null?w:0))}}var C;if(l&&u){d=Math.max(d,y,l.depth+.25*h.xHeight),f=Math.max(f,h.sub2);var S=h.defaultRuleThickness,k=4*S;if(d-l.depth-(u.height-f)0&&(d+=R,f-=R)}var M=[{type:"elem",elem:u,shift:f,marginRight:x,marginLeft:b},{type:"elem",elem:l,shift:-d,marginRight:x}];C=An({positionType:"individualShift",children:M})}else if(u){f=Math.max(f,h.sub1,u.height-.8*h.xHeight);var P=[{type:"elem",elem:u,marginLeft:b,marginRight:x}];C=An({positionType:"shift",positionData:f,children:P})}else if(l)d=Math.max(d,y,l.depth+.25*h.xHeight),C=An({positionType:"shift",positionData:-d,children:[{type:"elem",elem:l,marginRight:x}]});else throw new Error("supsub must have either sup or sub.");var N=b7(o,"right")||"mord";return It([N],[o,It(["msupsub"],[C])],t)},mathmlBuilder(e,t){var r=!1,n,i;e.base&&e.base.type==="horizBrace"&&(i=!!e.sup,i===e.base.isOver&&(r=!0,n=e.base.isOver)),e.base&&(e.base.type==="op"||e.base.type==="operatorname")&&(e.base.parentIsSupSub=!0);var a=[Yn(e.base,t)];e.sub&&a.push(Yn(e.sub,t)),e.sup&&a.push(Yn(e.sup,t));var o;if(r)o=n?"mover":"munder";else if(e.sub)if(e.sup){var h=e.base;h&&h.type==="op"&&h.limits&&t.style===Vr.DISPLAY||h&&h.type==="operatorname"&&h.alwaysHandleSupSub&&(t.style===Vr.DISPLAY||h.limits)?o="munderover":o="msubsup"}else{var u=e.base;u&&u.type==="op"&&u.limits&&(t.style===Vr.DISPLAY||u.alwaysHandleSupSub)||u&&u.type==="operatorname"&&u.alwaysHandleSupSub&&(u.limits||t.style===Vr.DISPLAY)?o="munder":o="msub"}else{var l=e.base;l&&l.type==="op"&&l.limits&&(t.style===Vr.DISPLAY||l.alwaysHandleSupSub)||l&&l.type==="operatorname"&&l.alwaysHandleSupSub&&(l.limits||t.style===Vr.DISPLAY)?o="mover":o="msup"}return new Mt(o,a)}});Hp({type:"atom",htmlBuilder(e,t){return I7(e.text,e.mode,t,["m"+e.family])},mathmlBuilder(e,t){var r=new Mt("mo",[pl(e.text,e.mode)]);if(e.family==="bin"){var n=P7(e,t);n==="bold-italic"&&r.setAttribute("mathvariant",n)}else e.family==="punct"?r.setAttribute("separator","true"):(e.family==="open"||e.family==="close")&&r.setAttribute("stretchy","false");return r}});XQ={mi:"italic",mn:"normal",mtext:"normal"};Hp({type:"mathord",htmlBuilder(e,t){return Zw(e,t,"mathord")},mathmlBuilder(e,t){var r=new Mt("mi",[pl(e.text,e.mode,t)]),n=P7(e,t)||"italic";return n!==XQ[r.type]&&r.setAttribute("mathvariant",n),r}});Hp({type:"textord",htmlBuilder(e,t){return Zw(e,t,"textord")},mathmlBuilder(e,t){var r=pl(e.text,e.mode,t),n=P7(e,t)||"normal",i;return e.mode==="text"?i=new Mt("mtext",[r]):/[0-9]/.test(e.text)?i=new Mt("mn",[r]):e.text==="\\prime"?i=new Mt("mo",[r]):i=new Mt("mi",[r]),n!==XQ[i.type]&&i.setAttribute("mathvariant",n),i}});s7={"\\nobreak":"nobreak","\\allowbreak":"allowbreak"},o7={" ":{},"\\ ":{},"~":{className:"nobreak"},"\\space":{},"\\nobreakspace":{className:"nobreak"}};Hp({type:"spacing",htmlBuilder(e,t){if(o7.hasOwnProperty(e.text)){var r=o7[e.text].className||"";if(e.mode==="text"){var n=Zw(e,t,"textord");return n.classes.push(r),n}else return It(["mspace",r],[I7(e.text,e.mode,t)],t)}else{if(s7.hasOwnProperty(e.text))return It(["mspace",s7[e.text]],[],t);throw new Nt('Unknown type of space "'+e.text+'"')}},mathmlBuilder(e,t){var r;if(o7.hasOwnProperty(e.text))r=new Mt("mtext",[new Di("\xA0")]);else{if(s7.hasOwnProperty(e.text))return new Mt("mspace");throw new Nt('Unknown type of space "'+e.text+'"')}return r}});ZZ=s(()=>{var e=new Mt("mtd",[]);return e.setAttribute("width","50%"),e},"pad");Hp({type:"tag",mathmlBuilder(e,t){var r=new Mt("mtable",[new Mt("mtr",[ZZ(),new Mt("mtd",[Md(e.body,t)]),ZZ(),new Mt("mtd",[Md(e.tag,t)])])]);return r.setAttribute("width","100%"),r}});QZ={"\\text":void 0,"\\textrm":"textrm","\\textsf":"textsf","\\texttt":"texttt","\\textnormal":"textrm"},JZ={"\\textbf":"textbf","\\textmd":"textmd"},vWe={"\\textit":"textit","\\textup":"textup"},eQ=s((e,t)=>{var r=e.font;if(r){if(QZ[r])return t.withTextFontFamily(QZ[r]);if(JZ[r])return t.withTextFontWeight(JZ[r]);if(r==="\\emph")return t.fontShape==="textit"?t.withTextFontShape("textup"):t.withTextFontShape("textit")}else return t;return t.withTextFontShape(vWe[r])},"optionsWithFont");jt({type:"text",names:["\\text","\\textrm","\\textsf","\\texttt","\\textnormal","\\textbf","\\textmd","\\textit","\\textup","\\emph"],props:{numArgs:1,argTypes:["text"],allowedInArgument:!0,allowedInText:!0},handler(e,t){var{parser:r,funcName:n}=e,i=t[0];return{type:"text",mode:r.mode,body:ea(i),font:n}},htmlBuilder(e,t){var r=eQ(e,t),n=fa(e.body,r,!0);return It(["mord","text"],n,r)},mathmlBuilder(e,t){var r=eQ(e,t);return Md(e.body,r)}});jt({type:"underline",names:["\\underline"],props:{numArgs:1,allowedInText:!0},handler(e,t){var{parser:r}=e;return{type:"underline",mode:r.mode,body:t[0]}},htmlBuilder(e,t){var r=Rn(e.body,t),n=X0("underline-line",t),i=t.fontMetrics().defaultRuleThickness,a=An({positionType:"top",positionData:r.height,children:[{type:"kern",size:i},{type:"elem",elem:n},{type:"kern",size:3*i},{type:"elem",elem:r}]});return It(["mord","underline"],[a],t)},mathmlBuilder(e,t){var r=new Mt("mo",[new Di("\u203E")]);r.setAttribute("stretchy","true");var n=new Mt("munder",[Yn(e.body,t),r]);return n.setAttribute("accentunder","true"),n}});jt({type:"vcenter",names:["\\vcenter"],props:{numArgs:1,argTypes:["original"],allowedInText:!1},handler(e,t){var{parser:r}=e;return{type:"vcenter",mode:r.mode,body:t[0]}},htmlBuilder(e,t){var r=Rn(e.body,t),n=t.fontMetrics().axisHeight,i=.5*(r.height-n-(r.depth+n));return An({positionType:"shift",positionData:i,children:[{type:"elem",elem:r}]})},mathmlBuilder(e,t){var r=new Mt("mpadded",[Yn(e.body,t)],["vcenter"]);return new Mt("mrow",[r])}});jt({type:"verb",names:["\\verb"],props:{numArgs:0,allowedInText:!0},handler(e,t,r){throw new Nt("\\verb ended by end of line instead of matching delimiter")},htmlBuilder(e,t){for(var r=tQ(e),n=[],i=t.havingStyle(t.style.text()),a=0;ae.body.replace(/ /g,e.star?"\u2423":"\xA0"),"makeVerb"),Rd=yQ,KQ=`[ \r + ]`,xWe="\\\\[a-zA-Z@]+",bWe="\\\\[^\uD800-\uDFFF]",TWe="("+xWe+")"+KQ+"*",CWe=`\\\\( +|[ \r ]+ +?)[ \r ]*`,E7="[\u0300-\u036F]",SWe=new RegExp(E7+"+$"),wWe="("+KQ+"+)|"+(CWe+"|")+"([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]"+(E7+"*")+"|[\uD800-\uDBFF][\uDC00-\uDFFF]"+(E7+"*")+"|\\\\verb\\*([^]).*?\\4|\\\\verb([^*a-zA-Z]).*?\\5"+("|"+TWe)+("|"+bWe+")"),jw=class{static{s(this,"Lexer")}constructor(t,r){this.input=void 0,this.settings=void 0,this.tokenRegex=void 0,this.catcodes=void 0,this.input=t,this.settings=r,this.tokenRegex=new RegExp(wWe,"g"),this.catcodes={"%":14,"~":13}}setCatcode(t,r){this.catcodes[t]=r}lex(){var t=this.input,r=this.tokenRegex.lastIndex;if(r===t.length)return new Bo("EOF",new so(this,r,r));var n=this.tokenRegex.exec(t);if(n===null||n.index!==r)throw new Nt("Unexpected character: '"+t[r]+"'",new Bo(t[r],new so(this,r,r+1)));var i=n[6]||n[3]||(n[2]?"\\ ":" ");if(this.catcodes[i]===14){var a=t.indexOf(` +`,this.tokenRegex.lastIndex);return a===-1?(this.tokenRegex.lastIndex=t.length,this.settings.reportNonstrict("commentAtEnd","% comment has no terminating newline; LaTeX would fail because of commenting the end of math mode (e.g. $)")):this.tokenRegex.lastIndex=a+1,this.lex()}return new Bo(i,new so(this,r,this.tokenRegex.lastIndex))}},A7=class{static{s(this,"Namespace")}constructor(t,r){t===void 0&&(t={}),r===void 0&&(r={}),this.current=void 0,this.builtins=void 0,this.undefStack=void 0,this.current=r,this.builtins=t,this.undefStack=[]}beginGroup(){this.undefStack.push({})}endGroup(){if(this.undefStack.length===0)throw new Nt("Unbalanced namespace destruction: attempt to pop global namespace; please report this as a bug");var t=this.undefStack.pop();for(var r in t)t.hasOwnProperty(r)&&(t[r]==null?delete this.current[r]:this.current[r]=t[r])}endGroups(){for(;this.undefStack.length>0;)this.endGroup()}has(t){return this.current.hasOwnProperty(t)||this.builtins.hasOwnProperty(t)}get(t){return this.current.hasOwnProperty(t)?this.current[t]:this.builtins[t]}set(t,r,n){if(n===void 0&&(n=!1),n){for(var i=0;i0&&(this.undefStack[this.undefStack.length-1][t]=r)}else{var a=this.undefStack[this.undefStack.length-1];a&&!a.hasOwnProperty(t)&&(a[t]=this.current[t])}r==null?delete this.current[t]:this.current[t]=r}},kWe=FQ;ke("\\noexpand",function(e){var t=e.popToken();return e.isExpandable(t.text)&&(t.noexpand=!0,t.treatAsRelax=!0),{tokens:[t],numArgs:0}});ke("\\expandafter",function(e){var t=e.popToken();return e.expandOnce(!0),{tokens:[t],numArgs:0}});ke("\\@firstoftwo",function(e){var t=e.consumeArgs(2);return{tokens:t[0],numArgs:0}});ke("\\@secondoftwo",function(e){var t=e.consumeArgs(2);return{tokens:t[1],numArgs:0}});ke("\\@ifnextchar",function(e){var t=e.consumeArgs(3);e.consumeSpaces();var r=e.future();return t[0].length===1&&t[0][0].text===r.text?{tokens:t[1],numArgs:0}:{tokens:t[2],numArgs:0}});ke("\\@ifstar","\\@ifnextchar *{\\@firstoftwo{#1}}");ke("\\TextOrMath",function(e){var t=e.consumeArgs(2);return e.mode==="text"?{tokens:t[0],numArgs:0}:{tokens:t[1],numArgs:0}});rQ={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};ke("\\char",function(e){var t=e.popToken(),r,n=0;if(t.text==="'")r=8,t=e.popToken();else if(t.text==='"')r=16,t=e.popToken();else if(t.text==="`")if(t=e.popToken(),t.text[0]==="\\")n=t.text.charCodeAt(1);else{if(t.text==="EOF")throw new Nt("\\char` missing argument");n=t.text.charCodeAt(0)}else r=10;if(r){if(n=rQ[t.text],n==null||n>=r)throw new Nt("Invalid base-"+r+" digit "+t.text);for(var i;(i=rQ[e.future().text])!=null&&i{var i=e.consumeArg().tokens;if(i.length!==1)throw new Nt("\\newcommand's first argument must be a macro name");var a=i[0].text,o=e.isDefined(a);if(o&&!t)throw new Nt("\\newcommand{"+a+"} attempting to redefine "+(a+"; use \\renewcommand"));if(!o&&!r)throw new Nt("\\renewcommand{"+a+"} when command "+a+" does not yet exist; use \\newcommand");var l=0;if(i=e.consumeArg().tokens,i.length===1&&i[0].text==="["){for(var u="",h=e.expandNextToken();h.text!=="]"&&h.text!=="EOF";)u+=h.text,h=e.expandNextToken();if(!u.match(/^\s*[0-9]+\s*$/))throw new Nt("Invalid number of arguments: "+u);l=parseInt(u),i=e.consumeArg().tokens}return o&&n||e.macros.set(a,{tokens:i,numArgs:l}),""},"newcommand");ke("\\newcommand",e=>G7(e,!1,!0,!1));ke("\\renewcommand",e=>G7(e,!0,!1,!1));ke("\\providecommand",e=>G7(e,!0,!0,!0));ke("\\message",e=>{var t=e.consumeArgs(1)[0];return console.log(t.reverse().map(r=>r.text).join("")),""});ke("\\errmessage",e=>{var t=e.consumeArgs(1)[0];return console.error(t.reverse().map(r=>r.text).join("")),""});ke("\\show",e=>{var t=e.popToken(),r=t.text;return console.log(t,e.macros.get(r),Rd[r],pi.math[r],pi.text[r]),""});ke("\\bgroup","{");ke("\\egroup","}");ke("~","\\nobreakspace");ke("\\lq","`");ke("\\rq","'");ke("\\aa","\\r a");ke("\\AA","\\r A");ke("\\textcopyright","\\html@mathml{\\textcircled{c}}{\\char`\xA9}");ke("\\copyright","\\TextOrMath{\\textcopyright}{\\text{\\textcopyright}}");ke("\\textregistered","\\html@mathml{\\textcircled{\\scriptsize R}}{\\char`\xAE}");ke("\u212C","\\mathscr{B}");ke("\u2130","\\mathscr{E}");ke("\u2131","\\mathscr{F}");ke("\u210B","\\mathscr{H}");ke("\u2110","\\mathscr{I}");ke("\u2112","\\mathscr{L}");ke("\u2133","\\mathscr{M}");ke("\u211B","\\mathscr{R}");ke("\u212D","\\mathfrak{C}");ke("\u210C","\\mathfrak{H}");ke("\u2128","\\mathfrak{Z}");ke("\\Bbbk","\\Bbb{k}");ke("\\llap","\\mathllap{\\textrm{#1}}");ke("\\rlap","\\mathrlap{\\textrm{#1}}");ke("\\clap","\\mathclap{\\textrm{#1}}");ke("\\mathstrut","\\vphantom{(}");ke("\\underbar","\\underline{\\text{#1}}");ke("\\not",'\\html@mathml{\\mathrel{\\mathrlap\\@not}\\nobreak}{\\char"338}');ke("\\neq","\\html@mathml{\\mathrel{\\not=}}{\\mathrel{\\char`\u2260}}");ke("\\ne","\\neq");ke("\u2260","\\neq");ke("\\notin","\\html@mathml{\\mathrel{{\\in}\\mathllap{/\\mskip1mu}}}{\\mathrel{\\char`\u2209}}");ke("\u2209","\\notin");ke("\u2258","\\html@mathml{\\mathrel{=\\kern{-1em}\\raisebox{0.4em}{$\\scriptsize\\frown$}}}{\\mathrel{\\char`\u2258}}");ke("\u2259","\\html@mathml{\\stackrel{\\tiny\\wedge}{=}}{\\mathrel{\\char`\u2258}}");ke("\u225A","\\html@mathml{\\stackrel{\\tiny\\vee}{=}}{\\mathrel{\\char`\u225A}}");ke("\u225B","\\html@mathml{\\stackrel{\\scriptsize\\star}{=}}{\\mathrel{\\char`\u225B}}");ke("\u225D","\\html@mathml{\\stackrel{\\tiny\\mathrm{def}}{=}}{\\mathrel{\\char`\u225D}}");ke("\u225E","\\html@mathml{\\stackrel{\\tiny\\mathrm{m}}{=}}{\\mathrel{\\char`\u225E}}");ke("\u225F","\\html@mathml{\\stackrel{\\tiny?}{=}}{\\mathrel{\\char`\u225F}}");ke("\u27C2","\\perp");ke("\u203C","\\mathclose{!\\mkern-0.8mu!}");ke("\u220C","\\notni");ke("\u231C","\\ulcorner");ke("\u231D","\\urcorner");ke("\u231E","\\llcorner");ke("\u231F","\\lrcorner");ke("\xA9","\\copyright");ke("\xAE","\\textregistered");ke("\\ulcorner",'\\html@mathml{\\@ulcorner}{\\mathop{\\char"231c}}');ke("\\urcorner",'\\html@mathml{\\@urcorner}{\\mathop{\\char"231d}}');ke("\\llcorner",'\\html@mathml{\\@llcorner}{\\mathop{\\char"231e}}');ke("\\lrcorner",'\\html@mathml{\\@lrcorner}{\\mathop{\\char"231f}}');ke("\\vdots","{\\varvdots\\rule{0pt}{15pt}}");ke("\u22EE","\\vdots");ke("\\varGamma","\\mathit{\\Gamma}");ke("\\varDelta","\\mathit{\\Delta}");ke("\\varTheta","\\mathit{\\Theta}");ke("\\varLambda","\\mathit{\\Lambda}");ke("\\varXi","\\mathit{\\Xi}");ke("\\varPi","\\mathit{\\Pi}");ke("\\varSigma","\\mathit{\\Sigma}");ke("\\varUpsilon","\\mathit{\\Upsilon}");ke("\\varPhi","\\mathit{\\Phi}");ke("\\varPsi","\\mathit{\\Psi}");ke("\\varOmega","\\mathit{\\Omega}");ke("\\substack","\\begin{subarray}{c}#1\\end{subarray}");ke("\\colon","\\nobreak\\mskip2mu\\mathpunct{}\\mathchoice{\\mkern-3mu}{\\mkern-3mu}{}{}{:}\\mskip6mu\\relax");ke("\\boxed","\\fbox{$\\displaystyle{#1}$}");ke("\\iff","\\DOTSB\\;\\Longleftrightarrow\\;");ke("\\implies","\\DOTSB\\;\\Longrightarrow\\;");ke("\\impliedby","\\DOTSB\\;\\Longleftarrow\\;");ke("\\dddot","{\\overset{\\raisebox{-0.1ex}{\\normalsize ...}}{#1}}");ke("\\ddddot","{\\overset{\\raisebox{-0.1ex}{\\normalsize ....}}{#1}}");nQ={",":"\\dotsc","\\not":"\\dotsb","+":"\\dotsb","=":"\\dotsb","<":"\\dotsb",">":"\\dotsb","-":"\\dotsb","*":"\\dotsb",":":"\\dotsb","\\DOTSB":"\\dotsb","\\coprod":"\\dotsb","\\bigvee":"\\dotsb","\\bigwedge":"\\dotsb","\\biguplus":"\\dotsb","\\bigcap":"\\dotsb","\\bigcup":"\\dotsb","\\prod":"\\dotsb","\\sum":"\\dotsb","\\bigotimes":"\\dotsb","\\bigoplus":"\\dotsb","\\bigodot":"\\dotsb","\\bigsqcup":"\\dotsb","\\And":"\\dotsb","\\longrightarrow":"\\dotsb","\\Longrightarrow":"\\dotsb","\\longleftarrow":"\\dotsb","\\Longleftarrow":"\\dotsb","\\longleftrightarrow":"\\dotsb","\\Longleftrightarrow":"\\dotsb","\\mapsto":"\\dotsb","\\longmapsto":"\\dotsb","\\hookrightarrow":"\\dotsb","\\doteq":"\\dotsb","\\mathbin":"\\dotsb","\\mathrel":"\\dotsb","\\relbar":"\\dotsb","\\Relbar":"\\dotsb","\\xrightarrow":"\\dotsb","\\xleftarrow":"\\dotsb","\\DOTSI":"\\dotsi","\\int":"\\dotsi","\\oint":"\\dotsi","\\iint":"\\dotsi","\\iiint":"\\dotsi","\\iiiint":"\\dotsi","\\idotsint":"\\dotsi","\\DOTSX":"\\dotsx"},EWe=new Set(["bin","rel"]);ke("\\dots",function(e){var t="\\dotso",r=e.expandAfterFuture().text;return r in nQ?t=nQ[r]:(r.slice(0,4)==="\\not"||r in pi.math&&EWe.has(pi.math[r].group))&&(t="\\dotsb"),t});z7={")":!0,"]":!0,"\\rbrack":!0,"\\}":!0,"\\rbrace":!0,"\\rangle":!0,"\\rceil":!0,"\\rfloor":!0,"\\rgroup":!0,"\\rmoustache":!0,"\\right":!0,"\\bigr":!0,"\\biggr":!0,"\\Bigr":!0,"\\Biggr":!0,$:!0,";":!0,".":!0,",":!0};ke("\\dotso",function(e){var t=e.future().text;return t in z7?"\\ldots\\,":"\\ldots"});ke("\\dotsc",function(e){var t=e.future().text;return t in z7&&t!==","?"\\ldots\\,":"\\ldots"});ke("\\cdots",function(e){var t=e.future().text;return t in z7?"\\@cdots\\,":"\\@cdots"});ke("\\dotsb","\\cdots");ke("\\dotsm","\\cdots");ke("\\dotsi","\\!\\cdots");ke("\\dotsx","\\ldots\\,");ke("\\DOTSI","\\relax");ke("\\DOTSB","\\relax");ke("\\DOTSX","\\relax");ke("\\tmspace","\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax");ke("\\,","\\tmspace+{3mu}{.1667em}");ke("\\thinspace","\\,");ke("\\>","\\mskip{4mu}");ke("\\:","\\tmspace+{4mu}{.2222em}");ke("\\medspace","\\:");ke("\\;","\\tmspace+{5mu}{.2777em}");ke("\\thickspace","\\;");ke("\\!","\\tmspace-{3mu}{.1667em}");ke("\\negthinspace","\\!");ke("\\negmedspace","\\tmspace-{4mu}{.2222em}");ke("\\negthickspace","\\tmspace-{5mu}{.277em}");ke("\\enspace","\\kern.5em ");ke("\\enskip","\\hskip.5em\\relax");ke("\\quad","\\hskip1em\\relax");ke("\\qquad","\\hskip2em\\relax");ke("\\tag","\\@ifstar\\tag@literal\\tag@paren");ke("\\tag@paren","\\tag@literal{({#1})}");ke("\\tag@literal",e=>{if(e.macros.get("\\df@tag"))throw new Nt("Multiple \\tag");return"\\gdef\\df@tag{\\text{#1}}"});ke("\\bmod","\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}");ke("\\pod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)");ke("\\pmod","\\pod{{\\rm mod}\\mkern6mu#1}");ke("\\mod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1");ke("\\newline","\\\\\\relax");ke("\\TeX","\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}");ZQ=Ft(Bc["Main-Regular"][84][1]-.7*Bc["Main-Regular"][65][1]);ke("\\LaTeX","\\textrm{\\html@mathml{"+("L\\kern-.36em\\raisebox{"+ZQ+"}{\\scriptstyle A}")+"\\kern-.15em\\TeX}{LaTeX}}");ke("\\KaTeX","\\textrm{\\html@mathml{"+("K\\kern-.17em\\raisebox{"+ZQ+"}{\\scriptstyle A}")+"\\kern-.15em\\TeX}{KaTeX}}");ke("\\hspace","\\@ifstar\\@hspacer\\@hspace");ke("\\@hspace","\\hskip #1\\relax");ke("\\@hspacer","\\rule{0pt}{0pt}\\hskip #1\\relax");ke("\\ordinarycolon",":");ke("\\vcentcolon","\\mathrel{\\mathop\\ordinarycolon}");ke("\\dblcolon",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char"2237}}');ke("\\coloneqq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2254}}');ke("\\Coloneqq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2237\\char"3d}}');ke("\\coloneq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"3a\\char"2212}}');ke("\\Coloneq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"2237\\char"2212}}');ke("\\eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2255}}');ke("\\Eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"3d\\char"2237}}');ke("\\eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2239}}');ke("\\Eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"2212\\char"2237}}');ke("\\colonapprox",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"3a\\char"2248}}');ke("\\Colonapprox",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"2237\\char"2248}}');ke("\\colonsim",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"3a\\char"223c}}');ke("\\Colonsim",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"2237\\char"223c}}');ke("\u2237","\\dblcolon");ke("\u2239","\\eqcolon");ke("\u2254","\\coloneqq");ke("\u2255","\\eqqcolon");ke("\u2A74","\\Coloneqq");ke("\\ratio","\\vcentcolon");ke("\\coloncolon","\\dblcolon");ke("\\colonequals","\\coloneqq");ke("\\coloncolonequals","\\Coloneqq");ke("\\equalscolon","\\eqqcolon");ke("\\equalscoloncolon","\\Eqqcolon");ke("\\colonminus","\\coloneq");ke("\\coloncolonminus","\\Coloneq");ke("\\minuscolon","\\eqcolon");ke("\\minuscoloncolon","\\Eqcolon");ke("\\coloncolonapprox","\\Colonapprox");ke("\\coloncolonsim","\\Colonsim");ke("\\simcolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}");ke("\\simcoloncolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}");ke("\\approxcolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}");ke("\\approxcoloncolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}");ke("\\notni","\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220C}}");ke("\\limsup","\\DOTSB\\operatorname*{lim\\,sup}");ke("\\liminf","\\DOTSB\\operatorname*{lim\\,inf}");ke("\\injlim","\\DOTSB\\operatorname*{inj\\,lim}");ke("\\projlim","\\DOTSB\\operatorname*{proj\\,lim}");ke("\\varlimsup","\\DOTSB\\operatorname*{\\overline{lim}}");ke("\\varliminf","\\DOTSB\\operatorname*{\\underline{lim}}");ke("\\varinjlim","\\DOTSB\\operatorname*{\\underrightarrow{lim}}");ke("\\varprojlim","\\DOTSB\\operatorname*{\\underleftarrow{lim}}");ke("\\gvertneqq","\\html@mathml{\\@gvertneqq}{\u2269}");ke("\\lvertneqq","\\html@mathml{\\@lvertneqq}{\u2268}");ke("\\ngeqq","\\html@mathml{\\@ngeqq}{\u2271}");ke("\\ngeqslant","\\html@mathml{\\@ngeqslant}{\u2271}");ke("\\nleqq","\\html@mathml{\\@nleqq}{\u2270}");ke("\\nleqslant","\\html@mathml{\\@nleqslant}{\u2270}");ke("\\nshortmid","\\html@mathml{\\@nshortmid}{\u2224}");ke("\\nshortparallel","\\html@mathml{\\@nshortparallel}{\u2226}");ke("\\nsubseteqq","\\html@mathml{\\@nsubseteqq}{\u2288}");ke("\\nsupseteqq","\\html@mathml{\\@nsupseteqq}{\u2289}");ke("\\varsubsetneq","\\html@mathml{\\@varsubsetneq}{\u228A}");ke("\\varsubsetneqq","\\html@mathml{\\@varsubsetneqq}{\u2ACB}");ke("\\varsupsetneq","\\html@mathml{\\@varsupsetneq}{\u228B}");ke("\\varsupsetneqq","\\html@mathml{\\@varsupsetneqq}{\u2ACC}");ke("\\imath","\\html@mathml{\\@imath}{\u0131}");ke("\\jmath","\\html@mathml{\\@jmath}{\u0237}");ke("\\llbracket","\\html@mathml{\\mathopen{[\\mkern-3.2mu[}}{\\mathopen{\\char`\u27E6}}");ke("\\rrbracket","\\html@mathml{\\mathclose{]\\mkern-3.2mu]}}{\\mathclose{\\char`\u27E7}}");ke("\u27E6","\\llbracket");ke("\u27E7","\\rrbracket");ke("\\lBrace","\\html@mathml{\\mathopen{\\{\\mkern-3.2mu[}}{\\mathopen{\\char`\u2983}}");ke("\\rBrace","\\html@mathml{\\mathclose{]\\mkern-3.2mu\\}}}{\\mathclose{\\char`\u2984}}");ke("\u2983","\\lBrace");ke("\u2984","\\rBrace");ke("\\minuso","\\mathbin{\\html@mathml{{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}{\\char`\u29B5}}");ke("\u29B5","\\minuso");ke("\\darr","\\downarrow");ke("\\dArr","\\Downarrow");ke("\\Darr","\\Downarrow");ke("\\lang","\\langle");ke("\\rang","\\rangle");ke("\\uarr","\\uparrow");ke("\\uArr","\\Uparrow");ke("\\Uarr","\\Uparrow");ke("\\N","\\mathbb{N}");ke("\\R","\\mathbb{R}");ke("\\Z","\\mathbb{Z}");ke("\\alef","\\aleph");ke("\\alefsym","\\aleph");ke("\\Alpha","\\mathrm{A}");ke("\\Beta","\\mathrm{B}");ke("\\bull","\\bullet");ke("\\Chi","\\mathrm{X}");ke("\\clubs","\\clubsuit");ke("\\cnums","\\mathbb{C}");ke("\\Complex","\\mathbb{C}");ke("\\Dagger","\\ddagger");ke("\\diamonds","\\diamondsuit");ke("\\empty","\\emptyset");ke("\\Epsilon","\\mathrm{E}");ke("\\Eta","\\mathrm{H}");ke("\\exist","\\exists");ke("\\harr","\\leftrightarrow");ke("\\hArr","\\Leftrightarrow");ke("\\Harr","\\Leftrightarrow");ke("\\hearts","\\heartsuit");ke("\\image","\\Im");ke("\\infin","\\infty");ke("\\Iota","\\mathrm{I}");ke("\\isin","\\in");ke("\\Kappa","\\mathrm{K}");ke("\\larr","\\leftarrow");ke("\\lArr","\\Leftarrow");ke("\\Larr","\\Leftarrow");ke("\\lrarr","\\leftrightarrow");ke("\\lrArr","\\Leftrightarrow");ke("\\Lrarr","\\Leftrightarrow");ke("\\Mu","\\mathrm{M}");ke("\\natnums","\\mathbb{N}");ke("\\Nu","\\mathrm{N}");ke("\\Omicron","\\mathrm{O}");ke("\\plusmn","\\pm");ke("\\rarr","\\rightarrow");ke("\\rArr","\\Rightarrow");ke("\\Rarr","\\Rightarrow");ke("\\real","\\Re");ke("\\reals","\\mathbb{R}");ke("\\Reals","\\mathbb{R}");ke("\\Rho","\\mathrm{P}");ke("\\sdot","\\cdot");ke("\\sect","\\S");ke("\\spades","\\spadesuit");ke("\\sub","\\subset");ke("\\sube","\\subseteq");ke("\\supe","\\supseteq");ke("\\Tau","\\mathrm{T}");ke("\\thetasym","\\vartheta");ke("\\weierp","\\wp");ke("\\Zeta","\\mathrm{Z}");ke("\\argmin","\\DOTSB\\operatorname*{arg\\,min}");ke("\\argmax","\\DOTSB\\operatorname*{arg\\,max}");ke("\\plim","\\DOTSB\\mathop{\\operatorname{plim}}\\limits");ke("\\bra","\\mathinner{\\langle{#1}|}");ke("\\ket","\\mathinner{|{#1}\\rangle}");ke("\\braket","\\mathinner{\\langle{#1}\\rangle}");ke("\\Bra","\\left\\langle#1\\right|");ke("\\Ket","\\left|#1\\right\\rangle");QQ=s(e=>t=>{var r=t.consumeArg().tokens,n=t.consumeArg().tokens,i=t.consumeArg().tokens,a=t.consumeArg().tokens,o=t.macros.get("|"),l=t.macros.get("\\|");t.macros.beginGroup();var u=s(f=>p=>{e&&(p.macros.set("|",o),i.length&&p.macros.set("\\|",l));var m=f;if(!f&&i.length){var g=p.future();g.text==="|"&&(p.popToken(),m=!0)}return{tokens:m?i:n,numArgs:0}},"midMacro");t.macros.set("|",u(!1)),i.length&&t.macros.set("\\|",u(!0));var h=t.consumeArg().tokens,d=t.expandTokens([...a,...h,...r]);return t.macros.endGroup(),{tokens:d.reverse(),numArgs:0}},"braketHelper");ke("\\bra@ket",QQ(!1));ke("\\bra@set",QQ(!0));ke("\\Braket","\\bra@ket{\\left\\langle}{\\,\\middle\\vert\\,}{\\,\\middle\\vert\\,}{\\right\\rangle}");ke("\\Set","\\bra@set{\\left\\{\\:}{\\;\\middle\\vert\\;}{\\;\\middle\\Vert\\;}{\\:\\right\\}}");ke("\\set","\\bra@set{\\{\\,}{\\mid}{}{\\,\\}}");ke("\\angln","{\\angl n}");ke("\\blue","\\textcolor{##6495ed}{#1}");ke("\\orange","\\textcolor{##ffa500}{#1}");ke("\\pink","\\textcolor{##ff00af}{#1}");ke("\\red","\\textcolor{##df0030}{#1}");ke("\\green","\\textcolor{##28ae7b}{#1}");ke("\\gray","\\textcolor{gray}{#1}");ke("\\purple","\\textcolor{##9d38bd}{#1}");ke("\\blueA","\\textcolor{##ccfaff}{#1}");ke("\\blueB","\\textcolor{##80f6ff}{#1}");ke("\\blueC","\\textcolor{##63d9ea}{#1}");ke("\\blueD","\\textcolor{##11accd}{#1}");ke("\\blueE","\\textcolor{##0c7f99}{#1}");ke("\\tealA","\\textcolor{##94fff5}{#1}");ke("\\tealB","\\textcolor{##26edd5}{#1}");ke("\\tealC","\\textcolor{##01d1c1}{#1}");ke("\\tealD","\\textcolor{##01a995}{#1}");ke("\\tealE","\\textcolor{##208170}{#1}");ke("\\greenA","\\textcolor{##b6ffb0}{#1}");ke("\\greenB","\\textcolor{##8af281}{#1}");ke("\\greenC","\\textcolor{##74cf70}{#1}");ke("\\greenD","\\textcolor{##1fab54}{#1}");ke("\\greenE","\\textcolor{##0d923f}{#1}");ke("\\goldA","\\textcolor{##ffd0a9}{#1}");ke("\\goldB","\\textcolor{##ffbb71}{#1}");ke("\\goldC","\\textcolor{##ff9c39}{#1}");ke("\\goldD","\\textcolor{##e07d10}{#1}");ke("\\goldE","\\textcolor{##a75a05}{#1}");ke("\\redA","\\textcolor{##fca9a9}{#1}");ke("\\redB","\\textcolor{##ff8482}{#1}");ke("\\redC","\\textcolor{##f9685d}{#1}");ke("\\redD","\\textcolor{##e84d39}{#1}");ke("\\redE","\\textcolor{##bc2612}{#1}");ke("\\maroonA","\\textcolor{##ffbde0}{#1}");ke("\\maroonB","\\textcolor{##ff92c6}{#1}");ke("\\maroonC","\\textcolor{##ed5fa6}{#1}");ke("\\maroonD","\\textcolor{##ca337c}{#1}");ke("\\maroonE","\\textcolor{##9e034e}{#1}");ke("\\purpleA","\\textcolor{##ddd7ff}{#1}");ke("\\purpleB","\\textcolor{##c6b9fc}{#1}");ke("\\purpleC","\\textcolor{##aa87ff}{#1}");ke("\\purpleD","\\textcolor{##7854ab}{#1}");ke("\\purpleE","\\textcolor{##543b78}{#1}");ke("\\mintA","\\textcolor{##f5f9e8}{#1}");ke("\\mintB","\\textcolor{##edf2df}{#1}");ke("\\mintC","\\textcolor{##e0e5cc}{#1}");ke("\\grayA","\\textcolor{##f6f7f7}{#1}");ke("\\grayB","\\textcolor{##f0f1f2}{#1}");ke("\\grayC","\\textcolor{##e3e5e6}{#1}");ke("\\grayD","\\textcolor{##d6d8da}{#1}");ke("\\grayE","\\textcolor{##babec2}{#1}");ke("\\grayF","\\textcolor{##888d93}{#1}");ke("\\grayG","\\textcolor{##626569}{#1}");ke("\\grayH","\\textcolor{##3b3e40}{#1}");ke("\\grayI","\\textcolor{##21242c}{#1}");ke("\\kaBlue","\\textcolor{##314453}{#1}");ke("\\kaGreen","\\textcolor{##71B307}{#1}");JQ={"^":!0,_:!0,"\\limits":!0,"\\nolimits":!0},R7=class{static{s(this,"MacroExpander")}constructor(t,r,n){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=r,this.expansionCount=0,this.feed(t),this.macros=new A7(kWe,r.macros),this.mode=n,this.stack=[]}feed(t){this.lexer=new jw(t,this.settings)}switchMode(t){this.mode=t}beginGroup(){this.macros.beginGroup()}endGroup(){this.macros.endGroup()}endGroups(){this.macros.endGroups()}future(){return this.stack.length===0&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]}popToken(){return this.future(),this.stack.pop()}pushToken(t){this.stack.push(t)}pushTokens(t){this.stack.push(...t)}scanArgument(t){var r,n,i;if(t){if(this.consumeSpaces(),this.future().text!=="[")return null;r=this.popToken(),{tokens:i,end:n}=this.consumeArg(["]"])}else({tokens:i,start:r,end:n}=this.consumeArg());return this.pushToken(new Bo("EOF",n.loc)),this.pushTokens(i),new Bo("",so.range(r,n))}consumeSpaces(){for(;;){var t=this.future();if(t.text===" ")this.stack.pop();else break}}consumeArg(t){var r=[],n=t&&t.length>0;n||this.consumeSpaces();var i=this.future(),a,o=0,l=0;do{if(a=this.popToken(),r.push(a),a.text==="{")++o;else if(a.text==="}"){if(--o,o===-1)throw new Nt("Extra }",a)}else if(a.text==="EOF")throw new Nt("Unexpected end of input in a macro argument, expected '"+(t&&n?t[l]:"}")+"'",a);if(t&&n)if((o===0||o===1&&t[l]==="{")&&a.text===t[l]){if(++l,l===t.length){r.splice(-l,l);break}}else l=0}while(o!==0||n);return i.text==="{"&&r[r.length-1].text==="}"&&(r.pop(),r.shift()),r.reverse(),{tokens:r,start:i,end:a}}consumeArgs(t,r){if(r){if(r.length!==t+1)throw new Nt("The length of delimiters doesn't match the number of args!");for(var n=r[0],i=0;ithis.settings.maxExpand)throw new Nt("Too many expansions: infinite loop or need to increase maxExpand setting")}expandOnce(t){var r=this.popToken(),n=r.text,i=r.noexpand?null:this._getExpansion(n);if(i==null||t&&i.unexpandable){if(t&&i==null&&n[0]==="\\"&&!this.isDefined(n))throw new Nt("Undefined control sequence: "+n);return this.pushToken(r),!1}this.countExpansion(1);var a=i.tokens,o=this.consumeArgs(i.numArgs,i.delimiters);if(i.numArgs){a=a.slice();for(var l=a.length-1;l>=0;--l){var u=a[l];if(u.text==="#"){if(l===0)throw new Nt("Incomplete placeholder at end of macro body",u);if(u=a[--l],u.text==="#")a.splice(l+1,1);else if(/^[1-9]$/.test(u.text))a.splice(l,2,...o[+u.text-1]);else throw new Nt("Not a valid argument number",u)}}}return this.pushTokens(a),a.length}expandAfterFuture(){return this.expandOnce(),this.future()}expandNextToken(){for(;;)if(this.expandOnce()===!1){var t=this.stack.pop();return t.treatAsRelax&&(t.text="\\relax"),t}}expandMacro(t){return this.macros.has(t)?this.expandTokens([new Bo(t)]):void 0}expandTokens(t){var r=[],n=this.stack.length;for(this.pushTokens(t);this.stack.length>n;)if(this.expandOnce(!0)===!1){var i=this.stack.pop();i.treatAsRelax&&(i.noexpand=!1,i.treatAsRelax=!1),r.push(i)}return this.countExpansion(r.length),r}expandMacroAsText(t){var r=this.expandMacro(t);return r&&r.map(n=>n.text).join("")}_getExpansion(t){var r=this.macros.get(t);if(r==null)return r;if(t.length===1){var n=this.lexer.catcodes[t];if(n!=null&&n!==13)return}var i=typeof r=="function"?r(this):r;if(typeof i=="string"){var a=0;if(i.includes("#"))for(var o=i.replace(/##/g,"");o.includes("#"+(a+1));)++a;for(var l=new jw(i,this.settings),u=[],h=l.lex();h.text!=="EOF";)u.push(h),h=l.lex();u.reverse();var d={tokens:u,numArgs:a};return d}return i}isDefined(t){return this.macros.has(t)||Rd.hasOwnProperty(t)||pi.math.hasOwnProperty(t)||pi.text.hasOwnProperty(t)||JQ.hasOwnProperty(t)}isExpandable(t){var r=this.macros.get(t);return r!=null?typeof r=="string"||typeof r=="function"||!r.unexpandable:Rd.hasOwnProperty(t)&&!Rd[t].primitive}},iQ=/^[₊₋₌₍₎₀₁₂₃₄₅₆₇₈₉ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵦᵧᵨᵩᵪ]/,Bw=Object.freeze({"\u208A":"+","\u208B":"-","\u208C":"=","\u208D":"(","\u208E":")","\u2080":"0","\u2081":"1","\u2082":"2","\u2083":"3","\u2084":"4","\u2085":"5","\u2086":"6","\u2087":"7","\u2088":"8","\u2089":"9","\u2090":"a","\u2091":"e","\u2095":"h","\u1D62":"i","\u2C7C":"j","\u2096":"k","\u2097":"l","\u2098":"m","\u2099":"n","\u2092":"o","\u209A":"p","\u1D63":"r","\u209B":"s","\u209C":"t","\u1D64":"u","\u1D65":"v","\u2093":"x","\u1D66":"\u03B2","\u1D67":"\u03B3","\u1D68":"\u03C1","\u1D69":"\u03D5","\u1D6A":"\u03C7","\u207A":"+","\u207B":"-","\u207C":"=","\u207D":"(","\u207E":")","\u2070":"0","\xB9":"1","\xB2":"2","\xB3":"3","\u2074":"4","\u2075":"5","\u2076":"6","\u2077":"7","\u2078":"8","\u2079":"9","\u1D2C":"A","\u1D2E":"B","\u1D30":"D","\u1D31":"E","\u1D33":"G","\u1D34":"H","\u1D35":"I","\u1D36":"J","\u1D37":"K","\u1D38":"L","\u1D39":"M","\u1D3A":"N","\u1D3C":"O","\u1D3E":"P","\u1D3F":"R","\u1D40":"T","\u1D41":"U","\u2C7D":"V","\u1D42":"W","\u1D43":"a","\u1D47":"b","\u1D9C":"c","\u1D48":"d","\u1D49":"e","\u1DA0":"f","\u1D4D":"g",\u02B0:"h","\u2071":"i",\u02B2:"j","\u1D4F":"k",\u02E1:"l","\u1D50":"m",\u207F:"n","\u1D52":"o","\u1D56":"p",\u02B3:"r",\u02E2:"s","\u1D57":"t","\u1D58":"u","\u1D5B":"v",\u02B7:"w",\u02E3:"x",\u02B8:"y","\u1DBB":"z","\u1D5D":"\u03B2","\u1D5E":"\u03B3","\u1D5F":"\u03B4","\u1D60":"\u03D5","\u1D61":"\u03C7","\u1DBF":"\u03B8"}),l7={"\u0301":{text:"\\'",math:"\\acute"},"\u0300":{text:"\\`",math:"\\grave"},"\u0308":{text:'\\"',math:"\\ddot"},"\u0303":{text:"\\~",math:"\\tilde"},"\u0304":{text:"\\=",math:"\\bar"},"\u0306":{text:"\\u",math:"\\breve"},"\u030C":{text:"\\v",math:"\\check"},"\u0302":{text:"\\^",math:"\\hat"},"\u0307":{text:"\\.",math:"\\dot"},"\u030A":{text:"\\r",math:"\\mathring"},"\u030B":{text:"\\H"},"\u0327":{text:"\\c"}},aQ={\u00E1:"a\u0301",\u00E0:"a\u0300",\u00E4:"a\u0308",\u01DF:"a\u0308\u0304",\u00E3:"a\u0303",\u0101:"a\u0304",\u0103:"a\u0306",\u1EAF:"a\u0306\u0301",\u1EB1:"a\u0306\u0300",\u1EB5:"a\u0306\u0303",\u01CE:"a\u030C",\u00E2:"a\u0302",\u1EA5:"a\u0302\u0301",\u1EA7:"a\u0302\u0300",\u1EAB:"a\u0302\u0303",\u0227:"a\u0307",\u01E1:"a\u0307\u0304",\u00E5:"a\u030A",\u01FB:"a\u030A\u0301",\u1E03:"b\u0307",\u0107:"c\u0301",\u1E09:"c\u0327\u0301",\u010D:"c\u030C",\u0109:"c\u0302",\u010B:"c\u0307",\u00E7:"c\u0327",\u010F:"d\u030C",\u1E0B:"d\u0307",\u1E11:"d\u0327",\u00E9:"e\u0301",\u00E8:"e\u0300",\u00EB:"e\u0308",\u1EBD:"e\u0303",\u0113:"e\u0304",\u1E17:"e\u0304\u0301",\u1E15:"e\u0304\u0300",\u0115:"e\u0306",\u1E1D:"e\u0327\u0306",\u011B:"e\u030C",\u00EA:"e\u0302",\u1EBF:"e\u0302\u0301",\u1EC1:"e\u0302\u0300",\u1EC5:"e\u0302\u0303",\u0117:"e\u0307",\u0229:"e\u0327",\u1E1F:"f\u0307",\u01F5:"g\u0301",\u1E21:"g\u0304",\u011F:"g\u0306",\u01E7:"g\u030C",\u011D:"g\u0302",\u0121:"g\u0307",\u0123:"g\u0327",\u1E27:"h\u0308",\u021F:"h\u030C",\u0125:"h\u0302",\u1E23:"h\u0307",\u1E29:"h\u0327",\u00ED:"i\u0301",\u00EC:"i\u0300",\u00EF:"i\u0308",\u1E2F:"i\u0308\u0301",\u0129:"i\u0303",\u012B:"i\u0304",\u012D:"i\u0306",\u01D0:"i\u030C",\u00EE:"i\u0302",\u01F0:"j\u030C",\u0135:"j\u0302",\u1E31:"k\u0301",\u01E9:"k\u030C",\u0137:"k\u0327",\u013A:"l\u0301",\u013E:"l\u030C",\u013C:"l\u0327",\u1E3F:"m\u0301",\u1E41:"m\u0307",\u0144:"n\u0301",\u01F9:"n\u0300",\u00F1:"n\u0303",\u0148:"n\u030C",\u1E45:"n\u0307",\u0146:"n\u0327",\u00F3:"o\u0301",\u00F2:"o\u0300",\u00F6:"o\u0308",\u022B:"o\u0308\u0304",\u00F5:"o\u0303",\u1E4D:"o\u0303\u0301",\u1E4F:"o\u0303\u0308",\u022D:"o\u0303\u0304",\u014D:"o\u0304",\u1E53:"o\u0304\u0301",\u1E51:"o\u0304\u0300",\u014F:"o\u0306",\u01D2:"o\u030C",\u00F4:"o\u0302",\u1ED1:"o\u0302\u0301",\u1ED3:"o\u0302\u0300",\u1ED7:"o\u0302\u0303",\u022F:"o\u0307",\u0231:"o\u0307\u0304",\u0151:"o\u030B",\u1E55:"p\u0301",\u1E57:"p\u0307",\u0155:"r\u0301",\u0159:"r\u030C",\u1E59:"r\u0307",\u0157:"r\u0327",\u015B:"s\u0301",\u1E65:"s\u0301\u0307",\u0161:"s\u030C",\u1E67:"s\u030C\u0307",\u015D:"s\u0302",\u1E61:"s\u0307",\u015F:"s\u0327",\u1E97:"t\u0308",\u0165:"t\u030C",\u1E6B:"t\u0307",\u0163:"t\u0327",\u00FA:"u\u0301",\u00F9:"u\u0300",\u00FC:"u\u0308",\u01D8:"u\u0308\u0301",\u01DC:"u\u0308\u0300",\u01D6:"u\u0308\u0304",\u01DA:"u\u0308\u030C",\u0169:"u\u0303",\u1E79:"u\u0303\u0301",\u016B:"u\u0304",\u1E7B:"u\u0304\u0308",\u016D:"u\u0306",\u01D4:"u\u030C",\u00FB:"u\u0302",\u016F:"u\u030A",\u0171:"u\u030B",\u1E7D:"v\u0303",\u1E83:"w\u0301",\u1E81:"w\u0300",\u1E85:"w\u0308",\u0175:"w\u0302",\u1E87:"w\u0307",\u1E98:"w\u030A",\u1E8D:"x\u0308",\u1E8B:"x\u0307",\u00FD:"y\u0301",\u1EF3:"y\u0300",\u00FF:"y\u0308",\u1EF9:"y\u0303",\u0233:"y\u0304",\u0177:"y\u0302",\u1E8F:"y\u0307",\u1E99:"y\u030A",\u017A:"z\u0301",\u017E:"z\u030C",\u1E91:"z\u0302",\u017C:"z\u0307",\u00C1:"A\u0301",\u00C0:"A\u0300",\u00C4:"A\u0308",\u01DE:"A\u0308\u0304",\u00C3:"A\u0303",\u0100:"A\u0304",\u0102:"A\u0306",\u1EAE:"A\u0306\u0301",\u1EB0:"A\u0306\u0300",\u1EB4:"A\u0306\u0303",\u01CD:"A\u030C",\u00C2:"A\u0302",\u1EA4:"A\u0302\u0301",\u1EA6:"A\u0302\u0300",\u1EAA:"A\u0302\u0303",\u0226:"A\u0307",\u01E0:"A\u0307\u0304",\u00C5:"A\u030A",\u01FA:"A\u030A\u0301",\u1E02:"B\u0307",\u0106:"C\u0301",\u1E08:"C\u0327\u0301",\u010C:"C\u030C",\u0108:"C\u0302",\u010A:"C\u0307",\u00C7:"C\u0327",\u010E:"D\u030C",\u1E0A:"D\u0307",\u1E10:"D\u0327",\u00C9:"E\u0301",\u00C8:"E\u0300",\u00CB:"E\u0308",\u1EBC:"E\u0303",\u0112:"E\u0304",\u1E16:"E\u0304\u0301",\u1E14:"E\u0304\u0300",\u0114:"E\u0306",\u1E1C:"E\u0327\u0306",\u011A:"E\u030C",\u00CA:"E\u0302",\u1EBE:"E\u0302\u0301",\u1EC0:"E\u0302\u0300",\u1EC4:"E\u0302\u0303",\u0116:"E\u0307",\u0228:"E\u0327",\u1E1E:"F\u0307",\u01F4:"G\u0301",\u1E20:"G\u0304",\u011E:"G\u0306",\u01E6:"G\u030C",\u011C:"G\u0302",\u0120:"G\u0307",\u0122:"G\u0327",\u1E26:"H\u0308",\u021E:"H\u030C",\u0124:"H\u0302",\u1E22:"H\u0307",\u1E28:"H\u0327",\u00CD:"I\u0301",\u00CC:"I\u0300",\u00CF:"I\u0308",\u1E2E:"I\u0308\u0301",\u0128:"I\u0303",\u012A:"I\u0304",\u012C:"I\u0306",\u01CF:"I\u030C",\u00CE:"I\u0302",\u0130:"I\u0307",\u0134:"J\u0302",\u1E30:"K\u0301",\u01E8:"K\u030C",\u0136:"K\u0327",\u0139:"L\u0301",\u013D:"L\u030C",\u013B:"L\u0327",\u1E3E:"M\u0301",\u1E40:"M\u0307",\u0143:"N\u0301",\u01F8:"N\u0300",\u00D1:"N\u0303",\u0147:"N\u030C",\u1E44:"N\u0307",\u0145:"N\u0327",\u00D3:"O\u0301",\u00D2:"O\u0300",\u00D6:"O\u0308",\u022A:"O\u0308\u0304",\u00D5:"O\u0303",\u1E4C:"O\u0303\u0301",\u1E4E:"O\u0303\u0308",\u022C:"O\u0303\u0304",\u014C:"O\u0304",\u1E52:"O\u0304\u0301",\u1E50:"O\u0304\u0300",\u014E:"O\u0306",\u01D1:"O\u030C",\u00D4:"O\u0302",\u1ED0:"O\u0302\u0301",\u1ED2:"O\u0302\u0300",\u1ED6:"O\u0302\u0303",\u022E:"O\u0307",\u0230:"O\u0307\u0304",\u0150:"O\u030B",\u1E54:"P\u0301",\u1E56:"P\u0307",\u0154:"R\u0301",\u0158:"R\u030C",\u1E58:"R\u0307",\u0156:"R\u0327",\u015A:"S\u0301",\u1E64:"S\u0301\u0307",\u0160:"S\u030C",\u1E66:"S\u030C\u0307",\u015C:"S\u0302",\u1E60:"S\u0307",\u015E:"S\u0327",\u0164:"T\u030C",\u1E6A:"T\u0307",\u0162:"T\u0327",\u00DA:"U\u0301",\u00D9:"U\u0300",\u00DC:"U\u0308",\u01D7:"U\u0308\u0301",\u01DB:"U\u0308\u0300",\u01D5:"U\u0308\u0304",\u01D9:"U\u0308\u030C",\u0168:"U\u0303",\u1E78:"U\u0303\u0301",\u016A:"U\u0304",\u1E7A:"U\u0304\u0308",\u016C:"U\u0306",\u01D3:"U\u030C",\u00DB:"U\u0302",\u016E:"U\u030A",\u0170:"U\u030B",\u1E7C:"V\u0303",\u1E82:"W\u0301",\u1E80:"W\u0300",\u1E84:"W\u0308",\u0174:"W\u0302",\u1E86:"W\u0307",\u1E8C:"X\u0308",\u1E8A:"X\u0307",\u00DD:"Y\u0301",\u1EF2:"Y\u0300",\u0178:"Y\u0308",\u1EF8:"Y\u0303",\u0232:"Y\u0304",\u0176:"Y\u0302",\u1E8E:"Y\u0307",\u0179:"Z\u0301",\u017D:"Z\u030C",\u1E90:"Z\u0302",\u017B:"Z\u0307",\u03AC:"\u03B1\u0301",\u1F70:"\u03B1\u0300",\u1FB1:"\u03B1\u0304",\u1FB0:"\u03B1\u0306",\u03AD:"\u03B5\u0301",\u1F72:"\u03B5\u0300",\u03AE:"\u03B7\u0301",\u1F74:"\u03B7\u0300",\u03AF:"\u03B9\u0301",\u1F76:"\u03B9\u0300",\u03CA:"\u03B9\u0308",\u0390:"\u03B9\u0308\u0301",\u1FD2:"\u03B9\u0308\u0300",\u1FD1:"\u03B9\u0304",\u1FD0:"\u03B9\u0306",\u03CC:"\u03BF\u0301",\u1F78:"\u03BF\u0300",\u03CD:"\u03C5\u0301",\u1F7A:"\u03C5\u0300",\u03CB:"\u03C5\u0308",\u03B0:"\u03C5\u0308\u0301",\u1FE2:"\u03C5\u0308\u0300",\u1FE1:"\u03C5\u0304",\u1FE0:"\u03C5\u0306",\u03CE:"\u03C9\u0301",\u1F7C:"\u03C9\u0300",\u038E:"\u03A5\u0301",\u1FEA:"\u03A5\u0300",\u03AB:"\u03A5\u0308",\u1FE9:"\u03A5\u0304",\u1FE8:"\u03A5\u0306",\u038F:"\u03A9\u0301",\u1FFA:"\u03A9\u0300"},Xw=class e{static{s(this,"Parser")}constructor(t,r){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode="math",this.gullet=new R7(t,r,this.mode),this.settings=r,this.leftrightDepth=0,this.nextToken=null}expect(t,r){if(r===void 0&&(r=!0),this.fetch().text!==t)throw new Nt("Expected '"+t+"', got '"+this.fetch().text+"'",this.fetch());r&&this.consume()}consume(){this.nextToken=null}fetch(){return this.nextToken==null&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken}switchMode(t){this.mode=t,this.gullet.switchMode(t)}parse(){this.settings.globalGroup||this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set("\\color","\\textcolor");try{var t=this.parseExpression(!1);return this.expect("EOF"),this.settings.globalGroup||this.gullet.endGroup(),t}finally{this.gullet.endGroups()}}subparse(t){var r=this.nextToken;this.consume(),this.gullet.pushToken(new Bo("}")),this.gullet.pushTokens(t);var n=this.parseExpression(!1);return this.expect("}"),this.nextToken=r,n}parseExpression(t,r){for(var n=[];;){this.mode==="math"&&this.consumeSpaces();var i=this.fetch();if(e.endOfExpression.has(i.text)||r&&i.text===r||t&&Rd[i.text]&&Rd[i.text].infix)break;var a=this.parseAtom(r);if(a){if(a.type==="internal")continue}else break;n.push(a)}return this.mode==="text"&&this.formLigatures(n),this.handleInfixNodes(n)}handleInfixNodes(t){for(var r=-1,n,i=0;i=128)this.settings.strict&&(sQ(r.charCodeAt(0))?this.mode==="math"&&this.settings.reportNonstrict("unicodeTextInMathMode",'Unicode text character "'+r[0]+'" used in math mode',t):this.settings.reportNonstrict("unknownSymbol",'Unrecognized Unicode character "'+r[0]+'"'+(" ("+r.charCodeAt(0)+")"),t)),o={type:"textord",mode:"text",loc:so.range(t),text:r};else return null;if(this.consume(),a)for(var d=0;d{t.tagName==="A"&&t.hasAttribute("target")&&t.setAttribute(e,t.getAttribute("target")??"")}),ao.addHook("afterSanitizeAttributes",t=>{t.tagName==="A"&&t.hasAttribute(e)&&(t.setAttribute("target",t.getAttribute(e)??""),t.removeAttribute(e),t.getAttribute("target")==="_blank"&&t.setAttribute("rel","noopener"))})}var Up,RWe,_We,uJ,lJ,xr,DWe,IWe,MWe,NWe,hJ,eb,PWe,OWe,Vc,q7,BWe,$We,cJ,lk,si,Yp,FWe,J0,Tt,Yr=F(()=>{"use strict";q0();br();Up=//gi,RWe=s(e=>e?hJ(e).replace(/\\n/g,"#br#").split("#br#"):[""],"getRows"),_We=(()=>{let e=!1;return()=>{e||(LWe(),e=!0)}})();s(LWe,"setupDompurifyHooks");uJ=s(e=>(_We(),ao.sanitize(e)),"removeScript"),lJ=s((e,t)=>{if(ai(t)){let r=t.securityLevel;r==="antiscript"||r==="strict"||r==="sandbox"?e=uJ(e):r!=="loose"&&(e=hJ(e),e=e.replace(//g,">"),e=e.replace(/=/g,"="),e=NWe(e))}return e},"sanitizeMore"),xr=s((e,t)=>e&&(t.dompurifyConfig?e=ao.sanitize(lJ(e,t),t.dompurifyConfig).toString():e=ao.sanitize(lJ(e,t),{FORBID_TAGS:["style"]}).toString(),e),"sanitizeText"),DWe=s((e,t)=>typeof e=="string"?xr(e,t):e.flat().map(r=>xr(r,t)),"sanitizeTextOrArray"),IWe=s(e=>Up.test(e),"hasBreaks"),MWe=s(e=>e.split(Up),"splitBreaks"),NWe=s(e=>e.replace(/#br#/g,"
"),"placeholderToBreak"),hJ=s(e=>e.replace(Up,"#br#"),"breakToPlaceholder"),eb=s(e=>{let t="";return e&&(t=window.location.protocol+"//"+window.location.host+window.location.pathname+window.location.search,t=CSS.escape(t)),t},"getUrl"),PWe=s(function(...e){let t=e.filter(r=>!isNaN(r));return Math.max(...t)},"getMax"),OWe=s(function(...e){let t=e.filter(r=>!isNaN(r));return Math.min(...t)},"getMin"),Vc=s(function(e){let t=e.split(/(,)/),r=[];for(let n=0;n0&&n+1Math.max(0,e.split(t).length-1),"countOccurrence"),BWe=s((e,t)=>{let r=q7(e,"~"),n=q7(t,"~");return r===1&&n===1},"shouldCombineSets"),$We=s(e=>{let t=q7(e,"~"),r=!1;if(t<=1)return e;t%2!==0&&e.startsWith("~")&&(e=e.substring(1),r=!0);let n=[...e],i=n.indexOf("~"),a=n.lastIndexOf("~");for(;i!==-1&&a!==-1&&i!==a;)n[i]="<",n[a]=">",i=n.indexOf("~"),a=n.lastIndexOf("~");return r&&n.unshift("~"),n.join("")},"processSet"),cJ=s(()=>window.MathMLElement!==void 0,"isMathMLSupported"),lk=/\$\$(.*?)\$\$/g,si=s(e=>(e.match(lk)?.length??0)>0,"hasKatex"),Yp=s(async(e,t)=>{let r=document.createElement("div");r.innerHTML=await J0(e,t),r.id="katex-temp",r.style.visibility="hidden",r.style.position="absolute",r.style.top="0",document.querySelector("body")?.insertAdjacentElement("beforeend",r);let i={width:r.clientWidth,height:r.clientHeight};return r.remove(),i},"calculateMathMLDimensions"),FWe=s(async(e,t)=>{if(!si(e))return e;if(!(cJ()||t.legacyMathML||t.forceLegacyMathML))return e.replace(lk,"MathML is unsupported in this environment.");{let{default:r}=await Promise.resolve().then(()=>(oJ(),sJ)),n=t.forceLegacyMathML||!cJ()&&t.legacyMathML?"htmlAndMathml":"mathml";return e.split(Up).map(i=>si(i)?`
${i}
`:`
${i}
`).join("").replace(lk,(i,a)=>r.renderToString(a,{throwOnError:!0,displayMode:!0,output:n}).replace(/\n/g," ").replace(//g,""))}return e.replace(lk,"Katex is not supported in @mermaid-js/tiny. Please use the full mermaid library.")},"renderKatexUnsanitized"),J0=s(async(e,t)=>xr(await FWe(e,t),t),"renderKatexSanitized"),Tt={getRows:RWe,sanitizeText:xr,sanitizeTextOrArray:DWe,hasBreaks:IWe,splitBreaks:MWe,lineBreakRegex:Up,removeScript:uJ,getUrl:eb,evaluate:Aa,getMax:PWe,getMin:OWe}});var U7,H7,dJ,ey,fJ,pJ,ps,Hl=F(()=>{"use strict";eK();br();Yr();St();U7={body:'?',height:80,width:80},H7=new Map,dJ=new Map,ey=s(e=>{for(let t of e){if(!t.name)throw new Error('Invalid icon loader. Must have a "name" property with non-empty string value.');if(se.debug("Registering icon pack:",t.name),"loader"in t)dJ.set(t.name,t.loader);else if("icons"in t)H7.set(t.name,t.icons);else throw se.error("Invalid icon loader:",t),new Error('Invalid icon loader. Must have either "icons" or "loader" property.')}},"registerIconPacks"),fJ=s(async(e,t)=>{let r=hD(e,!0,t!==void 0);if(!r)throw new Error(`Invalid icon name: ${e}`);let n=r.prefix||t;if(!n)throw new Error(`Icon name must contain a prefix: ${e}`);let i=H7.get(n);if(!i){let o=dJ.get(n);if(!o)throw new Error(`Icon set not found: ${r.prefix}`);try{i={...await o(),prefix:n},H7.set(n,i)}catch(l){throw se.error(l),new Error(`Failed to load icon set: ${r.prefix}`)}}let a=fD(i,r.name);if(!a)throw new Error(`Icon not found: ${e}`);return a},"getRegisteredIconData"),pJ=s(async e=>{try{return await fJ(e),!0}catch{return!1}},"isIconAvailable"),ps=s(async(e,t,r)=>{let n;try{n=await fJ(e,t?.fallbackPrefix)}catch(o){se.error(o),n=U7}let i=mD(n,t),a=yD(gD(i.body),{...i.attributes,...r});return xr(a,Lt())},"getIconSVG")});function ck(e){for(var t=[],r=1;r{"use strict";s(ck,"dedent")});var uk,jp,mJ,hk=F(()=>{"use strict";uk=/^([^\S\n\r]*)-{3}\s*[\n\r](.*?)[\n\r]\1-{3}\s*[\n\r]+/s,jp=/%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi,mJ=/\s*%%.*\n/gm});var ty,j7=F(()=>{"use strict";ty=class extends Error{static{s(this,"UnknownDiagramError")}constructor(t){super(t),this.name="UnknownDiagramError"}}});var ch,ry,tb,X7,gJ,Xp=F(()=>{"use strict";St();hk();j7();ch={},ry=s(function(e,t){e=e.replace(uk,"").replace(jp,"").replace(mJ,` +`);for(let[r,{detector:n}]of Object.entries(ch))if(n(e,t))return r;throw new ty(`No diagram type detected matching given configuration for text: ${e}`)},"detectType"),tb=s((...e)=>{for(let{id:t,detector:r,loader:n}of e)X7(t,r,n)},"registerLazyLoadedDiagrams"),X7=s((e,t,r)=>{ch[e]&&se.warn(`Detector with key ${e} already exists. Overwriting.`),ch[e]={detector:t,loader:r},se.debug(`Detector with key ${e} added${r?" with loader":""}`)},"addDetector"),gJ=s(e=>ch[e].loader,"getDiagramLoader")});var rb,yJ,K7=F(()=>{"use strict";rb=(function(){var e=s(function(ue,le,he,Te){for(he=he||{},Te=ue.length;Te--;he[ue[Te]]=le);return he},"o"),t=[1,24],r=[1,25],n=[1,26],i=[1,27],a=[1,28],o=[1,63],l=[1,64],u=[1,65],h=[1,66],d=[1,67],f=[1,68],p=[1,69],m=[1,29],g=[1,30],y=[1,31],v=[1,32],x=[1,33],b=[1,34],T=[1,35],w=[1,36],C=[1,37],S=[1,38],k=[1,39],R=[1,40],M=[1,41],P=[1,42],N=[1,43],_=[1,44],A=[1,45],I=[1,46],D=[1,47],L=[1,48],B=[1,50],O=[1,51],$=[1,52],W=[1,53],V=[1,54],z=[1,55],H=[1,56],K=[1,57],J=[1,58],ne=[1,59],Z=[1,60],pe=[14,42],te=[14,34,36,37,38,39,40,41,42,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74],fe=[12,14,34,36,37,38,39,40,41,42,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74],ce=[1,82],de=[1,83],Ee=[1,84],we=[1,85],Fe=[12,14,42],Ye=[12,14,33,42],He=[12,14,33,42,76,77,79,80],ye=[12,33],Be=[34,36,37,38,39,40,41,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74],Ie={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mermaidDoc:4,direction:5,direction_tb:6,direction_bt:7,direction_rl:8,direction_lr:9,graphConfig:10,C4_CONTEXT:11,NEWLINE:12,statements:13,EOF:14,C4_CONTAINER:15,C4_COMPONENT:16,C4_DYNAMIC:17,C4_DEPLOYMENT:18,otherStatements:19,diagramStatements:20,otherStatement:21,title:22,accDescription:23,acc_title:24,acc_title_value:25,acc_descr:26,acc_descr_value:27,acc_descr_multiline_value:28,boundaryStatement:29,boundaryStartStatement:30,boundaryStopStatement:31,boundaryStart:32,LBRACE:33,ENTERPRISE_BOUNDARY:34,attributes:35,SYSTEM_BOUNDARY:36,BOUNDARY:37,CONTAINER_BOUNDARY:38,NODE:39,NODE_L:40,NODE_R:41,RBRACE:42,diagramStatement:43,PERSON:44,PERSON_EXT:45,SYSTEM:46,SYSTEM_DB:47,SYSTEM_QUEUE:48,SYSTEM_EXT:49,SYSTEM_EXT_DB:50,SYSTEM_EXT_QUEUE:51,CONTAINER:52,CONTAINER_DB:53,CONTAINER_QUEUE:54,CONTAINER_EXT:55,CONTAINER_EXT_DB:56,CONTAINER_EXT_QUEUE:57,COMPONENT:58,COMPONENT_DB:59,COMPONENT_QUEUE:60,COMPONENT_EXT:61,COMPONENT_EXT_DB:62,COMPONENT_EXT_QUEUE:63,REL:64,BIREL:65,REL_U:66,REL_D:67,REL_L:68,REL_R:69,REL_B:70,REL_INDEX:71,UPDATE_EL_STYLE:72,UPDATE_REL_STYLE:73,UPDATE_LAYOUT_CONFIG:74,attribute:75,STR:76,STR_KEY:77,STR_VALUE:78,ATTRIBUTE:79,ATTRIBUTE_EMPTY:80,$accept:0,$end:1},terminals_:{2:"error",6:"direction_tb",7:"direction_bt",8:"direction_rl",9:"direction_lr",11:"C4_CONTEXT",12:"NEWLINE",14:"EOF",15:"C4_CONTAINER",16:"C4_COMPONENT",17:"C4_DYNAMIC",18:"C4_DEPLOYMENT",22:"title",23:"accDescription",24:"acc_title",25:"acc_title_value",26:"acc_descr",27:"acc_descr_value",28:"acc_descr_multiline_value",33:"LBRACE",34:"ENTERPRISE_BOUNDARY",36:"SYSTEM_BOUNDARY",37:"BOUNDARY",38:"CONTAINER_BOUNDARY",39:"NODE",40:"NODE_L",41:"NODE_R",42:"RBRACE",44:"PERSON",45:"PERSON_EXT",46:"SYSTEM",47:"SYSTEM_DB",48:"SYSTEM_QUEUE",49:"SYSTEM_EXT",50:"SYSTEM_EXT_DB",51:"SYSTEM_EXT_QUEUE",52:"CONTAINER",53:"CONTAINER_DB",54:"CONTAINER_QUEUE",55:"CONTAINER_EXT",56:"CONTAINER_EXT_DB",57:"CONTAINER_EXT_QUEUE",58:"COMPONENT",59:"COMPONENT_DB",60:"COMPONENT_QUEUE",61:"COMPONENT_EXT",62:"COMPONENT_EXT_DB",63:"COMPONENT_EXT_QUEUE",64:"REL",65:"BIREL",66:"REL_U",67:"REL_D",68:"REL_L",69:"REL_R",70:"REL_B",71:"REL_INDEX",72:"UPDATE_EL_STYLE",73:"UPDATE_REL_STYLE",74:"UPDATE_LAYOUT_CONFIG",76:"STR",77:"STR_KEY",78:"STR_VALUE",79:"ATTRIBUTE",80:"ATTRIBUTE_EMPTY"},productions_:[0,[3,1],[3,1],[5,1],[5,1],[5,1],[5,1],[4,1],[10,4],[10,4],[10,4],[10,4],[10,4],[13,1],[13,1],[13,2],[19,1],[19,2],[19,3],[21,1],[21,1],[21,2],[21,2],[21,1],[29,3],[30,3],[30,3],[30,4],[32,2],[32,2],[32,2],[32,2],[32,2],[32,2],[32,2],[31,1],[20,1],[20,2],[20,3],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,1],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[35,1],[35,2],[75,1],[75,2],[75,1],[75,1]],performAction:s(function(le,he,Te,oe,G,X,ee){var q=X.length-1;switch(G){case 3:oe.setDirection("TB");break;case 4:oe.setDirection("BT");break;case 5:oe.setDirection("RL");break;case 6:oe.setDirection("LR");break;case 8:case 9:case 10:case 11:case 12:oe.setC4Type(X[q-3]);break;case 19:oe.setTitle(X[q].substring(6)),this.$=X[q].substring(6);break;case 20:oe.setAccDescription(X[q].substring(15)),this.$=X[q].substring(15);break;case 21:this.$=X[q].trim(),oe.setTitle(this.$);break;case 22:case 23:this.$=X[q].trim(),oe.setAccDescription(this.$);break;case 28:X[q].splice(2,0,"ENTERPRISE"),oe.addPersonOrSystemBoundary(...X[q]),this.$=X[q];break;case 29:X[q].splice(2,0,"SYSTEM"),oe.addPersonOrSystemBoundary(...X[q]),this.$=X[q];break;case 30:oe.addPersonOrSystemBoundary(...X[q]),this.$=X[q];break;case 31:X[q].splice(2,0,"CONTAINER"),oe.addContainerBoundary(...X[q]),this.$=X[q];break;case 32:oe.addDeploymentNode("node",...X[q]),this.$=X[q];break;case 33:oe.addDeploymentNode("nodeL",...X[q]),this.$=X[q];break;case 34:oe.addDeploymentNode("nodeR",...X[q]),this.$=X[q];break;case 35:oe.popBoundaryParseStack();break;case 39:oe.addPersonOrSystem("person",...X[q]),this.$=X[q];break;case 40:oe.addPersonOrSystem("external_person",...X[q]),this.$=X[q];break;case 41:oe.addPersonOrSystem("system",...X[q]),this.$=X[q];break;case 42:oe.addPersonOrSystem("system_db",...X[q]),this.$=X[q];break;case 43:oe.addPersonOrSystem("system_queue",...X[q]),this.$=X[q];break;case 44:oe.addPersonOrSystem("external_system",...X[q]),this.$=X[q];break;case 45:oe.addPersonOrSystem("external_system_db",...X[q]),this.$=X[q];break;case 46:oe.addPersonOrSystem("external_system_queue",...X[q]),this.$=X[q];break;case 47:oe.addContainer("container",...X[q]),this.$=X[q];break;case 48:oe.addContainer("container_db",...X[q]),this.$=X[q];break;case 49:oe.addContainer("container_queue",...X[q]),this.$=X[q];break;case 50:oe.addContainer("external_container",...X[q]),this.$=X[q];break;case 51:oe.addContainer("external_container_db",...X[q]),this.$=X[q];break;case 52:oe.addContainer("external_container_queue",...X[q]),this.$=X[q];break;case 53:oe.addComponent("component",...X[q]),this.$=X[q];break;case 54:oe.addComponent("component_db",...X[q]),this.$=X[q];break;case 55:oe.addComponent("component_queue",...X[q]),this.$=X[q];break;case 56:oe.addComponent("external_component",...X[q]),this.$=X[q];break;case 57:oe.addComponent("external_component_db",...X[q]),this.$=X[q];break;case 58:oe.addComponent("external_component_queue",...X[q]),this.$=X[q];break;case 60:oe.addRel("rel",...X[q]),this.$=X[q];break;case 61:oe.addRel("birel",...X[q]),this.$=X[q];break;case 62:oe.addRel("rel_u",...X[q]),this.$=X[q];break;case 63:oe.addRel("rel_d",...X[q]),this.$=X[q];break;case 64:oe.addRel("rel_l",...X[q]),this.$=X[q];break;case 65:oe.addRel("rel_r",...X[q]),this.$=X[q];break;case 66:oe.addRel("rel_b",...X[q]),this.$=X[q];break;case 67:X[q].splice(0,1),oe.addRel("rel",...X[q]),this.$=X[q];break;case 68:oe.updateElStyle("update_el_style",...X[q]),this.$=X[q];break;case 69:oe.updateRelStyle("update_rel_style",...X[q]),this.$=X[q];break;case 70:oe.updateLayoutConfig("update_layout_config",...X[q]),this.$=X[q];break;case 71:this.$=[X[q]];break;case 72:X[q].unshift(X[q-1]),this.$=X[q];break;case 73:case 75:this.$=X[q].trim();break;case 74:let ie={};ie[X[q-1].trim()]=X[q].trim(),this.$=ie;break;case 76:this.$="";break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],7:[1,6],8:[1,7],9:[1,8],10:4,11:[1,9],15:[1,10],16:[1,11],17:[1,12],18:[1,13]},{1:[3]},{1:[2,1]},{1:[2,2]},{1:[2,7]},{1:[2,3]},{1:[2,4]},{1:[2,5]},{1:[2,6]},{12:[1,14]},{12:[1,15]},{12:[1,16]},{12:[1,17]},{12:[1,18]},{13:19,19:20,20:21,21:22,22:t,23:r,24:n,26:i,28:a,29:49,30:61,32:62,34:o,36:l,37:u,38:h,39:d,40:f,41:p,43:23,44:m,45:g,46:y,47:v,48:x,49:b,50:T,51:w,52:C,53:S,54:k,55:R,56:M,57:P,58:N,59:_,60:A,61:I,62:D,63:L,64:B,65:O,66:$,67:W,68:V,69:z,70:H,71:K,72:J,73:ne,74:Z},{13:70,19:20,20:21,21:22,22:t,23:r,24:n,26:i,28:a,29:49,30:61,32:62,34:o,36:l,37:u,38:h,39:d,40:f,41:p,43:23,44:m,45:g,46:y,47:v,48:x,49:b,50:T,51:w,52:C,53:S,54:k,55:R,56:M,57:P,58:N,59:_,60:A,61:I,62:D,63:L,64:B,65:O,66:$,67:W,68:V,69:z,70:H,71:K,72:J,73:ne,74:Z},{13:71,19:20,20:21,21:22,22:t,23:r,24:n,26:i,28:a,29:49,30:61,32:62,34:o,36:l,37:u,38:h,39:d,40:f,41:p,43:23,44:m,45:g,46:y,47:v,48:x,49:b,50:T,51:w,52:C,53:S,54:k,55:R,56:M,57:P,58:N,59:_,60:A,61:I,62:D,63:L,64:B,65:O,66:$,67:W,68:V,69:z,70:H,71:K,72:J,73:ne,74:Z},{13:72,19:20,20:21,21:22,22:t,23:r,24:n,26:i,28:a,29:49,30:61,32:62,34:o,36:l,37:u,38:h,39:d,40:f,41:p,43:23,44:m,45:g,46:y,47:v,48:x,49:b,50:T,51:w,52:C,53:S,54:k,55:R,56:M,57:P,58:N,59:_,60:A,61:I,62:D,63:L,64:B,65:O,66:$,67:W,68:V,69:z,70:H,71:K,72:J,73:ne,74:Z},{13:73,19:20,20:21,21:22,22:t,23:r,24:n,26:i,28:a,29:49,30:61,32:62,34:o,36:l,37:u,38:h,39:d,40:f,41:p,43:23,44:m,45:g,46:y,47:v,48:x,49:b,50:T,51:w,52:C,53:S,54:k,55:R,56:M,57:P,58:N,59:_,60:A,61:I,62:D,63:L,64:B,65:O,66:$,67:W,68:V,69:z,70:H,71:K,72:J,73:ne,74:Z},{14:[1,74]},e(pe,[2,13],{43:23,29:49,30:61,32:62,20:75,34:o,36:l,37:u,38:h,39:d,40:f,41:p,44:m,45:g,46:y,47:v,48:x,49:b,50:T,51:w,52:C,53:S,54:k,55:R,56:M,57:P,58:N,59:_,60:A,61:I,62:D,63:L,64:B,65:O,66:$,67:W,68:V,69:z,70:H,71:K,72:J,73:ne,74:Z}),e(pe,[2,14]),e(te,[2,16],{12:[1,76]}),e(pe,[2,36],{12:[1,77]}),e(fe,[2,19]),e(fe,[2,20]),{25:[1,78]},{27:[1,79]},e(fe,[2,23]),{35:80,75:81,76:ce,77:de,79:Ee,80:we},{35:86,75:81,76:ce,77:de,79:Ee,80:we},{35:87,75:81,76:ce,77:de,79:Ee,80:we},{35:88,75:81,76:ce,77:de,79:Ee,80:we},{35:89,75:81,76:ce,77:de,79:Ee,80:we},{35:90,75:81,76:ce,77:de,79:Ee,80:we},{35:91,75:81,76:ce,77:de,79:Ee,80:we},{35:92,75:81,76:ce,77:de,79:Ee,80:we},{35:93,75:81,76:ce,77:de,79:Ee,80:we},{35:94,75:81,76:ce,77:de,79:Ee,80:we},{35:95,75:81,76:ce,77:de,79:Ee,80:we},{35:96,75:81,76:ce,77:de,79:Ee,80:we},{35:97,75:81,76:ce,77:de,79:Ee,80:we},{35:98,75:81,76:ce,77:de,79:Ee,80:we},{35:99,75:81,76:ce,77:de,79:Ee,80:we},{35:100,75:81,76:ce,77:de,79:Ee,80:we},{35:101,75:81,76:ce,77:de,79:Ee,80:we},{35:102,75:81,76:ce,77:de,79:Ee,80:we},{35:103,75:81,76:ce,77:de,79:Ee,80:we},{35:104,75:81,76:ce,77:de,79:Ee,80:we},e(Fe,[2,59]),{35:105,75:81,76:ce,77:de,79:Ee,80:we},{35:106,75:81,76:ce,77:de,79:Ee,80:we},{35:107,75:81,76:ce,77:de,79:Ee,80:we},{35:108,75:81,76:ce,77:de,79:Ee,80:we},{35:109,75:81,76:ce,77:de,79:Ee,80:we},{35:110,75:81,76:ce,77:de,79:Ee,80:we},{35:111,75:81,76:ce,77:de,79:Ee,80:we},{35:112,75:81,76:ce,77:de,79:Ee,80:we},{35:113,75:81,76:ce,77:de,79:Ee,80:we},{35:114,75:81,76:ce,77:de,79:Ee,80:we},{35:115,75:81,76:ce,77:de,79:Ee,80:we},{20:116,29:49,30:61,32:62,34:o,36:l,37:u,38:h,39:d,40:f,41:p,43:23,44:m,45:g,46:y,47:v,48:x,49:b,50:T,51:w,52:C,53:S,54:k,55:R,56:M,57:P,58:N,59:_,60:A,61:I,62:D,63:L,64:B,65:O,66:$,67:W,68:V,69:z,70:H,71:K,72:J,73:ne,74:Z},{12:[1,118],33:[1,117]},{35:119,75:81,76:ce,77:de,79:Ee,80:we},{35:120,75:81,76:ce,77:de,79:Ee,80:we},{35:121,75:81,76:ce,77:de,79:Ee,80:we},{35:122,75:81,76:ce,77:de,79:Ee,80:we},{35:123,75:81,76:ce,77:de,79:Ee,80:we},{35:124,75:81,76:ce,77:de,79:Ee,80:we},{35:125,75:81,76:ce,77:de,79:Ee,80:we},{14:[1,126]},{14:[1,127]},{14:[1,128]},{14:[1,129]},{1:[2,8]},e(pe,[2,15]),e(te,[2,17],{21:22,19:130,22:t,23:r,24:n,26:i,28:a}),e(pe,[2,37],{19:20,20:21,21:22,43:23,29:49,30:61,32:62,13:131,22:t,23:r,24:n,26:i,28:a,34:o,36:l,37:u,38:h,39:d,40:f,41:p,44:m,45:g,46:y,47:v,48:x,49:b,50:T,51:w,52:C,53:S,54:k,55:R,56:M,57:P,58:N,59:_,60:A,61:I,62:D,63:L,64:B,65:O,66:$,67:W,68:V,69:z,70:H,71:K,72:J,73:ne,74:Z}),e(fe,[2,21]),e(fe,[2,22]),e(Fe,[2,39]),e(Ye,[2,71],{75:81,35:132,76:ce,77:de,79:Ee,80:we}),e(He,[2,73]),{78:[1,133]},e(He,[2,75]),e(He,[2,76]),e(Fe,[2,40]),e(Fe,[2,41]),e(Fe,[2,42]),e(Fe,[2,43]),e(Fe,[2,44]),e(Fe,[2,45]),e(Fe,[2,46]),e(Fe,[2,47]),e(Fe,[2,48]),e(Fe,[2,49]),e(Fe,[2,50]),e(Fe,[2,51]),e(Fe,[2,52]),e(Fe,[2,53]),e(Fe,[2,54]),e(Fe,[2,55]),e(Fe,[2,56]),e(Fe,[2,57]),e(Fe,[2,58]),e(Fe,[2,60]),e(Fe,[2,61]),e(Fe,[2,62]),e(Fe,[2,63]),e(Fe,[2,64]),e(Fe,[2,65]),e(Fe,[2,66]),e(Fe,[2,67]),e(Fe,[2,68]),e(Fe,[2,69]),e(Fe,[2,70]),{31:134,42:[1,135]},{12:[1,136]},{33:[1,137]},e(ye,[2,28]),e(ye,[2,29]),e(ye,[2,30]),e(ye,[2,31]),e(ye,[2,32]),e(ye,[2,33]),e(ye,[2,34]),{1:[2,9]},{1:[2,10]},{1:[2,11]},{1:[2,12]},e(te,[2,18]),e(pe,[2,38]),e(Ye,[2,72]),e(He,[2,74]),e(Fe,[2,24]),e(Fe,[2,35]),e(Be,[2,25]),e(Be,[2,26],{12:[1,138]}),e(Be,[2,27])],defaultActions:{2:[2,1],3:[2,2],4:[2,7],5:[2,3],6:[2,4],7:[2,5],8:[2,6],74:[2,8],126:[2,9],127:[2,10],128:[2,11],129:[2,12]},parseError:s(function(le,he){if(he.recoverable)this.trace(le);else{var Te=new Error(le);throw Te.hash=he,Te}},"parseError"),parse:s(function(le){var he=this,Te=[0],oe=[],G=[null],X=[],ee=this.table,q="",ie=0,j=0,U=0,Y=2,Ae=1,me=X.slice.call(arguments,1),Se=Object.create(this.lexer),Ce={yy:{}};for(var Me in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Me)&&(Ce.yy[Me]=this.yy[Me]);Se.setInput(le,Ce.yy),Ce.yy.lexer=Se,Ce.yy.parser=this,typeof Se.yylloc>"u"&&(Se.yylloc={});var _e=Se.yylloc;X.push(_e);var ze=Se.options&&Se.options.ranges;typeof Ce.yy.parseError=="function"?this.parseError=Ce.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Le(Ke){Te.length=Te.length-2*Ke,G.length=G.length-Ke,X.length=X.length-Ke}s(Le,"popStack");function qe(){var Ke;return Ke=oe.pop()||Se.lex()||Ae,typeof Ke!="number"&&(Ke instanceof Array&&(oe=Ke,Ke=oe.pop()),Ke=he.symbols_[Ke]||Ke),Ke}s(qe,"lex");for(var ve,lt,Oe,mt,At,Gt,Ct={},Rt,gt,be,ct;;){if(Oe=Te[Te.length-1],this.defaultActions[Oe]?mt=this.defaultActions[Oe]:((ve===null||typeof ve>"u")&&(ve=qe()),mt=ee[Oe]&&ee[Oe][ve]),typeof mt>"u"||!mt.length||!mt[0]){var xt="";ct=[];for(Rt in ee[Oe])this.terminals_[Rt]&&Rt>Y&&ct.push("'"+this.terminals_[Rt]+"'");Se.showPosition?xt="Parse error on line "+(ie+1)+`: +`+Se.showPosition()+` +Expecting `+ct.join(", ")+", got '"+(this.terminals_[ve]||ve)+"'":xt="Parse error on line "+(ie+1)+": Unexpected "+(ve==Ae?"end of input":"'"+(this.terminals_[ve]||ve)+"'"),this.parseError(xt,{text:Se.match,token:this.terminals_[ve]||ve,line:Se.yylineno,loc:_e,expected:ct})}if(mt[0]instanceof Array&&mt.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Oe+", token: "+ve);switch(mt[0]){case 1:Te.push(ve),G.push(Se.yytext),X.push(Se.yylloc),Te.push(mt[1]),ve=null,lt?(ve=lt,lt=null):(j=Se.yyleng,q=Se.yytext,ie=Se.yylineno,_e=Se.yylloc,U>0&&U--);break;case 2:if(gt=this.productions_[mt[1]][1],Ct.$=G[G.length-gt],Ct._$={first_line:X[X.length-(gt||1)].first_line,last_line:X[X.length-1].last_line,first_column:X[X.length-(gt||1)].first_column,last_column:X[X.length-1].last_column},ze&&(Ct._$.range=[X[X.length-(gt||1)].range[0],X[X.length-1].range[1]]),Gt=this.performAction.apply(Ct,[q,j,ie,Ce.yy,mt[1],G,X].concat(me)),typeof Gt<"u")return Gt;gt&&(Te=Te.slice(0,-1*gt*2),G=G.slice(0,-1*gt),X=X.slice(0,-1*gt)),Te.push(this.productions_[mt[1]][0]),G.push(Ct.$),X.push(Ct._$),be=ee[Te[Te.length-2]][Te[Te.length-1]],Te.push(be);break;case 3:return!0}}return!0},"parse")},Ne=(function(){var ue={EOF:1,parseError:s(function(he,Te){if(this.yy.parser)this.yy.parser.parseError(he,Te);else throw new Error(he)},"parseError"),setInput:s(function(le,he){return this.yy=he||this.yy||{},this._input=le,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var le=this._input[0];this.yytext+=le,this.yyleng++,this.offset++,this.match+=le,this.matched+=le;var he=le.match(/(?:\r\n?|\n).*/g);return he?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),le},"input"),unput:s(function(le){var he=le.length,Te=le.split(/(?:\r\n?|\n)/g);this._input=le+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-he),this.offset-=he;var oe=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),Te.length-1&&(this.yylineno-=Te.length-1);var G=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:Te?(Te.length===oe.length?this.yylloc.first_column:0)+oe[oe.length-Te.length].length-Te[0].length:this.yylloc.first_column-he},this.options.ranges&&(this.yylloc.range=[G[0],G[0]+this.yyleng-he]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(le){this.unput(this.match.slice(le))},"less"),pastInput:s(function(){var le=this.matched.substr(0,this.matched.length-this.match.length);return(le.length>20?"...":"")+le.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var le=this.match;return le.length<20&&(le+=this._input.substr(0,20-le.length)),(le.substr(0,20)+(le.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var le=this.pastInput(),he=new Array(le.length+1).join("-");return le+this.upcomingInput()+` +`+he+"^"},"showPosition"),test_match:s(function(le,he){var Te,oe,G;if(this.options.backtrack_lexer&&(G={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(G.yylloc.range=this.yylloc.range.slice(0))),oe=le[0].match(/(?:\r\n?|\n).*/g),oe&&(this.yylineno+=oe.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:oe?oe[oe.length-1].length-oe[oe.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+le[0].length},this.yytext+=le[0],this.match+=le[0],this.matches=le,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(le[0].length),this.matched+=le[0],Te=this.performAction.call(this,this.yy,this,he,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),Te)return Te;if(this._backtrack){for(var X in G)this[X]=G[X];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var le,he,Te,oe;this._more||(this.yytext="",this.match="");for(var G=this._currentRules(),X=0;Xhe[0].length)){if(he=Te,oe=X,this.options.backtrack_lexer){if(le=this.test_match(Te,G[X]),le!==!1)return le;if(this._backtrack){he=!1;continue}else return!1}else if(!this.options.flex)break}return he?(le=this.test_match(he,G[oe]),le!==!1?le:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var he=this.next();return he||this.lex()},"lex"),begin:s(function(he){this.conditionStack.push(he)},"begin"),popState:s(function(){var he=this.conditionStack.length-1;return he>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(he){return he=this.conditionStack.length-1-Math.abs(he||0),he>=0?this.conditionStack[he]:"INITIAL"},"topState"),pushState:s(function(he){this.begin(he)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:s(function(he,Te,oe,G){var X=G;switch(oe){case 0:return 6;case 1:return 7;case 2:return 8;case 3:return 9;case 4:return 22;case 5:return 23;case 6:return this.begin("acc_title"),24;break;case 7:return this.popState(),"acc_title_value";break;case 8:return this.begin("acc_descr"),26;break;case 9:return this.popState(),"acc_descr_value";break;case 10:this.begin("acc_descr_multiline");break;case 11:this.popState();break;case 12:return"acc_descr_multiline_value";case 13:break;case 14:c;break;case 15:return 12;case 16:break;case 17:return 11;case 18:return 15;case 19:return 16;case 20:return 17;case 21:return 18;case 22:return this.begin("person_ext"),45;break;case 23:return this.begin("person"),44;break;case 24:return this.begin("system_ext_queue"),51;break;case 25:return this.begin("system_ext_db"),50;break;case 26:return this.begin("system_ext"),49;break;case 27:return this.begin("system_queue"),48;break;case 28:return this.begin("system_db"),47;break;case 29:return this.begin("system"),46;break;case 30:return this.begin("boundary"),37;break;case 31:return this.begin("enterprise_boundary"),34;break;case 32:return this.begin("system_boundary"),36;break;case 33:return this.begin("container_ext_queue"),57;break;case 34:return this.begin("container_ext_db"),56;break;case 35:return this.begin("container_ext"),55;break;case 36:return this.begin("container_queue"),54;break;case 37:return this.begin("container_db"),53;break;case 38:return this.begin("container"),52;break;case 39:return this.begin("container_boundary"),38;break;case 40:return this.begin("component_ext_queue"),63;break;case 41:return this.begin("component_ext_db"),62;break;case 42:return this.begin("component_ext"),61;break;case 43:return this.begin("component_queue"),60;break;case 44:return this.begin("component_db"),59;break;case 45:return this.begin("component"),58;break;case 46:return this.begin("node"),39;break;case 47:return this.begin("node"),39;break;case 48:return this.begin("node_l"),40;break;case 49:return this.begin("node_r"),41;break;case 50:return this.begin("rel"),64;break;case 51:return this.begin("birel"),65;break;case 52:return this.begin("rel_u"),66;break;case 53:return this.begin("rel_u"),66;break;case 54:return this.begin("rel_d"),67;break;case 55:return this.begin("rel_d"),67;break;case 56:return this.begin("rel_l"),68;break;case 57:return this.begin("rel_l"),68;break;case 58:return this.begin("rel_r"),69;break;case 59:return this.begin("rel_r"),69;break;case 60:return this.begin("rel_b"),70;break;case 61:return this.begin("rel_index"),71;break;case 62:return this.begin("update_el_style"),72;break;case 63:return this.begin("update_rel_style"),73;break;case 64:return this.begin("update_layout_config"),74;break;case 65:return"EOF_IN_STRUCT";case 66:return this.begin("attribute"),"ATTRIBUTE_EMPTY";break;case 67:this.begin("attribute");break;case 68:this.popState(),this.popState();break;case 69:return 80;case 70:break;case 71:return 80;case 72:this.begin("string");break;case 73:this.popState();break;case 74:return"STR";case 75:this.begin("string_kv");break;case 76:return this.begin("string_kv_key"),"STR_KEY";break;case 77:this.popState(),this.begin("string_kv_value");break;case 78:return"STR_VALUE";case 79:this.popState(),this.popState();break;case 80:return"STR";case 81:return"LBRACE";case 82:return"RBRACE";case 83:return"SPACE";case 84:return"EOL";case 85:return 14}},"anonymous"),rules:[/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:title\s[^#\n;]+)/,/^(?:accDescription\s[^#\n;]+)/,/^(?:accTitle\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*\{\s*)/,/^(?:[\}])/,/^(?:[^\}]*)/,/^(?:%%(?!\{)*[^\n]*(\r?\n?)+)/,/^(?:%%[^\n]*(\r?\n)*)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:C4Context\b)/,/^(?:C4Container\b)/,/^(?:C4Component\b)/,/^(?:C4Dynamic\b)/,/^(?:C4Deployment\b)/,/^(?:Person_Ext\b)/,/^(?:Person\b)/,/^(?:SystemQueue_Ext\b)/,/^(?:SystemDb_Ext\b)/,/^(?:System_Ext\b)/,/^(?:SystemQueue\b)/,/^(?:SystemDb\b)/,/^(?:System\b)/,/^(?:Boundary\b)/,/^(?:Enterprise_Boundary\b)/,/^(?:System_Boundary\b)/,/^(?:ContainerQueue_Ext\b)/,/^(?:ContainerDb_Ext\b)/,/^(?:Container_Ext\b)/,/^(?:ContainerQueue\b)/,/^(?:ContainerDb\b)/,/^(?:Container\b)/,/^(?:Container_Boundary\b)/,/^(?:ComponentQueue_Ext\b)/,/^(?:ComponentDb_Ext\b)/,/^(?:Component_Ext\b)/,/^(?:ComponentQueue\b)/,/^(?:ComponentDb\b)/,/^(?:Component\b)/,/^(?:Deployment_Node\b)/,/^(?:Node\b)/,/^(?:Node_L\b)/,/^(?:Node_R\b)/,/^(?:Rel\b)/,/^(?:BiRel\b)/,/^(?:Rel_Up\b)/,/^(?:Rel_U\b)/,/^(?:Rel_Down\b)/,/^(?:Rel_D\b)/,/^(?:Rel_Left\b)/,/^(?:Rel_L\b)/,/^(?:Rel_Right\b)/,/^(?:Rel_R\b)/,/^(?:Rel_Back\b)/,/^(?:RelIndex\b)/,/^(?:UpdateElementStyle\b)/,/^(?:UpdateRelStyle\b)/,/^(?:UpdateLayoutConfig\b)/,/^(?:$)/,/^(?:[(][ ]*[,])/,/^(?:[(])/,/^(?:[)])/,/^(?:,,)/,/^(?:,)/,/^(?:[ ]*["]["])/,/^(?:[ ]*["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:[ ]*[\$])/,/^(?:[^=]*)/,/^(?:[=][ ]*["])/,/^(?:[^"]+)/,/^(?:["])/,/^(?:[^,]+)/,/^(?:\{)/,/^(?:\})/,/^(?:[\s]+)/,/^(?:[\n\r]+)/,/^(?:$)/],conditions:{acc_descr_multiline:{rules:[11,12],inclusive:!1},acc_descr:{rules:[9],inclusive:!1},acc_title:{rules:[7],inclusive:!1},string_kv_value:{rules:[78,79],inclusive:!1},string_kv_key:{rules:[77],inclusive:!1},string_kv:{rules:[76],inclusive:!1},string:{rules:[73,74],inclusive:!1},attribute:{rules:[68,69,70,71,72,75,80],inclusive:!1},update_layout_config:{rules:[65,66,67,68],inclusive:!1},update_rel_style:{rules:[65,66,67,68],inclusive:!1},update_el_style:{rules:[65,66,67,68],inclusive:!1},rel_b:{rules:[65,66,67,68],inclusive:!1},rel_r:{rules:[65,66,67,68],inclusive:!1},rel_l:{rules:[65,66,67,68],inclusive:!1},rel_d:{rules:[65,66,67,68],inclusive:!1},rel_u:{rules:[65,66,67,68],inclusive:!1},rel_bi:{rules:[],inclusive:!1},rel:{rules:[65,66,67,68],inclusive:!1},node_r:{rules:[65,66,67,68],inclusive:!1},node_l:{rules:[65,66,67,68],inclusive:!1},node:{rules:[65,66,67,68],inclusive:!1},index:{rules:[],inclusive:!1},rel_index:{rules:[65,66,67,68],inclusive:!1},component_ext_queue:{rules:[65,66,67,68],inclusive:!1},component_ext_db:{rules:[65,66,67,68],inclusive:!1},component_ext:{rules:[65,66,67,68],inclusive:!1},component_queue:{rules:[65,66,67,68],inclusive:!1},component_db:{rules:[65,66,67,68],inclusive:!1},component:{rules:[65,66,67,68],inclusive:!1},container_boundary:{rules:[65,66,67,68],inclusive:!1},container_ext_queue:{rules:[65,66,67,68],inclusive:!1},container_ext_db:{rules:[65,66,67,68],inclusive:!1},container_ext:{rules:[65,66,67,68],inclusive:!1},container_queue:{rules:[65,66,67,68],inclusive:!1},container_db:{rules:[65,66,67,68],inclusive:!1},container:{rules:[65,66,67,68],inclusive:!1},birel:{rules:[65,66,67,68],inclusive:!1},system_boundary:{rules:[65,66,67,68],inclusive:!1},enterprise_boundary:{rules:[65,66,67,68],inclusive:!1},boundary:{rules:[65,66,67,68],inclusive:!1},system_ext_queue:{rules:[65,66,67,68],inclusive:!1},system_ext_db:{rules:[65,66,67,68],inclusive:!1},system_ext:{rules:[65,66,67,68],inclusive:!1},system_queue:{rules:[65,66,67,68],inclusive:!1},system_db:{rules:[65,66,67,68],inclusive:!1},system:{rules:[65,66,67,68],inclusive:!1},person_ext:{rules:[65,66,67,68],inclusive:!1},person:{rules:[65,66,67,68],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,8,10,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,81,82,83,84,85],inclusive:!0}}};return ue})();Ie.lexer=Ne;function re(){this.yy={}}return s(re,"Parser"),re.prototype=Ie,Ie.Parser=re,new re})();rb.parser=rb;yJ=rb});var GWe,zWe,jr,ml,Vn=F(()=>{"use strict";St();GWe=s(function(e,t){for(let r of t)e.attr(r[0],r[1])},"d3Attrs"),zWe=s(function(e,t,r){let n=new Map;return r?(n.set("width","100%"),n.set("style",`max-width: ${t}px;`)):(n.set("height",e),n.set("width",t)),n},"calculateSvgSizeAttrs"),jr=s(function(e,t,r,n){let i=zWe(t,r,n);GWe(e,i)},"configureSvgSize"),ml=s(function(e,t,r,n){let i=t.node().getBBox(),a=i.width,o=i.height;se.info(`SVG bounds: ${a}x${o}`,i);let l=0,u=0;se.info(`Graph bounds: ${l}x${u}`,e),l=a+r*2,u=o+r*2,se.info(`Calculated bounds: ${l}x${u}`),jr(t,u,l,n);let h=`${i.x-r} ${i.y-r} ${i.width+2*r} ${i.height+2*r}`;t.attr("viewBox",h)},"setupGraphViewbox")});function Z7(e){return[...e.cssRules].map(t=>t.cssText).join(` +`)}var dk,VWe,vJ,xJ,Q7=F(()=>{"use strict";St();dk={};s(Z7,"cssStyleSheetToString");VWe=s((e,t,r,n)=>{let i="";return e in dk&&dk[e]?i=dk[e]({...r,svgId:n}):se.warn(`No theme found for ${e}`),`& { + font-family: ${r.fontFamily}; + font-size: ${r.fontSize}; + fill: ${r.textColor} + } + @keyframes edge-animation-frame { + from { + stroke-dashoffset: 0; + } + } + @keyframes dash { + to { + stroke-dashoffset: 0; + } + } + & .edge-animation-slow { + stroke-dasharray: 9,5 !important; + stroke-dashoffset: 900; + animation: dash 50s linear infinite; + stroke-linecap: round; + } + & .edge-animation-fast { + stroke-dasharray: 9,5 !important; + stroke-dashoffset: 900; + animation: dash 20s linear infinite; + stroke-linecap: round; + } + /* Classes common for multiple diagrams */ + + & .error-icon { + fill: ${r.errorBkgColor}; + } + & .error-text { + fill: ${r.errorTextColor}; + stroke: ${r.errorTextColor}; + } + + & .edge-thickness-normal { + stroke-width: ${r.strokeWidth??1}px; + } + & .edge-thickness-thick { + stroke-width: 3.5px + } + & .edge-pattern-solid { + stroke-dasharray: 0; + } + & .edge-thickness-invisible { + stroke-width: 0; + fill: none; + } + & .edge-pattern-dashed{ + stroke-dasharray: 3; + } + .edge-pattern-dotted { + stroke-dasharray: 2; + } + + & .marker { + fill: ${r.lineColor}; + stroke: ${r.lineColor}; + } + & .marker.cross { + stroke: ${r.lineColor}; + } + + & svg { + font-family: ${r.fontFamily}; + font-size: ${r.fontSize}; + } + & p { + margin: 0 + } + + ${i} + .node .neo-node { + stroke: ${r.nodeBorder}; + } + + [data-look="neo"].node rect, [data-look="neo"].cluster rect, [data-look="neo"].node polygon { + stroke: ${r.useGradient?"url("+n+"-gradient)":r.nodeBorder}; + filter: ${r.dropShadow?r.dropShadow.replace("url(#drop-shadow)",`url(${n}-drop-shadow)`):"none"}; + } + [data-look="neo"].swimlane.cluster rect { + filter: none; + } + + + [data-look="neo"].node path { + stroke: ${r.useGradient?"url("+n+"-gradient)":r.nodeBorder}; + stroke-width: ${r.strokeWidth??1}px; + } + + [data-look="neo"].node .outer-path { + filter: ${r.dropShadow?r.dropShadow.replace("url(#drop-shadow)",`url(${n}-drop-shadow)`):"none"}; + } + + [data-look="neo"].node .neo-line path { + stroke: ${r.nodeBorder}; + filter: none; + } + + [data-look="neo"].node circle{ + stroke: ${r.useGradient?"url("+n+"-gradient)":r.nodeBorder}; + filter: ${r.dropShadow?r.dropShadow.replace("url(#drop-shadow)",`url(${n}-drop-shadow)`):"none"}; + } + + [data-look="neo"].node circle .state-start{ + fill: #000000; + } + + [data-look="neo"].icon-shape .icon { + fill: ${r.useGradient?"url("+n+"-gradient)":r.nodeBorder}; + filter: ${r.dropShadow?r.dropShadow.replace("url(#drop-shadow)",`url(${n}-drop-shadow)`):"none"}; + } + + [data-look="neo"].icon-shape .icon-neo path { + stroke: ${r.useGradient?"url("+n+"-gradient)":r.nodeBorder}; + filter: ${r.dropShadow?r.dropShadow.replace("url(#drop-shadow)",`url(${n}-drop-shadow)`):"none"}; + } + + ${t} +`},"getStyles"),vJ=s((e,t)=>{t!==void 0&&(dk[e]=t)},"addStylesForDiagram"),xJ=VWe});var nb={};sr(nb,{clear:()=>Tr,getAccDescription:()=>Nr,getAccTitle:()=>Ir,getDiagramTitle:()=>Pr,setAccDescription:()=>Mr,setAccTitle:()=>Lr,setDiagramTitle:()=>Gr});var J7,eI,tI,rI,Tr,Lr,Ir,Mr,Nr,Gr,Pr,$n=F(()=>{"use strict";Yr();br();J7="",eI="",tI="",rI=s(e=>xr(e,Lt()),"sanitizeText"),Tr=s(()=>{J7="",tI="",eI=""},"clear"),Lr=s(e=>{J7=rI(e).replace(/^\s+/g,"")},"setAccTitle"),Ir=s(()=>J7,"getAccTitle"),Mr=s(e=>{tI=rI(e).replace(/\n\s+/g,` +`)},"setAccDescription"),Nr=s(()=>tI,"getAccDescription"),Gr=s(e=>{eI=rI(e)},"setDiagramTitle"),Pr=s(()=>eI,"getDiagramTitle")});var bJ,WWe,Pe,ib,pk,ab,sb,qWe,fk,Kp,ob,nI,Jt=F(()=>{"use strict";Xp();St();br();Yr();Vn();Q7();$n();bJ=se,WWe=Mx,Pe=Lt,ib=kw,pk=wd,ab=s(e=>xr(e,Pe()),"sanitizeText"),sb=ml,qWe=s(()=>nb,"getCommonDb"),fk={},Kp=s((e,t,r)=>{fk[e]&&bJ.warn(`Diagram with id ${e} already registered. Overwriting.`),fk[e]=t,r&&X7(e,r),vJ(e,t.styles),t.injectUtils?.(bJ,WWe,Pe,ab,sb,qWe(),()=>{})},"registerDiagram"),ob=s(e=>{if(e in fk)return fk[e];throw new nI(e)},"getDiagram"),nI=class extends Error{static{s(this,"DiagramNotFoundError")}constructor(t){super(`Diagram ${t} not found.`)}}});var mk,iI=F(()=>{"use strict";Jt();mk=s(e=>Pe()[e],"getRequiredConfig")});var Zp,TJ,Yl,Pd,Ns,Ul,Wc,lb,aI,sI,gk,yk,CJ,HWe,UWe,YWe,jWe,XWe,KWe,ZWe,QWe,JWe,eqe,tqe,rqe,nqe,iqe,aqe,sqe,oqe,SJ,lqe,cqe,wJ,uqe,hqe,dqe,fqe,Od,pqe,mqe,gqe,yqe,vqe,cb,oI=F(()=>{"use strict";Jt();iI();Yr();$n();Zp=s((e,t)=>{for(let[r,n]of Object.entries(t))if(n!==void 0)if(typeof n=="object"){let[i,a]=Object.entries(n)[0];e[i]=a}else e[r]=n},"assignAttributes"),TJ=s(()=>({alias:"global",label:{text:"global"},type:{text:"global"},tags:null,link:null,parentBoundary:""}),"createGlobalBoundary"),Yl=[],Pd=[""],Ns="global",Ul="",Wc=[TJ()],lb=[],aI="",sI=!1,gk=4,yk=2,HWe=s(function(){return CJ},"getC4Type"),UWe=s(function(e){CJ=xr(e,Pe())},"setC4Type"),YWe=s(function(e,t,r,n,i,a,o,l,u){if(e==null||t===void 0||t===null||r===void 0||r===null||n===void 0||n===null)return;let h={},d=lb.find(f=>f.from===t&&f.to===r);if(d?h=d:lb.push(h),h.type=e,h.from=t,h.to=r,h.label={text:n},i==null)h.techn={text:""};else if(typeof i=="object"){let[f,p]=Object.entries(i)[0];h[f]={text:p}}else h.techn={text:i};if(a==null)h.descr={text:""};else if(typeof a=="object"){let[f,p]=Object.entries(a)[0];h[f]={text:p}}else h.descr={text:a};Zp(h,{sprite:o,tags:l,link:u}),h.wrap=Od()},"addRel"),jWe=s(function(e,t,r,n,i,a,o){if(t===null||r===null)return;let l={},u=Yl.find(h=>h.alias===t);if(u&&t===u.alias?l=u:(l.alias=t,Yl.push(l)),r==null?l.label={text:""}:l.label={text:r},n==null)l.descr={text:""};else if(typeof n=="object"){let[h,d]=Object.entries(n)[0];l[h]={text:d}}else l.descr={text:n};Zp(l,{sprite:i,tags:a,link:o}),l.typeC4Shape={text:e},l.parentBoundary=Ns,l.wrap=Od()},"addPersonOrSystem"),XWe=s(function(e,t,r,n,i,a,o,l){if(t===null||r===null)return;let u={},h=Yl.find(d=>d.alias===t);if(h&&t===h.alias?u=h:(u.alias=t,Yl.push(u)),r==null?u.label={text:""}:u.label={text:r},n==null)u.techn={text:""};else if(typeof n=="object"){let[d,f]=Object.entries(n)[0];u[d]={text:f}}else u.techn={text:n};if(i==null)u.descr={text:""};else if(typeof i=="object"){let[d,f]=Object.entries(i)[0];u[d]={text:f}}else u.descr={text:i};Zp(u,{sprite:a,tags:o,link:l}),u.wrap=Od(),u.typeC4Shape={text:e},u.parentBoundary=Ns},"addContainer"),KWe=s(function(e,t,r,n,i,a,o,l){if(t===null||r===null)return;let u={},h=Yl.find(d=>d.alias===t);if(h&&t===h.alias?u=h:(u.alias=t,Yl.push(u)),r==null?u.label={text:""}:u.label={text:r},n==null)u.techn={text:""};else if(typeof n=="object"){let[d,f]=Object.entries(n)[0];u[d]={text:f}}else u.techn={text:n};if(i==null)u.descr={text:""};else if(typeof i=="object"){let[d,f]=Object.entries(i)[0];u[d]={text:f}}else u.descr={text:i};Zp(u,{sprite:a,tags:o,link:l}),u.wrap=Od(),u.typeC4Shape={text:e},u.parentBoundary=Ns},"addComponent"),ZWe=s(function(e,t,r,n,i){if(e===null||t===null)return;let a={},o=Wc.find(l=>l.alias===e);if(o&&e===o.alias?a=o:(a.alias=e,Wc.push(a)),t==null?a.label={text:""}:a.label={text:t},r==null)a.type={text:"system"};else if(typeof r=="object"){let[l,u]=Object.entries(r)[0];a[l]={text:u}}else a.type={text:r};Zp(a,{tags:n,link:i}),a.parentBoundary=Ns,a.wrap=Od(),Ul=Ns,Ns=e,Pd.push(Ul)},"addPersonOrSystemBoundary"),QWe=s(function(e,t,r,n,i){if(e===null||t===null)return;let a={},o=Wc.find(l=>l.alias===e);if(o&&e===o.alias?a=o:(a.alias=e,Wc.push(a)),t==null?a.label={text:""}:a.label={text:t},r==null)a.type={text:"container"};else if(typeof r=="object"){let[l,u]=Object.entries(r)[0];a[l]={text:u}}else a.type={text:r};Zp(a,{tags:n,link:i}),a.parentBoundary=Ns,a.wrap=Od(),Ul=Ns,Ns=e,Pd.push(Ul)},"addContainerBoundary"),JWe=s(function(e,t,r,n,i,a,o,l){if(t===null||r===null)return;let u={},h=Wc.find(d=>d.alias===t);if(h&&t===h.alias?u=h:(u.alias=t,Wc.push(u)),r==null?u.label={text:""}:u.label={text:r},n==null)u.type={text:"node"};else if(typeof n=="object"){let[d,f]=Object.entries(n)[0];u[d]={text:f}}else u.type={text:n};if(i==null)u.descr={text:""};else if(typeof i=="object"){let[d,f]=Object.entries(i)[0];u[d]={text:f}}else u.descr={text:i};Zp(u,{tags:o,link:l}),u.nodeType=e,u.parentBoundary=Ns,u.wrap=Od(),Ul=Ns,Ns=t,Pd.push(Ul)},"addDeploymentNode"),eqe=s(function(){Ns=Ul,Pd.pop(),Ul=Pd.pop(),Pd.push(Ul)},"popBoundaryParseStack"),tqe=s(function(e,t,r,n,i,a,o,l,u,h,d){let f=Yl.find(p=>p.alias===t);if(!(f===void 0&&(f=Wc.find(p=>p.alias===t),f===void 0))){if(r!=null)if(typeof r=="object"){let[p,m]=Object.entries(r)[0];f[p]=m}else f.bgColor=r;if(n!=null)if(typeof n=="object"){let[p,m]=Object.entries(n)[0];f[p]=m}else f.fontColor=n;if(i!=null)if(typeof i=="object"){let[p,m]=Object.entries(i)[0];f[p]=m}else f.borderColor=i;if(a!=null)if(typeof a=="object"){let[p,m]=Object.entries(a)[0];f[p]=m}else f.shadowing=a;if(o!=null)if(typeof o=="object"){let[p,m]=Object.entries(o)[0];f[p]=m}else f.shape=o;if(l!=null)if(typeof l=="object"){let[p,m]=Object.entries(l)[0];f[p]=m}else f.sprite=l;if(u!=null)if(typeof u=="object"){let[p,m]=Object.entries(u)[0];f[p]=m}else f.techn=u;if(h!=null)if(typeof h=="object"){let[p,m]=Object.entries(h)[0];f[p]=m}else f.legendText=h;if(d!=null)if(typeof d=="object"){let[p,m]=Object.entries(d)[0];f[p]=m}else f.legendSprite=d}},"updateElStyle"),rqe=s(function(e,t,r,n,i,a,o){let l=lb.find(u=>u.from===t&&u.to===r);if(l!==void 0){if(n!=null)if(typeof n=="object"){let[u,h]=Object.entries(n)[0];l[u]=h}else l.textColor=n;if(i!=null)if(typeof i=="object"){let[u,h]=Object.entries(i)[0];l[u]=h}else l.lineColor=i;if(a!=null)if(typeof a=="object"){let[u,h]=Object.entries(a)[0];l[u]=parseInt(h)}else l.offsetX=parseInt(a);if(o!=null)if(typeof o=="object"){let[u,h]=Object.entries(o)[0];l[u]=parseInt(h)}else l.offsetY=parseInt(o)}},"updateRelStyle"),nqe=s(function(e,t,r){let n=gk,i=yk;if(typeof t=="object"){let a=Object.values(t)[0];n=parseInt(a)}else n=parseInt(t);if(typeof r=="object"){let a=Object.values(r)[0];i=parseInt(a)}else i=parseInt(r);n>=1&&(gk=n),i>=1&&(yk=i)},"updateLayoutConfig"),iqe=s(function(){return gk},"getC4ShapeInRow"),aqe=s(function(){return yk},"getC4BoundaryInRow"),sqe=s(function(){return Ns},"getCurrentBoundaryParse"),oqe=s(function(){return Ul},"getParentBoundaryParse"),SJ=s(function(e){return e==null?Yl:Yl.filter(t=>t.parentBoundary===e)},"getC4ShapeArray"),lqe=s(function(e){return Yl.find(t=>t.alias===e)},"getC4Shape"),cqe=s(function(e){return Object.keys(SJ(e))},"getC4ShapeKeys"),wJ=s(function(e){return e==null?Wc:Wc.filter(t=>t.parentBoundary===e)},"getBoundaries"),uqe=wJ,hqe=s(function(){return lb},"getRels"),dqe=s(function(){return aI},"getTitle"),fqe=s(function(e){sI=e},"setWrap"),Od=s(function(){return sI},"autoWrap"),pqe=s(function(){Yl=[],Wc=[TJ()],Ul="",Ns="global",Pd=[""],lb=[],Pd=[""],aI="",sI=!1,gk=4,yk=2},"clear"),mqe={SOLID:0,DOTTED:1,NOTE:2,SOLID_CROSS:3,DOTTED_CROSS:4,SOLID_OPEN:5,DOTTED_OPEN:6,LOOP_START:10,LOOP_END:11,ALT_START:12,ALT_ELSE:13,ALT_END:14,OPT_START:15,OPT_END:16,ACTIVE_START:17,ACTIVE_END:18,PAR_START:19,PAR_AND:20,PAR_END:21,RECT_START:22,RECT_END:23,SOLID_POINT:24,DOTTED_POINT:25},gqe={FILLED:0,OPEN:1},yqe={LEFTOF:0,RIGHTOF:1,OVER:2},vqe=s(function(e){aI=xr(e,Pe())},"setTitle"),cb={addPersonOrSystem:jWe,addPersonOrSystemBoundary:ZWe,addContainer:XWe,addContainerBoundary:QWe,addComponent:KWe,addDeploymentNode:JWe,popBoundaryParseStack:eqe,addRel:YWe,updateElStyle:tqe,updateRelStyle:rqe,updateLayoutConfig:nqe,autoWrap:Od,setWrap:fqe,getC4ShapeArray:SJ,getC4Shape:lqe,getC4ShapeKeys:cqe,getBoundaries:wJ,getBoundarys:uqe,getCurrentBoundaryParse:sqe,getParentBoundaryParse:oqe,getRels:hqe,getTitle:dqe,getC4Type:HWe,getC4ShapeInRow:iqe,getC4BoundaryInRow:aqe,setAccTitle:Lr,getAccTitle:Ir,getAccDescription:Nr,setAccDescription:Mr,getConfig:s(()=>mk("c4"),"getConfig"),clear:pqe,LINETYPE:mqe,ARROWTYPE:gqe,PLACEMENT:yqe,setTitle:vqe,setC4Type:UWe}});var kJ=As(La=>{"use strict";Object.defineProperty(La,"__esModule",{value:!0});La.BLANK_URL=La.relativeFirstCharacters=La.whitespaceEscapeCharsRegex=La.urlSchemeRegex=La.ctrlCharactersRegex=La.htmlCtrlEntityRegex=La.htmlEntitiesRegex=La.invalidProtocolRegex=void 0;La.invalidProtocolRegex=/^([^\w]*)(javascript|data|vbscript)/im;La.htmlEntitiesRegex=/&#(\w+)(^\w|;)?/g;La.htmlCtrlEntityRegex=/&(newline|tab);/gi;La.ctrlCharactersRegex=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim;La.urlSchemeRegex=/^.+(:|:)/gim;La.whitespaceEscapeCharsRegex=/(\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g;La.relativeFirstCharacters=[".","/"];La.BLANK_URL="about:blank"});var ub=As(lI=>{"use strict";Object.defineProperty(lI,"__esModule",{value:!0});lI.sanitizeUrl=Cqe;var ms=kJ();function xqe(e){return ms.relativeFirstCharacters.indexOf(e[0])>-1}s(xqe,"isRelativeUrlWithoutProtocol");function bqe(e){var t=e.replace(ms.ctrlCharactersRegex,"");return t.replace(ms.htmlEntitiesRegex,function(r,n){return String.fromCharCode(n)})}s(bqe,"decodeHtmlCharacters");function Tqe(e){return URL.canParse(e)}s(Tqe,"isValidUrl");function EJ(e){try{return decodeURIComponent(e)}catch{return e}}s(EJ,"decodeURI");function Cqe(e){if(!e)return ms.BLANK_URL;var t,r=EJ(e.trim());do r=bqe(r).replace(ms.htmlCtrlEntityRegex,"").replace(ms.ctrlCharactersRegex,"").replace(ms.whitespaceEscapeCharsRegex,"").trim(),r=EJ(r),t=r.match(ms.ctrlCharactersRegex)||r.match(ms.htmlEntitiesRegex)||r.match(ms.htmlCtrlEntityRegex)||r.match(ms.whitespaceEscapeCharsRegex);while(t&&t.length>0);var n=r;if(!n)return ms.BLANK_URL;if(xqe(n))return n;var i=n.trimStart(),a=i.match(ms.urlSchemeRegex);if(!a)return n;var o=a[0].toLowerCase().trim();if(ms.invalidProtocolRegex.test(o))return ms.BLANK_URL;var l=i.replace(/\\/g,"/");if(o==="mailto:"||o.includes("://"))return l;if(o==="http:"||o==="https:"){if(!Tqe(l))return ms.BLANK_URL;var u=new URL(l);return u.protocol=u.protocol.toLowerCase(),u.hostname=u.hostname.toLowerCase(),u.toString()}return l}s(Cqe,"sanitizeUrl")});function Qp(e,t){return e==null||t==null?NaN:et?1:e>=t?0:NaN}var cI=F(()=>{"use strict";s(Qp,"ascending")});function uI(e,t){return e==null||t==null?NaN:te?1:t>=e?0:NaN}var AJ=F(()=>{"use strict";s(uI,"descending")});function Jp(e){let t,r,n;e.length!==2?(t=Qp,r=s((l,u)=>Qp(e(l),u),"compare2"),n=s((l,u)=>e(l)-u,"delta")):(t=e===Qp||e===uI?e:Sqe,r=e,n=e);function i(l,u,h=0,d=l.length){if(h>>1;r(l[f],u)<0?h=f+1:d=f}while(h>>1;r(l[f],u)<=0?h=f+1:d=f}while(hh&&n(l[f-1],u)>-n(l[f],u)?f-1:f}return s(o,"center"),{left:i,center:o,right:a}}function Sqe(){return 0}var hI=F(()=>{"use strict";cI();AJ();s(Jp,"bisector");s(Sqe,"zero")});function dI(e){return e===null?NaN:+e}var RJ=F(()=>{"use strict";s(dI,"number")});var _J,LJ,wqe,kqe,fI,DJ=F(()=>{"use strict";cI();hI();RJ();_J=Jp(Qp),LJ=_J.right,wqe=_J.left,kqe=Jp(dI).center,fI=LJ});function IJ({_intern:e,_key:t},r){let n=t(r);return e.has(n)?e.get(n):r}function Eqe({_intern:e,_key:t},r){let n=t(r);return e.has(n)?e.get(n):(e.set(n,r),r)}function Aqe({_intern:e,_key:t},r){let n=t(r);return e.has(n)&&(r=e.get(n),e.delete(n)),r}function Rqe(e){return e!==null&&typeof e=="object"?e.valueOf():e}var ny,MJ=F(()=>{"use strict";ny=class extends Map{static{s(this,"InternMap")}constructor(t,r=Rqe){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:r}}),t!=null)for(let[n,i]of t)this.set(n,i)}get(t){return super.get(IJ(this,t))}has(t){return super.has(IJ(this,t))}set(t,r){return super.set(Eqe(this,t),r)}delete(t){return super.delete(Aqe(this,t))}};s(IJ,"intern_get");s(Eqe,"intern_set");s(Aqe,"intern_delete");s(Rqe,"keyof")});function vk(e,t,r){let n=(t-e)/Math.max(0,r),i=Math.floor(Math.log10(n)),a=n/Math.pow(10,i),o=a>=_qe?10:a>=Lqe?5:a>=Dqe?2:1,l,u,h;return i<0?(h=Math.pow(10,-i)/o,l=Math.round(e*h),u=Math.round(t*h),l/ht&&--u,h=-h):(h=Math.pow(10,i)*o,l=Math.round(e/h),u=Math.round(t/h),l*ht&&--u),u0))return[];if(e===t)return[e];let n=t=i))return[];let l=a-i+1,u=new Array(l);if(n)if(o<0)for(let h=0;h{"use strict";_qe=Math.sqrt(50),Lqe=Math.sqrt(10),Dqe=Math.sqrt(2);s(vk,"tickSpec");s(xk,"ticks");s(hb,"tickIncrement");s(iy,"tickStep")});function bk(e,t){let r;if(t===void 0)for(let n of e)n!=null&&(r=n)&&(r=n);else{let n=-1;for(let i of e)(i=t(i,++n,e))!=null&&(r=i)&&(r=i)}return r}var PJ=F(()=>{"use strict";s(bk,"max")});function Tk(e,t){let r;if(t===void 0)for(let n of e)n!=null&&(r>n||r===void 0&&n>=n)&&(r=n);else{let n=-1;for(let i of e)(i=t(i,++n,e))!=null&&(r>i||r===void 0&&i>=i)&&(r=i)}return r}var OJ=F(()=>{"use strict";s(Tk,"min")});function Ck(e,t,r){e=+e,t=+t,r=(i=arguments.length)<2?(t=e,e=0,1):i<3?1:+r;for(var n=-1,i=Math.max(0,Math.ceil((t-e)/r))|0,a=new Array(i);++n{"use strict";s(Ck,"range")});var Bd=F(()=>{"use strict";DJ();hI();PJ();OJ();BJ();NJ();MJ()});function pI(e){return e}var $J=F(()=>{"use strict";s(pI,"default")});function Iqe(e){return"translate("+e+",0)"}function Mqe(e){return"translate(0,"+e+")"}function Nqe(e){return t=>+e(t)}function Pqe(e,t){return t=Math.max(0,e.bandwidth()-t*2)/2,e.round()&&(t=Math.round(t)),r=>+e(r)+t}function Oqe(){return!this.__axis}function GJ(e,t){var r=[],n=null,i=null,a=6,o=6,l=3,u=typeof window<"u"&&window.devicePixelRatio>1?0:.5,h=e===wk||e===Sk?-1:1,d=e===Sk||e===mI?"x":"y",f=e===wk||e===gI?Iqe:Mqe;function p(m){var g=n??(t.ticks?t.ticks.apply(t,r):t.domain()),y=i??(t.tickFormat?t.tickFormat.apply(t,r):pI),v=Math.max(a,0)+l,x=t.range(),b=+x[0]+u,T=+x[x.length-1]+u,w=(t.bandwidth?Pqe:Nqe)(t.copy(),u),C=m.selection?m.selection():m,S=C.selectAll(".domain").data([null]),k=C.selectAll(".tick").data(g,t).order(),R=k.exit(),M=k.enter().append("g").attr("class","tick"),P=k.select("line"),N=k.select("text");S=S.merge(S.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),k=k.merge(M),P=P.merge(M.append("line").attr("stroke","currentColor").attr(d+"2",h*a)),N=N.merge(M.append("text").attr("fill","currentColor").attr(d,h*v).attr("dy",e===wk?"0em":e===gI?"0.71em":"0.32em")),m!==C&&(S=S.transition(m),k=k.transition(m),P=P.transition(m),N=N.transition(m),R=R.transition(m).attr("opacity",FJ).attr("transform",function(_){return isFinite(_=w(_))?f(_+u):this.getAttribute("transform")}),M.attr("opacity",FJ).attr("transform",function(_){var A=this.parentNode.__axis;return f((A&&isFinite(A=A(_))?A:w(_))+u)})),R.remove(),S.attr("d",e===Sk||e===mI?o?"M"+h*o+","+b+"H"+u+"V"+T+"H"+h*o:"M"+u+","+b+"V"+T:o?"M"+b+","+h*o+"V"+u+"H"+T+"V"+h*o:"M"+b+","+u+"H"+T),k.attr("opacity",1).attr("transform",function(_){return f(w(_)+u)}),P.attr(d+"2",h*a),N.attr(d,h*v).text(y),C.filter(Oqe).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",e===mI?"start":e===Sk?"end":"middle"),C.each(function(){this.__axis=w})}return s(p,"axis"),p.scale=function(m){return arguments.length?(t=m,p):t},p.ticks=function(){return r=Array.from(arguments),p},p.tickArguments=function(m){return arguments.length?(r=m==null?[]:Array.from(m),p):r.slice()},p.tickValues=function(m){return arguments.length?(n=m==null?null:Array.from(m),p):n&&n.slice()},p.tickFormat=function(m){return arguments.length?(i=m,p):i},p.tickSize=function(m){return arguments.length?(a=o=+m,p):a},p.tickSizeInner=function(m){return arguments.length?(a=+m,p):a},p.tickSizeOuter=function(m){return arguments.length?(o=+m,p):o},p.tickPadding=function(m){return arguments.length?(l=+m,p):l},p.offset=function(m){return arguments.length?(u=+m,p):u},p}function yI(e){return GJ(wk,e)}function vI(e){return GJ(gI,e)}var wk,mI,gI,Sk,FJ,zJ=F(()=>{"use strict";$J();wk=1,mI=2,gI=3,Sk=4,FJ=1e-6;s(Iqe,"translateX");s(Mqe,"translateY");s(Nqe,"number");s(Pqe,"center");s(Oqe,"entering");s(GJ,"axis");s(yI,"axisTop");s(vI,"axisBottom")});var VJ=F(()=>{"use strict";zJ()});function qJ(){for(var e=0,t=arguments.length,r={},n;e=0&&(n=r.slice(i+1),r=r.slice(0,i)),r&&!t.hasOwnProperty(r))throw new Error("unknown type: "+r);return{type:r,name:n}})}function Fqe(e,t){for(var r=0,n=e.length,i;r{"use strict";Bqe={value:s(()=>{},"value")};s(qJ,"dispatch");s(kk,"Dispatch");s($qe,"parseTypenames");kk.prototype=qJ.prototype={constructor:kk,on:s(function(e,t){var r=this._,n=$qe(e+"",r),i,a=-1,o=n.length;if(arguments.length<2){for(;++a0)for(var r=new Array(i),n=0,i,a;n{"use strict";HJ()});var Ek,TI,CI=F(()=>{"use strict";Ek="http://www.w3.org/1999/xhtml",TI={svg:"http://www.w3.org/2000/svg",xhtml:Ek,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"}});function qc(e){var t=e+="",r=t.indexOf(":");return r>=0&&(t=e.slice(0,r))!=="xmlns"&&(e=e.slice(r+1)),TI.hasOwnProperty(t)?{space:TI[t],local:e}:e}var Ak=F(()=>{"use strict";CI();s(qc,"default")});function Gqe(e){return function(){var t=this.ownerDocument,r=this.namespaceURI;return r===Ek&&t.documentElement.namespaceURI===Ek?t.createElement(e):t.createElementNS(r,e)}}function zqe(e){return function(){return this.ownerDocument.createElementNS(e.space,e.local)}}function db(e){var t=qc(e);return(t.local?zqe:Gqe)(t)}var SI=F(()=>{"use strict";Ak();CI();s(Gqe,"creatorInherit");s(zqe,"creatorFixed");s(db,"default")});function Vqe(){}function $d(e){return e==null?Vqe:function(){return this.querySelector(e)}}var Rk=F(()=>{"use strict";s(Vqe,"none");s($d,"default")});function wI(e){typeof e!="function"&&(e=$d(e));for(var t=this._groups,r=t.length,n=new Array(r),i=0;i{"use strict";jl();Rk();s(wI,"default")});function kI(e){return e==null?[]:Array.isArray(e)?e:Array.from(e)}var YJ=F(()=>{"use strict";s(kI,"array")});function Wqe(){return[]}function ay(e){return e==null?Wqe:function(){return this.querySelectorAll(e)}}var EI=F(()=>{"use strict";s(Wqe,"empty");s(ay,"default")});function qqe(e){return function(){return kI(e.apply(this,arguments))}}function AI(e){typeof e=="function"?e=qqe(e):e=ay(e);for(var t=this._groups,r=t.length,n=[],i=[],a=0;a{"use strict";jl();YJ();EI();s(qqe,"arrayAll");s(AI,"default")});function sy(e){return function(){return this.matches(e)}}function _k(e){return function(t){return t.matches(e)}}var fb=F(()=>{"use strict";s(sy,"default");s(_k,"childMatcher")});function Uqe(e){return function(){return Hqe.call(this.children,e)}}function Yqe(){return this.firstElementChild}function RI(e){return this.select(e==null?Yqe:Uqe(typeof e=="function"?e:_k(e)))}var Hqe,XJ=F(()=>{"use strict";fb();Hqe=Array.prototype.find;s(Uqe,"childFind");s(Yqe,"childFirst");s(RI,"default")});function Xqe(){return Array.from(this.children)}function Kqe(e){return function(){return jqe.call(this.children,e)}}function _I(e){return this.selectAll(e==null?Xqe:Kqe(typeof e=="function"?e:_k(e)))}var jqe,KJ=F(()=>{"use strict";fb();jqe=Array.prototype.filter;s(Xqe,"children");s(Kqe,"childrenFilter");s(_I,"default")});function LI(e){typeof e!="function"&&(e=sy(e));for(var t=this._groups,r=t.length,n=new Array(r),i=0;i{"use strict";jl();fb();s(LI,"default")});function pb(e){return new Array(e.length)}var DI=F(()=>{"use strict";s(pb,"default")});function II(){return new Ii(this._enter||this._groups.map(pb),this._parents)}function mb(e,t){this.ownerDocument=e.ownerDocument,this.namespaceURI=e.namespaceURI,this._next=null,this._parent=e,this.__data__=t}var MI=F(()=>{"use strict";DI();jl();s(II,"default");s(mb,"EnterNode");mb.prototype={constructor:mb,appendChild:s(function(e){return this._parent.insertBefore(e,this._next)},"appendChild"),insertBefore:s(function(e,t){return this._parent.insertBefore(e,t)},"insertBefore"),querySelector:s(function(e){return this._parent.querySelector(e)},"querySelector"),querySelectorAll:s(function(e){return this._parent.querySelectorAll(e)},"querySelectorAll")}});function NI(e){return function(){return e}}var QJ=F(()=>{"use strict";s(NI,"default")});function Zqe(e,t,r,n,i,a){for(var o=0,l,u=t.length,h=a.length;o=T&&(T=b+1);!(C=v[T])&&++T{"use strict";jl();MI();QJ();s(Zqe,"bindIndex");s(Qqe,"bindKey");s(Jqe,"datum");s(PI,"default");s(eHe,"arraylike")});function OI(){return new Ii(this._exit||this._groups.map(pb),this._parents)}var eee=F(()=>{"use strict";DI();jl();s(OI,"default")});function BI(e,t,r){var n=this.enter(),i=this,a=this.exit();return typeof e=="function"?(n=e(n),n&&(n=n.selection())):n=n.append(e+""),t!=null&&(i=t(i),i&&(i=i.selection())),r==null?a.remove():r(a),n&&i?n.merge(i).order():i}var tee=F(()=>{"use strict";s(BI,"default")});function $I(e){for(var t=e.selection?e.selection():e,r=this._groups,n=t._groups,i=r.length,a=n.length,o=Math.min(i,a),l=new Array(i),u=0;u{"use strict";jl();s($I,"default")});function FI(){for(var e=this._groups,t=-1,r=e.length;++t=0;)(o=n[i])&&(a&&o.compareDocumentPosition(a)^4&&a.parentNode.insertBefore(o,a),a=o);return this}var nee=F(()=>{"use strict";s(FI,"default")});function GI(e){e||(e=tHe);function t(f,p){return f&&p?e(f.__data__,p.__data__):!f-!p}s(t,"compareNode");for(var r=this._groups,n=r.length,i=new Array(n),a=0;at?1:e>=t?0:NaN}var iee=F(()=>{"use strict";jl();s(GI,"default");s(tHe,"ascending")});function zI(){var e=arguments[0];return arguments[0]=this,e.apply(null,arguments),this}var aee=F(()=>{"use strict";s(zI,"default")});function VI(){return Array.from(this)}var see=F(()=>{"use strict";s(VI,"default")});function WI(){for(var e=this._groups,t=0,r=e.length;t{"use strict";s(WI,"default")});function qI(){let e=0;for(let t of this)++e;return e}var lee=F(()=>{"use strict";s(qI,"default")});function HI(){return!this.node()}var cee=F(()=>{"use strict";s(HI,"default")});function UI(e){for(var t=this._groups,r=0,n=t.length;r{"use strict";s(UI,"default")});function rHe(e){return function(){this.removeAttribute(e)}}function nHe(e){return function(){this.removeAttributeNS(e.space,e.local)}}function iHe(e,t){return function(){this.setAttribute(e,t)}}function aHe(e,t){return function(){this.setAttributeNS(e.space,e.local,t)}}function sHe(e,t){return function(){var r=t.apply(this,arguments);r==null?this.removeAttribute(e):this.setAttribute(e,r)}}function oHe(e,t){return function(){var r=t.apply(this,arguments);r==null?this.removeAttributeNS(e.space,e.local):this.setAttributeNS(e.space,e.local,r)}}function YI(e,t){var r=qc(e);if(arguments.length<2){var n=this.node();return r.local?n.getAttributeNS(r.space,r.local):n.getAttribute(r)}return this.each((t==null?r.local?nHe:rHe:typeof t=="function"?r.local?oHe:sHe:r.local?aHe:iHe)(r,t))}var hee=F(()=>{"use strict";Ak();s(rHe,"attrRemove");s(nHe,"attrRemoveNS");s(iHe,"attrConstant");s(aHe,"attrConstantNS");s(sHe,"attrFunction");s(oHe,"attrFunctionNS");s(YI,"default")});function gb(e){return e.ownerDocument&&e.ownerDocument.defaultView||e.document&&e||e.defaultView}var jI=F(()=>{"use strict";s(gb,"default")});function lHe(e){return function(){this.style.removeProperty(e)}}function cHe(e,t,r){return function(){this.style.setProperty(e,t,r)}}function uHe(e,t,r){return function(){var n=t.apply(this,arguments);n==null?this.style.removeProperty(e):this.style.setProperty(e,n,r)}}function XI(e,t,r){return arguments.length>1?this.each((t==null?lHe:typeof t=="function"?uHe:cHe)(e,t,r??"")):Fd(this.node(),e)}function Fd(e,t){return e.style.getPropertyValue(t)||gb(e).getComputedStyle(e,null).getPropertyValue(t)}var KI=F(()=>{"use strict";jI();s(lHe,"styleRemove");s(cHe,"styleConstant");s(uHe,"styleFunction");s(XI,"default");s(Fd,"styleValue")});function hHe(e){return function(){delete this[e]}}function dHe(e,t){return function(){this[e]=t}}function fHe(e,t){return function(){var r=t.apply(this,arguments);r==null?delete this[e]:this[e]=r}}function ZI(e,t){return arguments.length>1?this.each((t==null?hHe:typeof t=="function"?fHe:dHe)(e,t)):this.node()[e]}var dee=F(()=>{"use strict";s(hHe,"propertyRemove");s(dHe,"propertyConstant");s(fHe,"propertyFunction");s(ZI,"default")});function fee(e){return e.trim().split(/^|\s+/)}function QI(e){return e.classList||new pee(e)}function pee(e){this._node=e,this._names=fee(e.getAttribute("class")||"")}function mee(e,t){for(var r=QI(e),n=-1,i=t.length;++n{"use strict";s(fee,"classArray");s(QI,"classList");s(pee,"ClassList");pee.prototype={add:s(function(e){var t=this._names.indexOf(e);t<0&&(this._names.push(e),this._node.setAttribute("class",this._names.join(" ")))},"add"),remove:s(function(e){var t=this._names.indexOf(e);t>=0&&(this._names.splice(t,1),this._node.setAttribute("class",this._names.join(" ")))},"remove"),contains:s(function(e){return this._names.indexOf(e)>=0},"contains")};s(mee,"classedAdd");s(gee,"classedRemove");s(pHe,"classedTrue");s(mHe,"classedFalse");s(gHe,"classedFunction");s(JI,"default")});function yHe(){this.textContent=""}function vHe(e){return function(){this.textContent=e}}function xHe(e){return function(){var t=e.apply(this,arguments);this.textContent=t??""}}function e8(e){return arguments.length?this.each(e==null?yHe:(typeof e=="function"?xHe:vHe)(e)):this.node().textContent}var vee=F(()=>{"use strict";s(yHe,"textRemove");s(vHe,"textConstant");s(xHe,"textFunction");s(e8,"default")});function bHe(){this.innerHTML=""}function THe(e){return function(){this.innerHTML=e}}function CHe(e){return function(){var t=e.apply(this,arguments);this.innerHTML=t??""}}function t8(e){return arguments.length?this.each(e==null?bHe:(typeof e=="function"?CHe:THe)(e)):this.node().innerHTML}var xee=F(()=>{"use strict";s(bHe,"htmlRemove");s(THe,"htmlConstant");s(CHe,"htmlFunction");s(t8,"default")});function SHe(){this.nextSibling&&this.parentNode.appendChild(this)}function r8(){return this.each(SHe)}var bee=F(()=>{"use strict";s(SHe,"raise");s(r8,"default")});function wHe(){this.previousSibling&&this.parentNode.insertBefore(this,this.parentNode.firstChild)}function n8(){return this.each(wHe)}var Tee=F(()=>{"use strict";s(wHe,"lower");s(n8,"default")});function i8(e){var t=typeof e=="function"?e:db(e);return this.select(function(){return this.appendChild(t.apply(this,arguments))})}var Cee=F(()=>{"use strict";SI();s(i8,"default")});function kHe(){return null}function a8(e,t){var r=typeof e=="function"?e:db(e),n=t==null?kHe:typeof t=="function"?t:$d(t);return this.select(function(){return this.insertBefore(r.apply(this,arguments),n.apply(this,arguments)||null)})}var See=F(()=>{"use strict";SI();Rk();s(kHe,"constantNull");s(a8,"default")});function EHe(){var e=this.parentNode;e&&e.removeChild(this)}function s8(){return this.each(EHe)}var wee=F(()=>{"use strict";s(EHe,"remove");s(s8,"default")});function AHe(){var e=this.cloneNode(!1),t=this.parentNode;return t?t.insertBefore(e,this.nextSibling):e}function RHe(){var e=this.cloneNode(!0),t=this.parentNode;return t?t.insertBefore(e,this.nextSibling):e}function o8(e){return this.select(e?RHe:AHe)}var kee=F(()=>{"use strict";s(AHe,"selection_cloneShallow");s(RHe,"selection_cloneDeep");s(o8,"default")});function l8(e){return arguments.length?this.property("__data__",e):this.node().__data__}var Eee=F(()=>{"use strict";s(l8,"default")});function _He(e){return function(t){e.call(this,t,this.__data__)}}function LHe(e){return e.trim().split(/^|\s+/).map(function(t){var r="",n=t.indexOf(".");return n>=0&&(r=t.slice(n+1),t=t.slice(0,n)),{type:t,name:r}})}function DHe(e){return function(){var t=this.__on;if(t){for(var r=0,n=-1,i=t.length,a;r{"use strict";s(_He,"contextListener");s(LHe,"parseTypenames");s(DHe,"onRemove");s(IHe,"onAdd");s(c8,"default")});function Ree(e,t,r){var n=gb(e),i=n.CustomEvent;typeof i=="function"?i=new i(t,r):(i=n.document.createEvent("Event"),r?(i.initEvent(t,r.bubbles,r.cancelable),i.detail=r.detail):i.initEvent(t,!1,!1)),e.dispatchEvent(i)}function MHe(e,t){return function(){return Ree(this,e,t)}}function NHe(e,t){return function(){return Ree(this,e,t.apply(this,arguments))}}function u8(e,t){return this.each((typeof t=="function"?NHe:MHe)(e,t))}var _ee=F(()=>{"use strict";jI();s(Ree,"dispatchEvent");s(MHe,"dispatchConstant");s(NHe,"dispatchFunction");s(u8,"default")});function*h8(){for(var e=this._groups,t=0,r=e.length;t{"use strict";s(h8,"default")});function Ii(e,t){this._groups=e,this._parents=t}function Dee(){return new Ii([[document.documentElement]],d8)}function PHe(){return this}var d8,uh,jl=F(()=>{"use strict";UJ();jJ();XJ();KJ();ZJ();JJ();MI();eee();tee();ree();nee();iee();aee();see();oee();lee();cee();uee();hee();KI();dee();yee();vee();xee();bee();Tee();Cee();See();wee();kee();Eee();Aee();_ee();Lee();d8=[null];s(Ii,"Selection");s(Dee,"selection");s(PHe,"selection_selection");Ii.prototype=Dee.prototype={constructor:Ii,select:wI,selectAll:AI,selectChild:RI,selectChildren:_I,filter:LI,data:PI,enter:II,exit:OI,join:BI,merge:$I,selection:PHe,order:FI,sort:GI,call:zI,nodes:VI,node:WI,size:qI,empty:HI,each:UI,attr:YI,style:XI,property:ZI,classed:JI,text:e8,html:t8,raise:r8,lower:n8,append:i8,insert:a8,remove:s8,clone:o8,datum:l8,on:c8,dispatch:u8,[Symbol.iterator]:h8};uh=Dee});function ut(e){return typeof e=="string"?new Ii([[document.querySelector(e)]],[document.documentElement]):new Ii([[e]],d8)}var Iee=F(()=>{"use strict";jl();s(ut,"default")});var Xl=F(()=>{"use strict";fb();Ak();Iee();jl();Rk();EI();KI()});var Mee=F(()=>{"use strict"});function Gd(e,t,r){e.prototype=t.prototype=r,r.constructor=e}function oy(e,t){var r=Object.create(e.prototype);for(var n in t)r[n]=t[n];return r}var f8=F(()=>{"use strict";s(Gd,"default");s(oy,"extend")});function zd(){}function Pee(){return this.rgb().formatHex()}function WHe(){return this.rgb().formatHex8()}function qHe(){return Vee(this).formatHsl()}function Oee(){return this.rgb().formatRgb()}function Zl(e){var t,r;return e=(e+"").trim().toLowerCase(),(t=OHe.exec(e))?(r=t[1].length,t=parseInt(t[1],16),r===6?Bee(t):r===3?new Xa(t>>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):r===8?Lk(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):r===4?Lk(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=BHe.exec(e))?new Xa(t[1],t[2],t[3],1):(t=$He.exec(e))?new Xa(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=FHe.exec(e))?Lk(t[1],t[2],t[3],t[4]):(t=GHe.exec(e))?Lk(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=zHe.exec(e))?Gee(t[1],t[2]/100,t[3]/100,1):(t=VHe.exec(e))?Gee(t[1],t[2]/100,t[3]/100,t[4]):Nee.hasOwnProperty(e)?Bee(Nee[e]):e==="transparent"?new Xa(NaN,NaN,NaN,0):null}function Bee(e){return new Xa(e>>16&255,e>>8&255,e&255,1)}function Lk(e,t,r,n){return n<=0&&(e=t=r=NaN),new Xa(e,t,r,n)}function m8(e){return e instanceof zd||(e=Zl(e)),e?(e=e.rgb(),new Xa(e.r,e.g,e.b,e.opacity)):new Xa}function cy(e,t,r,n){return arguments.length===1?m8(e):new Xa(e,t,r,n??1)}function Xa(e,t,r,n){this.r=+e,this.g=+t,this.b=+r,this.opacity=+n}function $ee(){return`#${em(this.r)}${em(this.g)}${em(this.b)}`}function HHe(){return`#${em(this.r)}${em(this.g)}${em(this.b)}${em((isNaN(this.opacity)?1:this.opacity)*255)}`}function Fee(){let e=Mk(this.opacity);return`${e===1?"rgb(":"rgba("}${tm(this.r)}, ${tm(this.g)}, ${tm(this.b)}${e===1?")":`, ${e})`}`}function Mk(e){return isNaN(e)?1:Math.max(0,Math.min(1,e))}function tm(e){return Math.max(0,Math.min(255,Math.round(e)||0))}function em(e){return e=tm(e),(e<16?"0":"")+e.toString(16)}function Gee(e,t,r,n){return n<=0?e=t=r=NaN:r<=0||r>=1?e=t=NaN:t<=0&&(e=NaN),new Kl(e,t,r,n)}function Vee(e){if(e instanceof Kl)return new Kl(e.h,e.s,e.l,e.opacity);if(e instanceof zd||(e=Zl(e)),!e)return new Kl;if(e instanceof Kl)return e;e=e.rgb();var t=e.r/255,r=e.g/255,n=e.b/255,i=Math.min(t,r,n),a=Math.max(t,r,n),o=NaN,l=a-i,u=(a+i)/2;return l?(t===a?o=(r-n)/l+(r0&&u<1?0:o,new Kl(o,l,u,e.opacity)}function Wee(e,t,r,n){return arguments.length===1?Vee(e):new Kl(e,t,r,n??1)}function Kl(e,t,r,n){this.h=+e,this.s=+t,this.l=+r,this.opacity=+n}function zee(e){return e=(e||0)%360,e<0?e+360:e}function Dk(e){return Math.max(0,Math.min(1,e||0))}function p8(e,t,r){return(e<60?t+(r-t)*e/60:e<180?r:e<240?t+(r-t)*(240-e)/60:t)*255}var yb,Ik,ly,vb,Hc,OHe,BHe,$He,FHe,GHe,zHe,VHe,Nee,g8=F(()=>{"use strict";f8();s(zd,"Color");yb=.7,Ik=1/yb,ly="\\s*([+-]?\\d+)\\s*",vb="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",Hc="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",OHe=/^#([0-9a-f]{3,8})$/,BHe=new RegExp(`^rgb\\(${ly},${ly},${ly}\\)$`),$He=new RegExp(`^rgb\\(${Hc},${Hc},${Hc}\\)$`),FHe=new RegExp(`^rgba\\(${ly},${ly},${ly},${vb}\\)$`),GHe=new RegExp(`^rgba\\(${Hc},${Hc},${Hc},${vb}\\)$`),zHe=new RegExp(`^hsl\\(${vb},${Hc},${Hc}\\)$`),VHe=new RegExp(`^hsla\\(${vb},${Hc},${Hc},${vb}\\)$`),Nee={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};Gd(zd,Zl,{copy(e){return Object.assign(new this.constructor,this,e)},displayable(){return this.rgb().displayable()},hex:Pee,formatHex:Pee,formatHex8:WHe,formatHsl:qHe,formatRgb:Oee,toString:Oee});s(Pee,"color_formatHex");s(WHe,"color_formatHex8");s(qHe,"color_formatHsl");s(Oee,"color_formatRgb");s(Zl,"color");s(Bee,"rgbn");s(Lk,"rgba");s(m8,"rgbConvert");s(cy,"rgb");s(Xa,"Rgb");Gd(Xa,cy,oy(zd,{brighter(e){return e=e==null?Ik:Math.pow(Ik,e),new Xa(this.r*e,this.g*e,this.b*e,this.opacity)},darker(e){return e=e==null?yb:Math.pow(yb,e),new Xa(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new Xa(tm(this.r),tm(this.g),tm(this.b),Mk(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:$ee,formatHex:$ee,formatHex8:HHe,formatRgb:Fee,toString:Fee}));s($ee,"rgb_formatHex");s(HHe,"rgb_formatHex8");s(Fee,"rgb_formatRgb");s(Mk,"clampa");s(tm,"clampi");s(em,"hex");s(Gee,"hsla");s(Vee,"hslConvert");s(Wee,"hsl");s(Kl,"Hsl");Gd(Kl,Wee,oy(zd,{brighter(e){return e=e==null?Ik:Math.pow(Ik,e),new Kl(this.h,this.s,this.l*e,this.opacity)},darker(e){return e=e==null?yb:Math.pow(yb,e),new Kl(this.h,this.s,this.l*e,this.opacity)},rgb(){var e=this.h%360+(this.h<0)*360,t=isNaN(e)||isNaN(this.s)?0:this.s,r=this.l,n=r+(r<.5?r:1-r)*t,i=2*r-n;return new Xa(p8(e>=240?e-240:e+120,i,n),p8(e,i,n),p8(e<120?e+240:e-120,i,n),this.opacity)},clamp(){return new Kl(zee(this.h),Dk(this.s),Dk(this.l),Mk(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){let e=Mk(this.opacity);return`${e===1?"hsl(":"hsla("}${zee(this.h)}, ${Dk(this.s)*100}%, ${Dk(this.l)*100}%${e===1?")":`, ${e})`}`}}));s(zee,"clamph");s(Dk,"clampt");s(p8,"hsl2rgb")});var qee,Hee,Uee=F(()=>{"use strict";qee=Math.PI/180,Hee=180/Math.PI});function Qee(e){if(e instanceof Uc)return new Uc(e.l,e.a,e.b,e.opacity);if(e instanceof hh)return Jee(e);e instanceof Xa||(e=m8(e));var t=b8(e.r),r=b8(e.g),n=b8(e.b),i=y8((.2225045*t+.7168786*r+.0606169*n)/jee),a,o;return t===r&&r===n?a=o=i:(a=y8((.4360747*t+.3850649*r+.1430804*n)/Yee),o=y8((.0139322*t+.0971045*r+.7141733*n)/Xee)),new Uc(116*i-16,500*(a-i),200*(i-o),e.opacity)}function T8(e,t,r,n){return arguments.length===1?Qee(e):new Uc(e,t,r,n??1)}function Uc(e,t,r,n){this.l=+e,this.a=+t,this.b=+r,this.opacity=+n}function y8(e){return e>UHe?Math.pow(e,1/3):e/Zee+Kee}function v8(e){return e>uy?e*e*e:Zee*(e-Kee)}function x8(e){return 255*(e<=.0031308?12.92*e:1.055*Math.pow(e,1/2.4)-.055)}function b8(e){return(e/=255)<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4)}function YHe(e){if(e instanceof hh)return new hh(e.h,e.c,e.l,e.opacity);if(e instanceof Uc||(e=Qee(e)),e.a===0&&e.b===0)return new hh(NaN,0{"use strict";f8();g8();Uee();Nk=18,Yee=.96422,jee=1,Xee=.82521,Kee=4/29,uy=6/29,Zee=3*uy*uy,UHe=uy*uy*uy;s(Qee,"labConvert");s(T8,"lab");s(Uc,"Lab");Gd(Uc,T8,oy(zd,{brighter(e){return new Uc(this.l+Nk*(e??1),this.a,this.b,this.opacity)},darker(e){return new Uc(this.l-Nk*(e??1),this.a,this.b,this.opacity)},rgb(){var e=(this.l+16)/116,t=isNaN(this.a)?e:e+this.a/500,r=isNaN(this.b)?e:e-this.b/200;return t=Yee*v8(t),e=jee*v8(e),r=Xee*v8(r),new Xa(x8(3.1338561*t-1.6168667*e-.4906146*r),x8(-.9787684*t+1.9161415*e+.033454*r),x8(.0719453*t-.2289914*e+1.4052427*r),this.opacity)}}));s(y8,"xyz2lab");s(v8,"lab2xyz");s(x8,"lrgb2rgb");s(b8,"rgb2lrgb");s(YHe,"hclConvert");s(xb,"hcl");s(hh,"Hcl");s(Jee,"hcl2lab");Gd(hh,xb,oy(zd,{brighter(e){return new hh(this.h,this.c,this.l+Nk*(e??1),this.opacity)},darker(e){return new hh(this.h,this.c,this.l-Nk*(e??1),this.opacity)},rgb(){return Jee(this).rgb()}}))});var hy=F(()=>{"use strict";g8();ete()});function C8(e,t,r,n,i){var a=e*e,o=a*e;return((1-3*e+3*a-o)*t+(4-6*a+3*o)*r+(1+3*e+3*a-3*o)*n+o*i)/6}function S8(e){var t=e.length-1;return function(r){var n=r<=0?r=0:r>=1?(r=1,t-1):Math.floor(r*t),i=e[n],a=e[n+1],o=n>0?e[n-1]:2*i-a,l=n{"use strict";s(C8,"basis");s(S8,"default")});function k8(e){var t=e.length;return function(r){var n=Math.floor(((r%=1)<0?++r:r)*t),i=e[(n+t-1)%t],a=e[n%t],o=e[(n+1)%t],l=e[(n+2)%t];return C8((r-n/t)*t,i,a,o,l)}}var tte=F(()=>{"use strict";w8();s(k8,"default")});var dy,E8=F(()=>{"use strict";dy=s(e=>()=>e,"default")});function rte(e,t){return function(r){return e+r*t}}function jHe(e,t,r){return e=Math.pow(e,r),t=Math.pow(t,r)-e,r=1/r,function(n){return Math.pow(e+n*t,r)}}function nte(e,t){var r=t-e;return r?rte(e,r>180||r<-180?r-360*Math.round(r/360):r):dy(isNaN(e)?t:e)}function ite(e){return(e=+e)==1?dh:function(t,r){return r-t?jHe(t,r,e):dy(isNaN(t)?r:t)}}function dh(e,t){var r=t-e;return r?rte(e,r):dy(isNaN(e)?t:e)}var A8=F(()=>{"use strict";E8();s(rte,"linear");s(jHe,"exponential");s(nte,"hue");s(ite,"gamma");s(dh,"nogamma")});function ate(e){return function(t){var r=t.length,n=new Array(r),i=new Array(r),a=new Array(r),o,l;for(o=0;o{"use strict";hy();w8();tte();A8();rm=s((function e(t){var r=ite(t);function n(i,a){var o=r((i=cy(i)).r,(a=cy(a)).r),l=r(i.g,a.g),u=r(i.b,a.b),h=dh(i.opacity,a.opacity);return function(d){return i.r=o(d),i.g=l(d),i.b=u(d),i.opacity=h(d),i+""}}return s(n,"rgb"),n.gamma=e,n}),"rgbGamma")(1);s(ate,"rgbSpline");XHe=ate(S8),KHe=ate(k8)});function _8(e,t){t||(t=[]);var r=e?Math.min(t.length,e.length):0,n=t.slice(),i;return function(a){for(i=0;i{"use strict";s(_8,"default");s(ste,"isNumberArray")});function lte(e,t){var r=t?t.length:0,n=e?Math.min(r,e.length):0,i=new Array(n),a=new Array(r),o;for(o=0;o{"use strict";Pk();s(lte,"genericArray")});function L8(e,t){var r=new Date;return e=+e,t=+t,function(n){return r.setTime(e*(1-n)+t*n),r}}var ute=F(()=>{"use strict";s(L8,"default")});function Da(e,t){return e=+e,t=+t,function(r){return e*(1-r)+t*r}}var bb=F(()=>{"use strict";s(Da,"default")});function D8(e,t){var r={},n={},i;(e===null||typeof e!="object")&&(e={}),(t===null||typeof t!="object")&&(t={});for(i in t)i in e?r[i]=Vd(e[i],t[i]):n[i]=t[i];return function(a){for(i in r)n[i]=r[i](a);return n}}var hte=F(()=>{"use strict";Pk();s(D8,"default")});function ZHe(e){return function(){return e}}function QHe(e){return function(t){return e(t)+""}}function fy(e,t){var r=M8.lastIndex=I8.lastIndex=0,n,i,a,o=-1,l=[],u=[];for(e=e+"",t=t+"";(n=M8.exec(e))&&(i=I8.exec(t));)(a=i.index)>r&&(a=t.slice(r,a),l[o]?l[o]+=a:l[++o]=a),(n=n[0])===(i=i[0])?l[o]?l[o]+=i:l[++o]=i:(l[++o]=null,u.push({i:o,x:Da(n,i)})),r=I8.lastIndex;return r{"use strict";bb();M8=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,I8=new RegExp(M8.source,"g");s(ZHe,"zero");s(QHe,"one");s(fy,"default")});function Vd(e,t){var r=typeof t,n;return t==null||r==="boolean"?dy(t):(r==="number"?Da:r==="string"?(n=Zl(t))?(t=n,rm):fy:t instanceof Zl?rm:t instanceof Date?L8:ste(t)?_8:Array.isArray(t)?lte:typeof t.valueOf!="function"&&typeof t.toString!="function"||isNaN(t)?D8:Da)(e,t)}var Pk=F(()=>{"use strict";hy();R8();cte();ute();bb();hte();N8();E8();ote();s(Vd,"default")});function Ok(e,t){return e=+e,t=+t,function(r){return Math.round(e*(1-r)+t*r)}}var dte=F(()=>{"use strict";s(Ok,"default")});function $k(e,t,r,n,i,a){var o,l,u;return(o=Math.sqrt(e*e+t*t))&&(e/=o,t/=o),(u=e*r+t*n)&&(r-=e*u,n-=t*u),(l=Math.sqrt(r*r+n*n))&&(r/=l,n/=l,u/=l),e*n{"use strict";fte=180/Math.PI,Bk={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};s($k,"default")});function mte(e){let t=new(typeof DOMMatrix=="function"?DOMMatrix:WebKitCSSMatrix)(e+"");return t.isIdentity?Bk:$k(t.a,t.b,t.c,t.d,t.e,t.f)}function gte(e){return e==null?Bk:(Fk||(Fk=document.createElementNS("http://www.w3.org/2000/svg","g")),Fk.setAttribute("transform",e),(e=Fk.transform.baseVal.consolidate())?(e=e.matrix,$k(e.a,e.b,e.c,e.d,e.e,e.f)):Bk)}var Fk,yte=F(()=>{"use strict";pte();s(mte,"parseCss");s(gte,"parseSvg")});function vte(e,t,r,n){function i(h){return h.length?h.pop()+" ":""}s(i,"pop");function a(h,d,f,p,m,g){if(h!==f||d!==p){var y=m.push("translate(",null,t,null,r);g.push({i:y-4,x:Da(h,f)},{i:y-2,x:Da(d,p)})}else(f||p)&&m.push("translate("+f+t+p+r)}s(a,"translate");function o(h,d,f,p){h!==d?(h-d>180?d+=360:d-h>180&&(h+=360),p.push({i:f.push(i(f)+"rotate(",null,n)-2,x:Da(h,d)})):d&&f.push(i(f)+"rotate("+d+n)}s(o,"rotate");function l(h,d,f,p){h!==d?p.push({i:f.push(i(f)+"skewX(",null,n)-2,x:Da(h,d)}):d&&f.push(i(f)+"skewX("+d+n)}s(l,"skewX");function u(h,d,f,p,m,g){if(h!==f||d!==p){var y=m.push(i(m)+"scale(",null,",",null,")");g.push({i:y-4,x:Da(h,f)},{i:y-2,x:Da(d,p)})}else(f!==1||p!==1)&&m.push(i(m)+"scale("+f+","+p+")")}return s(u,"scale"),function(h,d){var f=[],p=[];return h=e(h),d=e(d),a(h.translateX,h.translateY,d.translateX,d.translateY,f,p),o(h.rotate,d.rotate,f,p),l(h.skewX,d.skewX,f,p),u(h.scaleX,h.scaleY,d.scaleX,d.scaleY,f,p),h=d=null,function(m){for(var g=-1,y=p.length,v;++g{"use strict";bb();yte();s(vte,"interpolateTransform");P8=vte(mte,"px, ","px)","deg)"),O8=vte(gte,", ",")",")")});function bte(e){return function(t,r){var n=e((t=xb(t)).h,(r=xb(r)).h),i=dh(t.c,r.c),a=dh(t.l,r.l),o=dh(t.opacity,r.opacity);return function(l){return t.h=n(l),t.c=i(l),t.l=a(l),t.opacity=o(l),t+""}}}var B8,JHe,Tte=F(()=>{"use strict";hy();A8();s(bte,"hcl");B8=bte(nte),JHe=bte(dh)});var py=F(()=>{"use strict";Pk();bb();dte();N8();xte();R8();Tte()});function Eb(){return nm||(wte(eUe),nm=wb.now()+Vk)}function eUe(){nm=0}function kb(){this._call=this._time=this._next=null}function Wk(e,t,r){var n=new kb;return n.restart(e,t,r),n}function kte(){Eb(),++my;for(var e=Gk,t;e;)(t=nm-e._time)>=0&&e._call.call(void 0,t),e=e._next;--my}function Cte(){nm=(zk=wb.now())+Vk,my=Cb=0;try{kte()}finally{my=0,rUe(),nm=0}}function tUe(){var e=wb.now(),t=e-zk;t>Ste&&(Vk-=t,zk=e)}function rUe(){for(var e,t=Gk,r,n=1/0;t;)t._call?(n>t._time&&(n=t._time),e=t,t=t._next):(r=t._next,t._next=null,t=e?e._next=r:Gk=r);Sb=e,$8(n)}function $8(e){if(!my){Cb&&(Cb=clearTimeout(Cb));var t=e-nm;t>24?(e<1/0&&(Cb=setTimeout(Cte,e-wb.now()-Vk)),Tb&&(Tb=clearInterval(Tb))):(Tb||(zk=wb.now(),Tb=setInterval(tUe,Ste)),my=1,wte(Cte))}}var my,Cb,Tb,Ste,Gk,Sb,zk,nm,Vk,wb,wte,F8=F(()=>{"use strict";my=0,Cb=0,Tb=0,Ste=1e3,zk=0,nm=0,Vk=0,wb=typeof performance=="object"&&performance.now?performance:Date,wte=typeof window=="object"&&window.requestAnimationFrame?window.requestAnimationFrame.bind(window):function(e){setTimeout(e,17)};s(Eb,"now");s(eUe,"clearNow");s(kb,"Timer");kb.prototype=Wk.prototype={constructor:kb,restart:s(function(e,t,r){if(typeof e!="function")throw new TypeError("callback is not a function");r=(r==null?Eb():+r)+(t==null?0:+t),!this._next&&Sb!==this&&(Sb?Sb._next=this:Gk=this,Sb=this),this._call=e,this._time=r,$8()},"restart"),stop:s(function(){this._call&&(this._call=null,this._time=1/0,$8())},"stop")};s(Wk,"timer");s(kte,"timerFlush");s(Cte,"wake");s(tUe,"poke");s(rUe,"nap");s($8,"sleep")});function Ab(e,t,r){var n=new kb;return t=t==null?0:+t,n.restart(i=>{n.stop(),e(i+t)},t,r),n}var Ete=F(()=>{"use strict";F8();s(Ab,"default")});var qk=F(()=>{"use strict";F8();Ete()});function fh(e,t,r,n,i,a){var o=e.__transition;if(!o)e.__transition={};else if(r in o)return;aUe(e,r,{name:t,index:n,group:i,on:nUe,tween:iUe,time:a.time,delay:a.delay,duration:a.duration,ease:a.ease,timer:null,state:_te})}function _b(e,t){var r=pa(e,t);if(r.state>_te)throw new Error("too late; already scheduled");return r}function Ka(e,t){var r=pa(e,t);if(r.state>Hk)throw new Error("too late; already running");return r}function pa(e,t){var r=e.__transition;if(!r||!(r=r[t]))throw new Error("transition not found");return r}function aUe(e,t,r){var n=e.__transition,i;n[t]=r,r.timer=Wk(a,0,r.time);function a(h){r.state=Ate,r.timer.restart(o,r.delay,r.time),r.delay<=h&&o(h-r.delay)}s(a,"schedule");function o(h){var d,f,p,m;if(r.state!==Ate)return u();for(d in n)if(m=n[d],m.name===r.name){if(m.state===Hk)return Ab(o);m.state===Rte?(m.state=Rb,m.timer.stop(),m.on.call("interrupt",e,e.__data__,m.index,m.group),delete n[d]):+d{"use strict";bI();qk();nUe=xI("start","end","cancel","interrupt"),iUe=[],_te=0,Ate=1,Uk=2,Hk=3,Rte=4,Yk=5,Rb=6;s(fh,"default");s(_b,"init");s(Ka,"set");s(pa,"get");s(aUe,"create")});function Lb(e,t){var r=e.__transition,n,i,a=!0,o;if(r){t=t==null?null:t+"";for(o in r){if((n=r[o]).name!==t){a=!1;continue}i=n.state>Uk&&n.state{"use strict";oo();s(Lb,"default")});function G8(e){return this.each(function(){Lb(this,e)})}var Dte=F(()=>{"use strict";Lte();s(G8,"default")});function sUe(e,t){var r,n;return function(){var i=Ka(this,e),a=i.tween;if(a!==r){n=r=a;for(var o=0,l=n.length;o{"use strict";oo();s(sUe,"tweenRemove");s(oUe,"tweenFunction");s(z8,"default");s(gy,"tweenValue")});function Ib(e,t){var r;return(typeof t=="number"?Da:t instanceof Zl?rm:(r=Zl(t))?(t=r,rm):fy)(e,t)}var V8=F(()=>{"use strict";hy();py();s(Ib,"default")});function lUe(e){return function(){this.removeAttribute(e)}}function cUe(e){return function(){this.removeAttributeNS(e.space,e.local)}}function uUe(e,t,r){var n,i=r+"",a;return function(){var o=this.getAttribute(e);return o===i?null:o===n?a:a=t(n=o,r)}}function hUe(e,t,r){var n,i=r+"",a;return function(){var o=this.getAttributeNS(e.space,e.local);return o===i?null:o===n?a:a=t(n=o,r)}}function dUe(e,t,r){var n,i,a;return function(){var o,l=r(this),u;return l==null?void this.removeAttribute(e):(o=this.getAttribute(e),u=l+"",o===u?null:o===n&&u===i?a:(i=u,a=t(n=o,l)))}}function fUe(e,t,r){var n,i,a;return function(){var o,l=r(this),u;return l==null?void this.removeAttributeNS(e.space,e.local):(o=this.getAttributeNS(e.space,e.local),u=l+"",o===u?null:o===n&&u===i?a:(i=u,a=t(n=o,l)))}}function W8(e,t){var r=qc(e),n=r==="transform"?O8:Ib;return this.attrTween(e,typeof t=="function"?(r.local?fUe:dUe)(r,n,gy(this,"attr."+e,t)):t==null?(r.local?cUe:lUe)(r):(r.local?hUe:uUe)(r,n,t))}var Ite=F(()=>{"use strict";py();Xl();Db();V8();s(lUe,"attrRemove");s(cUe,"attrRemoveNS");s(uUe,"attrConstant");s(hUe,"attrConstantNS");s(dUe,"attrFunction");s(fUe,"attrFunctionNS");s(W8,"default")});function pUe(e,t){return function(r){this.setAttribute(e,t.call(this,r))}}function mUe(e,t){return function(r){this.setAttributeNS(e.space,e.local,t.call(this,r))}}function gUe(e,t){var r,n;function i(){var a=t.apply(this,arguments);return a!==n&&(r=(n=a)&&mUe(e,a)),r}return s(i,"tween"),i._value=t,i}function yUe(e,t){var r,n;function i(){var a=t.apply(this,arguments);return a!==n&&(r=(n=a)&&pUe(e,a)),r}return s(i,"tween"),i._value=t,i}function q8(e,t){var r="attr."+e;if(arguments.length<2)return(r=this.tween(r))&&r._value;if(t==null)return this.tween(r,null);if(typeof t!="function")throw new Error;var n=qc(e);return this.tween(r,(n.local?gUe:yUe)(n,t))}var Mte=F(()=>{"use strict";Xl();s(pUe,"attrInterpolate");s(mUe,"attrInterpolateNS");s(gUe,"attrTweenNS");s(yUe,"attrTween");s(q8,"default")});function vUe(e,t){return function(){_b(this,e).delay=+t.apply(this,arguments)}}function xUe(e,t){return t=+t,function(){_b(this,e).delay=t}}function H8(e){var t=this._id;return arguments.length?this.each((typeof e=="function"?vUe:xUe)(t,e)):pa(this.node(),t).delay}var Nte=F(()=>{"use strict";oo();s(vUe,"delayFunction");s(xUe,"delayConstant");s(H8,"default")});function bUe(e,t){return function(){Ka(this,e).duration=+t.apply(this,arguments)}}function TUe(e,t){return t=+t,function(){Ka(this,e).duration=t}}function U8(e){var t=this._id;return arguments.length?this.each((typeof e=="function"?bUe:TUe)(t,e)):pa(this.node(),t).duration}var Pte=F(()=>{"use strict";oo();s(bUe,"durationFunction");s(TUe,"durationConstant");s(U8,"default")});function CUe(e,t){if(typeof t!="function")throw new Error;return function(){Ka(this,e).ease=t}}function Y8(e){var t=this._id;return arguments.length?this.each(CUe(t,e)):pa(this.node(),t).ease}var Ote=F(()=>{"use strict";oo();s(CUe,"easeConstant");s(Y8,"default")});function SUe(e,t){return function(){var r=t.apply(this,arguments);if(typeof r!="function")throw new Error;Ka(this,e).ease=r}}function j8(e){if(typeof e!="function")throw new Error;return this.each(SUe(this._id,e))}var Bte=F(()=>{"use strict";oo();s(SUe,"easeVarying");s(j8,"default")});function X8(e){typeof e!="function"&&(e=sy(e));for(var t=this._groups,r=t.length,n=new Array(r),i=0;i{"use strict";Xl();im();s(X8,"default")});function K8(e){if(e._id!==this._id)throw new Error;for(var t=this._groups,r=e._groups,n=t.length,i=r.length,a=Math.min(n,i),o=new Array(n),l=0;l{"use strict";im();s(K8,"default")});function wUe(e){return(e+"").trim().split(/^|\s+/).every(function(t){var r=t.indexOf(".");return r>=0&&(t=t.slice(0,r)),!t||t==="start"})}function kUe(e,t,r){var n,i,a=wUe(t)?_b:Ka;return function(){var o=a(this,e),l=o.on;l!==n&&(i=(n=l).copy()).on(t,r),o.on=i}}function Z8(e,t){var r=this._id;return arguments.length<2?pa(this.node(),r).on.on(e):this.each(kUe(r,e,t))}var Gte=F(()=>{"use strict";oo();s(wUe,"start");s(kUe,"onFunction");s(Z8,"default")});function EUe(e){return function(){var t=this.parentNode;for(var r in this.__transition)if(+r!==e)return;t&&t.removeChild(this)}}function Q8(){return this.on("end.remove",EUe(this._id))}var zte=F(()=>{"use strict";s(EUe,"removeFunction");s(Q8,"default")});function J8(e){var t=this._name,r=this._id;typeof e!="function"&&(e=$d(e));for(var n=this._groups,i=n.length,a=new Array(i),o=0;o{"use strict";Xl();im();oo();s(J8,"default")});function eM(e){var t=this._name,r=this._id;typeof e!="function"&&(e=ay(e));for(var n=this._groups,i=n.length,a=[],o=[],l=0;l{"use strict";Xl();im();oo();s(eM,"default")});function tM(){return new AUe(this._groups,this._parents)}var AUe,qte=F(()=>{"use strict";Xl();AUe=uh.prototype.constructor;s(tM,"default")});function RUe(e,t){var r,n,i;return function(){var a=Fd(this,e),o=(this.style.removeProperty(e),Fd(this,e));return a===o?null:a===r&&o===n?i:i=t(r=a,n=o)}}function Hte(e){return function(){this.style.removeProperty(e)}}function _Ue(e,t,r){var n,i=r+"",a;return function(){var o=Fd(this,e);return o===i?null:o===n?a:a=t(n=o,r)}}function LUe(e,t,r){var n,i,a;return function(){var o=Fd(this,e),l=r(this),u=l+"";return l==null&&(u=l=(this.style.removeProperty(e),Fd(this,e))),o===u?null:o===n&&u===i?a:(i=u,a=t(n=o,l))}}function DUe(e,t){var r,n,i,a="style."+t,o="end."+a,l;return function(){var u=Ka(this,e),h=u.on,d=u.value[a]==null?l||(l=Hte(t)):void 0;(h!==r||i!==d)&&(n=(r=h).copy()).on(o,i=d),u.on=n}}function rM(e,t,r){var n=(e+="")=="transform"?P8:Ib;return t==null?this.styleTween(e,RUe(e,n)).on("end.style."+e,Hte(e)):typeof t=="function"?this.styleTween(e,LUe(e,n,gy(this,"style."+e,t))).each(DUe(this._id,e)):this.styleTween(e,_Ue(e,n,t),r).on("end.style."+e,null)}var Ute=F(()=>{"use strict";py();Xl();oo();Db();V8();s(RUe,"styleNull");s(Hte,"styleRemove");s(_Ue,"styleConstant");s(LUe,"styleFunction");s(DUe,"styleMaybeRemove");s(rM,"default")});function IUe(e,t,r){return function(n){this.style.setProperty(e,t.call(this,n),r)}}function MUe(e,t,r){var n,i;function a(){var o=t.apply(this,arguments);return o!==i&&(n=(i=o)&&IUe(e,o,r)),n}return s(a,"tween"),a._value=t,a}function nM(e,t,r){var n="style."+(e+="");if(arguments.length<2)return(n=this.tween(n))&&n._value;if(t==null)return this.tween(n,null);if(typeof t!="function")throw new Error;return this.tween(n,MUe(e,t,r??""))}var Yte=F(()=>{"use strict";s(IUe,"styleInterpolate");s(MUe,"styleTween");s(nM,"default")});function NUe(e){return function(){this.textContent=e}}function PUe(e){return function(){var t=e(this);this.textContent=t??""}}function iM(e){return this.tween("text",typeof e=="function"?PUe(gy(this,"text",e)):NUe(e==null?"":e+""))}var jte=F(()=>{"use strict";Db();s(NUe,"textConstant");s(PUe,"textFunction");s(iM,"default")});function OUe(e){return function(t){this.textContent=e.call(this,t)}}function BUe(e){var t,r;function n(){var i=e.apply(this,arguments);return i!==r&&(t=(r=i)&&OUe(i)),t}return s(n,"tween"),n._value=e,n}function aM(e){var t="text";if(arguments.length<1)return(t=this.tween(t))&&t._value;if(e==null)return this.tween(t,null);if(typeof e!="function")throw new Error;return this.tween(t,BUe(e))}var Xte=F(()=>{"use strict";s(OUe,"textInterpolate");s(BUe,"textTween");s(aM,"default")});function sM(){for(var e=this._name,t=this._id,r=jk(),n=this._groups,i=n.length,a=0;a{"use strict";im();oo();s(sM,"default")});function oM(){var e,t,r=this,n=r._id,i=r.size();return new Promise(function(a,o){var l={value:o},u={value:s(function(){--i===0&&a()},"value")};r.each(function(){var h=Ka(this,n),d=h.on;d!==e&&(t=(e=d).copy(),t._.cancel.push(l),t._.interrupt.push(l),t._.end.push(u)),h.on=t}),i===0&&a()})}var Zte=F(()=>{"use strict";oo();s(oM,"default")});function Ps(e,t,r,n){this._groups=e,this._parents=t,this._name=r,this._id=n}function Qte(e){return uh().transition(e)}function jk(){return++$Ue}var $Ue,ph,im=F(()=>{"use strict";Xl();Ite();Mte();Nte();Pte();Ote();Bte();$te();Fte();Gte();zte();Vte();Wte();qte();Ute();Yte();jte();Xte();Kte();Db();Zte();$Ue=0;s(Ps,"Transition");s(Qte,"transition");s(jk,"newId");ph=uh.prototype;Ps.prototype=Qte.prototype={constructor:Ps,select:J8,selectAll:eM,selectChild:ph.selectChild,selectChildren:ph.selectChildren,filter:X8,merge:K8,selection:tM,transition:sM,call:ph.call,nodes:ph.nodes,node:ph.node,size:ph.size,empty:ph.empty,each:ph.each,on:Z8,attr:W8,attrTween:q8,style:rM,styleTween:nM,text:iM,textTween:aM,remove:Q8,tween:z8,delay:H8,duration:U8,ease:Y8,easeVarying:j8,end:oM,[Symbol.iterator]:ph[Symbol.iterator]}});function Xk(e){return((e*=2)<=1?e*e*e:(e-=2)*e*e+2)/2}var Jte=F(()=>{"use strict";s(Xk,"cubicInOut")});var lM=F(()=>{"use strict";Jte()});function GUe(e,t){for(var r;!(r=e.__transition)||!(r=r[t]);)if(!(e=e.parentNode))throw new Error(`transition ${t} not found`);return r}function cM(e){var t,r;e instanceof Ps?(t=e._id,e=e._name):(t=jk(),(r=FUe).time=Eb(),e=e==null?null:e+"");for(var n=this._groups,i=n.length,a=0;a{"use strict";im();oo();lM();qk();FUe={time:null,delay:0,duration:250,ease:Xk};s(GUe,"inherit");s(cM,"default")});var tre=F(()=>{"use strict";Xl();Dte();ere();uh.prototype.interrupt=G8;uh.prototype.transition=cM});var Kk=F(()=>{"use strict";tre()});var rre=F(()=>{"use strict"});var nre=F(()=>{"use strict"});var ire=F(()=>{"use strict"});function are(e){return[+e[0],+e[1]]}function zUe(e){return[are(e[0]),are(e[1])]}function uM(e){return{type:e}}var BUt,$Ut,FUt,GUt,zUt,VUt,sre=F(()=>{"use strict";Kk();rre();nre();ire();({abs:BUt,max:$Ut,min:FUt}=Math);s(are,"number1");s(zUe,"number2");GUt={name:"x",handles:["w","e"].map(uM),input:s(function(e,t){return e==null?null:[[+e[0],t[0][1]],[+e[1],t[1][1]]]},"input"),output:s(function(e){return e&&[e[0][0],e[1][0]]},"output")},zUt={name:"y",handles:["n","s"].map(uM),input:s(function(e,t){return e==null?null:[[t[0][0],+e[0]],[t[1][0],+e[1]]]},"input"),output:s(function(e){return e&&[e[0][1],e[1][1]]},"output")},VUt={name:"xy",handles:["n","w","e","s","nw","ne","sw","se"].map(uM),input:s(function(e){return e==null?null:zUe(e)},"input"),output:s(function(e){return e},"output")};s(uM,"type")});var ore=F(()=>{"use strict";sre()});function lre(e){this._+=e[0];for(let t=1,r=e.length;t=0))throw new Error(`invalid digits: ${e}`);if(t>15)return lre;let r=10**t;return function(n){this._+=n[0];for(let i=1,a=n.length;i{"use strict";hM=Math.PI,dM=2*hM,am=1e-6,VUe=dM-am;s(lre,"append");s(WUe,"appendRound");sm=class{static{s(this,"Path")}constructor(t){this._x0=this._y0=this._x1=this._y1=null,this._="",this._append=t==null?lre:WUe(t)}moveTo(t,r){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+r}`}closePath(){this._x1!==null&&(this._x1=this._x0,this._y1=this._y0,this._append`Z`)}lineTo(t,r){this._append`L${this._x1=+t},${this._y1=+r}`}quadraticCurveTo(t,r,n,i){this._append`Q${+t},${+r},${this._x1=+n},${this._y1=+i}`}bezierCurveTo(t,r,n,i,a,o){this._append`C${+t},${+r},${+n},${+i},${this._x1=+a},${this._y1=+o}`}arcTo(t,r,n,i,a){if(t=+t,r=+r,n=+n,i=+i,a=+a,a<0)throw new Error(`negative radius: ${a}`);let o=this._x1,l=this._y1,u=n-t,h=i-r,d=o-t,f=l-r,p=d*d+f*f;if(this._x1===null)this._append`M${this._x1=t},${this._y1=r}`;else if(p>am)if(!(Math.abs(f*u-h*d)>am)||!a)this._append`L${this._x1=t},${this._y1=r}`;else{let m=n-o,g=i-l,y=u*u+h*h,v=m*m+g*g,x=Math.sqrt(y),b=Math.sqrt(p),T=a*Math.tan((hM-Math.acos((y+p-v)/(2*x*b)))/2),w=T/b,C=T/x;Math.abs(w-1)>am&&this._append`L${t+w*d},${r+w*f}`,this._append`A${a},${a},0,0,${+(f*m>d*g)},${this._x1=t+C*u},${this._y1=r+C*h}`}}arc(t,r,n,i,a,o){if(t=+t,r=+r,n=+n,o=!!o,n<0)throw new Error(`negative radius: ${n}`);let l=n*Math.cos(i),u=n*Math.sin(i),h=t+l,d=r+u,f=1^o,p=o?i-a:a-i;this._x1===null?this._append`M${h},${d}`:(Math.abs(this._x1-h)>am||Math.abs(this._y1-d)>am)&&this._append`L${h},${d}`,n&&(p<0&&(p=p%dM+dM),p>VUe?this._append`A${n},${n},0,1,${f},${t-l},${r-u}A${n},${n},0,1,${f},${this._x1=h},${this._y1=d}`:p>am&&this._append`A${n},${n},0,${+(p>=hM)},${f},${this._x1=t+n*Math.cos(a)},${this._y1=r+n*Math.sin(a)}`)}rect(t,r,n,i){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+r}h${n=+n}v${+i}h${-n}Z`}toString(){return this._}};s(cre,"path");cre.prototype=sm.prototype});var fM=F(()=>{"use strict";ure()});var hre=F(()=>{"use strict"});var dre=F(()=>{"use strict"});var fre=F(()=>{"use strict"});var pre=F(()=>{"use strict"});var mre=F(()=>{"use strict"});var gre=F(()=>{"use strict"});var yre=F(()=>{"use strict"});function pM(e){return Math.abs(e=Math.round(e))>=1e21?e.toLocaleString("en").replace(/,/g,""):e.toString(10)}function om(e,t){if((r=(e=t?e.toExponential(t-1):e.toExponential()).indexOf("e"))<0)return null;var r,n=e.slice(0,r);return[n.length>1?n[0]+n.slice(2):n,+e.slice(r+1)]}var Mb=F(()=>{"use strict";s(pM,"default");s(om,"formatDecimalParts")});function Ql(e){return e=om(Math.abs(e)),e?e[1]:NaN}var Nb=F(()=>{"use strict";Mb();s(Ql,"default")});function mM(e,t){return function(r,n){for(var i=r.length,a=[],o=0,l=e[0],u=0;i>0&&l>0&&(u+l+1>n&&(l=Math.max(1,n-u)),a.push(r.substring(i-=l,i+l)),!((u+=l+1)>n));)l=e[o=(o+1)%e.length];return a.reverse().join(t)}}var vre=F(()=>{"use strict";s(mM,"default")});function gM(e){return function(t){return t.replace(/[0-9]/g,function(r){return e[+r]})}}var xre=F(()=>{"use strict";s(gM,"default")});function Wd(e){if(!(t=qUe.exec(e)))throw new Error("invalid format: "+e);var t;return new Zk({fill:t[1],align:t[2],sign:t[3],symbol:t[4],zero:t[5],width:t[6],comma:t[7],precision:t[8]&&t[8].slice(1),trim:t[9],type:t[10]})}function Zk(e){this.fill=e.fill===void 0?" ":e.fill+"",this.align=e.align===void 0?">":e.align+"",this.sign=e.sign===void 0?"-":e.sign+"",this.symbol=e.symbol===void 0?"":e.symbol+"",this.zero=!!e.zero,this.width=e.width===void 0?void 0:+e.width,this.comma=!!e.comma,this.precision=e.precision===void 0?void 0:+e.precision,this.trim=!!e.trim,this.type=e.type===void 0?"":e.type+""}var qUe,yM=F(()=>{"use strict";qUe=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;s(Wd,"formatSpecifier");Wd.prototype=Zk.prototype;s(Zk,"FormatSpecifier");Zk.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(this.width===void 0?"":Math.max(1,this.width|0))+(this.comma?",":"")+(this.precision===void 0?"":"."+Math.max(0,this.precision|0))+(this.trim?"~":"")+this.type}});function vM(e){e:for(var t=e.length,r=1,n=-1,i;r0&&(n=0);break}return n>0?e.slice(0,n)+e.slice(i+1):e}var bre=F(()=>{"use strict";s(vM,"default")});function bM(e,t){var r=om(e,t);if(!r)return e+"";var n=r[0],i=r[1],a=i-(xM=Math.max(-8,Math.min(8,Math.floor(i/3)))*3)+1,o=n.length;return a===o?n:a>o?n+new Array(a-o+1).join("0"):a>0?n.slice(0,a)+"."+n.slice(a):"0."+new Array(1-a).join("0")+om(e,Math.max(0,t+a-1))[0]}var xM,TM=F(()=>{"use strict";Mb();s(bM,"default")});function Qk(e,t){var r=om(e,t);if(!r)return e+"";var n=r[0],i=r[1];return i<0?"0."+new Array(-i).join("0")+n:n.length>i+1?n.slice(0,i+1)+"."+n.slice(i+1):n+new Array(i-n.length+2).join("0")}var Tre=F(()=>{"use strict";Mb();s(Qk,"default")});var CM,Cre=F(()=>{"use strict";Mb();TM();Tre();CM={"%":s((e,t)=>(e*100).toFixed(t),"%"),b:s(e=>Math.round(e).toString(2),"b"),c:s(e=>e+"","c"),d:pM,e:s((e,t)=>e.toExponential(t),"e"),f:s((e,t)=>e.toFixed(t),"f"),g:s((e,t)=>e.toPrecision(t),"g"),o:s(e=>Math.round(e).toString(8),"o"),p:s((e,t)=>Qk(e*100,t),"p"),r:Qk,s:bM,X:s(e=>Math.round(e).toString(16).toUpperCase(),"X"),x:s(e=>Math.round(e).toString(16),"x")}});function Jk(e){return e}var Sre=F(()=>{"use strict";s(Jk,"default")});function SM(e){var t=e.grouping===void 0||e.thousands===void 0?Jk:mM(wre.call(e.grouping,Number),e.thousands+""),r=e.currency===void 0?"":e.currency[0]+"",n=e.currency===void 0?"":e.currency[1]+"",i=e.decimal===void 0?".":e.decimal+"",a=e.numerals===void 0?Jk:gM(wre.call(e.numerals,String)),o=e.percent===void 0?"%":e.percent+"",l=e.minus===void 0?"\u2212":e.minus+"",u=e.nan===void 0?"NaN":e.nan+"";function h(f){f=Wd(f);var p=f.fill,m=f.align,g=f.sign,y=f.symbol,v=f.zero,x=f.width,b=f.comma,T=f.precision,w=f.trim,C=f.type;C==="n"?(b=!0,C="g"):CM[C]||(T===void 0&&(T=12),w=!0,C="g"),(v||p==="0"&&m==="=")&&(v=!0,p="0",m="=");var S=y==="$"?r:y==="#"&&/[boxX]/.test(C)?"0"+C.toLowerCase():"",k=y==="$"?n:/[%p]/.test(C)?o:"",R=CM[C],M=/[defgprs%]/.test(C);T=T===void 0?6:/[gprs]/.test(C)?Math.max(1,Math.min(21,T)):Math.max(0,Math.min(20,T));function P(N){var _=S,A=k,I,D,L;if(C==="c")A=R(N)+A,N="";else{N=+N;var B=N<0||1/N<0;if(N=isNaN(N)?u:R(Math.abs(N),T),w&&(N=vM(N)),B&&+N==0&&g!=="+"&&(B=!1),_=(B?g==="("?g:l:g==="-"||g==="("?"":g)+_,A=(C==="s"?kre[8+xM/3]:"")+A+(B&&g==="("?")":""),M){for(I=-1,D=N.length;++IL||L>57){A=(L===46?i+N.slice(I+1):N.slice(I))+A,N=N.slice(0,I);break}}}b&&!v&&(N=t(N,1/0));var O=_.length+N.length+A.length,$=O>1)+_+N+A+$.slice(O);break;default:N=$+_+N+A;break}return a(N)}return s(P,"format"),P.toString=function(){return f+""},P}s(h,"newFormat");function d(f,p){var m=h((f=Wd(f),f.type="f",f)),g=Math.max(-8,Math.min(8,Math.floor(Ql(p)/3)))*3,y=Math.pow(10,-g),v=kre[8+g/3];return function(x){return m(y*x)+v}}return s(d,"formatPrefix"),{format:h,formatPrefix:d}}var wre,kre,Ere=F(()=>{"use strict";Nb();vre();xre();yM();bre();Cre();TM();Sre();wre=Array.prototype.map,kre=["y","z","a","f","p","n","\xB5","m","","k","M","G","T","P","E","Z","Y"];s(SM,"default")});function wM(e){return eE=SM(e),Yc=eE.format,tE=eE.formatPrefix,eE}var eE,Yc,tE,Are=F(()=>{"use strict";Ere();wM({thousands:",",grouping:[3],currency:["$",""]});s(wM,"defaultLocale")});function rE(e){return Math.max(0,-Ql(Math.abs(e)))}var Rre=F(()=>{"use strict";Nb();s(rE,"default")});function nE(e,t){return Math.max(0,Math.max(-8,Math.min(8,Math.floor(Ql(t)/3)))*3-Ql(Math.abs(e)))}var _re=F(()=>{"use strict";Nb();s(nE,"default")});function iE(e,t){return e=Math.abs(e),t=Math.abs(t)-e,Math.max(0,Ql(t)-Ql(e))+1}var Lre=F(()=>{"use strict";Nb();s(iE,"default")});var kM=F(()=>{"use strict";Are();yM();Rre();_re();Lre()});var Dre=F(()=>{"use strict"});function HUe(e){var t=0,r=e.children,n=r&&r.length;if(!n)t=1;else for(;--n>=0;)t+=r[n].value;e.value=t}function EM(){return this.eachAfter(HUe)}var Ire=F(()=>{"use strict";s(HUe,"count");s(EM,"default")});function AM(e,t){let r=-1;for(let n of this)e.call(t,n,++r,this);return this}var Mre=F(()=>{"use strict";s(AM,"default")});function RM(e,t){for(var r=this,n=[r],i,a,o=-1;r=n.pop();)if(e.call(t,r,++o,this),i=r.children)for(a=i.length-1;a>=0;--a)n.push(i[a]);return this}var Nre=F(()=>{"use strict";s(RM,"default")});function _M(e,t){for(var r=this,n=[r],i=[],a,o,l,u=-1;r=n.pop();)if(i.push(r),a=r.children)for(o=0,l=a.length;o{"use strict";s(_M,"default")});function LM(e,t){let r=-1;for(let n of this)if(e.call(t,n,++r,this))return n}var Ore=F(()=>{"use strict";s(LM,"default")});function DM(e){return this.eachAfter(function(t){for(var r=+e(t.data)||0,n=t.children,i=n&&n.length;--i>=0;)r+=n[i].value;t.value=r})}var Bre=F(()=>{"use strict";s(DM,"default")});function IM(e){return this.eachBefore(function(t){t.children&&t.children.sort(e)})}var $re=F(()=>{"use strict";s(IM,"default")});function MM(e){for(var t=this,r=UUe(t,e),n=[t];t!==r;)t=t.parent,n.push(t);for(var i=n.length;e!==r;)n.splice(i,0,e),e=e.parent;return n}function UUe(e,t){if(e===t)return e;var r=e.ancestors(),n=t.ancestors(),i=null;for(e=r.pop(),t=n.pop();e===t;)i=e,e=r.pop(),t=n.pop();return i}var Fre=F(()=>{"use strict";s(MM,"default");s(UUe,"leastCommonAncestor")});function NM(){for(var e=this,t=[e];e=e.parent;)t.push(e);return t}var Gre=F(()=>{"use strict";s(NM,"default")});function PM(){return Array.from(this)}var zre=F(()=>{"use strict";s(PM,"default")});function OM(){var e=[];return this.eachBefore(function(t){t.children||e.push(t)}),e}var Vre=F(()=>{"use strict";s(OM,"default")});function BM(){var e=this,t=[];return e.each(function(r){r!==e&&t.push({source:r.parent,target:r})}),t}var Wre=F(()=>{"use strict";s(BM,"default")});function*$M(){var e=this,t,r=[e],n,i,a;do for(t=r.reverse(),r=[];e=t.pop();)if(yield e,n=e.children)for(i=0,a=n.length;i{"use strict";s($M,"default")});function yy(e,t){e instanceof Map?(e=[void 0,e],t===void 0&&(t=XUe)):t===void 0&&(t=jUe);for(var r=new Pb(e),n,i=[r],a,o,l,u;n=i.pop();)if((o=t(n.data))&&(u=(o=Array.from(o)).length))for(n.children=o,l=u-1;l>=0;--l)i.push(a=o[l]=new Pb(o[l])),a.parent=n,a.depth=n.depth+1;return r.eachBefore(ZUe)}function YUe(){return yy(this).eachBefore(KUe)}function jUe(e){return e.children}function XUe(e){return Array.isArray(e)?e[1]:null}function KUe(e){e.data.value!==void 0&&(e.value=e.data.value),e.data=e.data.data}function ZUe(e){var t=0;do e.height=t;while((e=e.parent)&&e.height<++t)}function Pb(e){this.data=e,this.depth=this.height=0,this.parent=null}var Hre=F(()=>{"use strict";Ire();Mre();Nre();Pre();Ore();Bre();$re();Fre();Gre();zre();Vre();Wre();qre();s(yy,"hierarchy");s(YUe,"node_copy");s(jUe,"objectChildren");s(XUe,"mapChildren");s(KUe,"copyData");s(ZUe,"computeHeight");s(Pb,"Node");Pb.prototype=yy.prototype={constructor:Pb,count:EM,each:AM,eachAfter:_M,eachBefore:RM,find:LM,sum:DM,sort:IM,path:MM,ancestors:NM,descendants:PM,leaves:OM,links:BM,copy:YUe,[Symbol.iterator]:$M}});function Ure(e){if(typeof e!="function")throw new Error;return e}var Yre=F(()=>{"use strict";s(Ure,"required")});function vy(){return 0}function lm(e){return function(){return e}}var jre=F(()=>{"use strict";s(vy,"constantZero");s(lm,"default")});function FM(e){e.x0=Math.round(e.x0),e.y0=Math.round(e.y0),e.x1=Math.round(e.x1),e.y1=Math.round(e.y1)}var Xre=F(()=>{"use strict";s(FM,"default")});function GM(e,t,r,n,i){for(var a=e.children,o,l=-1,u=a.length,h=e.value&&(n-t)/e.value;++l{"use strict";s(GM,"default")});function zM(e,t,r,n,i){for(var a=e.children,o,l=-1,u=a.length,h=e.value&&(i-r)/e.value;++l{"use strict";s(zM,"default")});function JUe(e,t,r,n,i,a){for(var o=[],l=t.children,u,h,d=0,f=0,p=l.length,m,g,y=t.value,v,x,b,T,w,C,S;db&&(b=h),S=v*v*C,T=Math.max(b/S,S/x),T>w){v-=h;break}w=T}o.push(u={value:v,dice:m{"use strict";Kre();Zre();QUe=(1+Math.sqrt(5))/2;s(JUe,"squarifyRatio");Qre=s((function e(t){function r(n,i,a,o,l){JUe(t,n,i,a,o,l)}return s(r,"squarify"),r.ratio=function(n){return e((n=+n)>1?n:1)},r}),"custom")(QUe)});function aE(){var e=Qre,t=!1,r=1,n=1,i=[0],a=vy,o=vy,l=vy,u=vy,h=vy;function d(p){return p.x0=p.y0=0,p.x1=r,p.y1=n,p.eachBefore(f),i=[0],t&&p.eachBefore(FM),p}s(d,"treemap");function f(p){var m=i[p.depth],g=p.x0+m,y=p.y0+m,v=p.x1-m,x=p.y1-m;v{"use strict";Xre();Jre();Yre();jre();s(aE,"default")});var tne=F(()=>{"use strict";Hre();ene()});var rne=F(()=>{"use strict"});var nne=F(()=>{"use strict"});function qd(e,t){switch(arguments.length){case 0:break;case 1:this.range(e);break;default:this.range(t).domain(e);break}return this}var Ob=F(()=>{"use strict";s(qd,"initRange")});function Go(){var e=new ny,t=[],r=[],n=VM;function i(a){let o=e.get(a);if(o===void 0){if(n!==VM)return n;e.set(a,o=t.push(a)-1)}return r[o%r.length]}return s(i,"scale"),i.domain=function(a){if(!arguments.length)return t.slice();t=[],e=new ny;for(let o of a)e.has(o)||e.set(o,t.push(o)-1);return i},i.range=function(a){return arguments.length?(r=Array.from(a),i):r.slice()},i.unknown=function(a){return arguments.length?(n=a,i):n},i.copy=function(){return Go(t,r).unknown(n)},qd.apply(i,arguments),i}var VM,WM=F(()=>{"use strict";Bd();Ob();VM=Symbol("implicit");s(Go,"ordinal")});function xy(){var e=Go().unknown(void 0),t=e.domain,r=e.range,n=0,i=1,a,o,l=!1,u=0,h=0,d=.5;delete e.unknown;function f(){var p=t().length,m=i{"use strict";Bd();Ob();WM();s(xy,"band")});function qM(e){return function(){return e}}var ane=F(()=>{"use strict";s(qM,"constants")});function HM(e){return+e}var sne=F(()=>{"use strict";s(HM,"number")});function by(e){return e}function UM(e,t){return(t-=e=+e)?function(r){return(r-e)/t}:qM(isNaN(t)?NaN:.5)}function eYe(e,t){var r;return e>t&&(r=e,e=t,t=r),function(n){return Math.max(e,Math.min(t,n))}}function tYe(e,t,r){var n=e[0],i=e[1],a=t[0],o=t[1];return i2?rYe:tYe,u=h=null,f}s(d,"rescale");function f(p){return p==null||isNaN(p=+p)?a:(u||(u=l(e.map(n),t,r)))(n(o(p)))}return s(f,"scale"),f.invert=function(p){return o(i((h||(h=l(t,e.map(n),Da)))(p)))},f.domain=function(p){return arguments.length?(e=Array.from(p,HM),d()):e.slice()},f.range=function(p){return arguments.length?(t=Array.from(p),d()):t.slice()},f.rangeRound=function(p){return t=Array.from(p),r=Ok,d()},f.clamp=function(p){return arguments.length?(o=p?!0:by,d()):o!==by},f.interpolate=function(p){return arguments.length?(r=p,d()):r},f.unknown=function(p){return arguments.length?(a=p,f):a},function(p,m){return n=p,i=m,d()}}function Bb(){return nYe()(by,by)}var one,YM=F(()=>{"use strict";Bd();py();ane();sne();one=[0,1];s(by,"identity");s(UM,"normalize");s(eYe,"clamper");s(tYe,"bimap");s(rYe,"polymap");s(sE,"copy");s(nYe,"transformer");s(Bb,"continuous")});function jM(e,t,r,n){var i=iy(e,t,r),a;switch(n=Wd(n??",f"),n.type){case"s":{var o=Math.max(Math.abs(e),Math.abs(t));return n.precision==null&&!isNaN(a=nE(i,o))&&(n.precision=a),tE(n,o)}case"":case"e":case"g":case"p":case"r":{n.precision==null&&!isNaN(a=iE(i,Math.max(Math.abs(e),Math.abs(t))))&&(n.precision=a-(n.type==="e"));break}case"f":case"%":{n.precision==null&&!isNaN(a=rE(i))&&(n.precision=a-(n.type==="%")*2);break}}return Yc(n)}var lne=F(()=>{"use strict";Bd();kM();s(jM,"tickFormat")});function iYe(e){var t=e.domain;return e.ticks=function(r){var n=t();return xk(n[0],n[n.length-1],r??10)},e.tickFormat=function(r,n){var i=t();return jM(i[0],i[i.length-1],r??10,n)},e.nice=function(r){r==null&&(r=10);var n=t(),i=0,a=n.length-1,o=n[i],l=n[a],u,h,d=10;for(l0;){if(h=hb(o,l,r),h===u)return n[i]=o,n[a]=l,t(n);if(h>0)o=Math.floor(o/h)*h,l=Math.ceil(l/h)*h;else if(h<0)o=Math.ceil(o*h)/h,l=Math.floor(l*h)/h;else break;u=h}return e},e}function Jl(){var e=Bb();return e.copy=function(){return sE(e,Jl())},qd.apply(e,arguments),iYe(e)}var cne=F(()=>{"use strict";Bd();YM();Ob();lne();s(iYe,"linearish");s(Jl,"linear")});function XM(e,t){e=e.slice();var r=0,n=e.length-1,i=e[r],a=e[n],o;return a{"use strict";s(XM,"nice")});function jn(e,t,r,n){function i(a){return e(a=arguments.length===0?new Date:new Date(+a)),a}return s(i,"interval"),i.floor=a=>(e(a=new Date(+a)),a),i.ceil=a=>(e(a=new Date(a-1)),t(a,1),e(a),a),i.round=a=>{let o=i(a),l=i.ceil(a);return a-o(t(a=new Date(+a),o==null?1:Math.floor(o)),a),i.range=(a,o,l)=>{let u=[];if(a=i.ceil(a),l=l==null?1:Math.floor(l),!(a0))return u;let h;do u.push(h=new Date(+a)),t(a,l),e(a);while(hjn(o=>{if(o>=o)for(;e(o),!a(o);)o.setTime(o-1)},(o,l)=>{if(o>=o)if(l<0)for(;++l<=0;)for(;t(o,-1),!a(o););else for(;--l>=0;)for(;t(o,1),!a(o););}),r&&(i.count=(a,o)=>(KM.setTime(+a),ZM.setTime(+o),e(KM),e(ZM),Math.floor(r(KM,ZM))),i.every=a=>(a=Math.floor(a),!isFinite(a)||!(a>0)?null:a>1?i.filter(n?o=>n(o)%a===0:o=>i.count(0,o)%a===0):i)),i}var KM,ZM,mh=F(()=>{"use strict";KM=new Date,ZM=new Date;s(jn,"timeInterval")});var jc,hne,QM=F(()=>{"use strict";mh();jc=jn(()=>{},(e,t)=>{e.setTime(+e+t)},(e,t)=>t-e);jc.every=e=>(e=Math.floor(e),!isFinite(e)||!(e>0)?null:e>1?jn(t=>{t.setTime(Math.floor(t/e)*e)},(t,r)=>{t.setTime(+t+r*e)},(t,r)=>(r-t)/e):jc);hne=jc.range});var zo,dne,JM=F(()=>{"use strict";mh();zo=jn(e=>{e.setTime(e-e.getMilliseconds())},(e,t)=>{e.setTime(+e+t*1e3)},(e,t)=>(t-e)/1e3,e=>e.getUTCSeconds()),dne=zo.range});var gh,aYe,oE,sYe,eN=F(()=>{"use strict";mh();gh=jn(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*1e3)},(e,t)=>{e.setTime(+e+t*6e4)},(e,t)=>(t-e)/6e4,e=>e.getMinutes()),aYe=gh.range,oE=jn(e=>{e.setUTCSeconds(0,0)},(e,t)=>{e.setTime(+e+t*6e4)},(e,t)=>(t-e)/6e4,e=>e.getUTCMinutes()),sYe=oE.range});var yh,oYe,lE,lYe,tN=F(()=>{"use strict";mh();yh=jn(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*1e3-e.getMinutes()*6e4)},(e,t)=>{e.setTime(+e+t*36e5)},(e,t)=>(t-e)/36e5,e=>e.getHours()),oYe=yh.range,lE=jn(e=>{e.setUTCMinutes(0,0,0)},(e,t)=>{e.setTime(+e+t*36e5)},(e,t)=>(t-e)/36e5,e=>e.getUTCHours()),lYe=lE.range});var gl,cYe,Fb,uYe,cE,hYe,rN=F(()=>{"use strict";mh();gl=jn(e=>e.setHours(0,0,0,0),(e,t)=>e.setDate(e.getDate()+t),(e,t)=>(t-e-(t.getTimezoneOffset()-e.getTimezoneOffset())*6e4)/864e5,e=>e.getDate()-1),cYe=gl.range,Fb=jn(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/864e5,e=>e.getUTCDate()-1),uYe=Fb.range,cE=jn(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/864e5,e=>Math.floor(e/864e5)),hYe=cE.range});function hm(e){return jn(t=>{t.setDate(t.getDate()-(t.getDay()+7-e)%7),t.setHours(0,0,0,0)},(t,r)=>{t.setDate(t.getDate()+r*7)},(t,r)=>(r-t-(r.getTimezoneOffset()-t.getTimezoneOffset())*6e4)/6048e5)}function dm(e){return jn(t=>{t.setUTCDate(t.getUTCDate()-(t.getUTCDay()+7-e)%7),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCDate(t.getUTCDate()+r*7)},(t,r)=>(r-t)/6048e5)}var ec,Hd,uE,hE,Kc,dE,fE,pne,dYe,fYe,pYe,mYe,gYe,yYe,fm,Ty,mne,gne,Ud,yne,vne,xne,vYe,xYe,bYe,TYe,CYe,SYe,nN=F(()=>{"use strict";mh();s(hm,"timeWeekday");ec=hm(0),Hd=hm(1),uE=hm(2),hE=hm(3),Kc=hm(4),dE=hm(5),fE=hm(6),pne=ec.range,dYe=Hd.range,fYe=uE.range,pYe=hE.range,mYe=Kc.range,gYe=dE.range,yYe=fE.range;s(dm,"utcWeekday");fm=dm(0),Ty=dm(1),mne=dm(2),gne=dm(3),Ud=dm(4),yne=dm(5),vne=dm(6),xne=fm.range,vYe=Ty.range,xYe=mne.range,bYe=gne.range,TYe=Ud.range,CYe=yne.range,SYe=vne.range});var vh,wYe,pE,kYe,iN=F(()=>{"use strict";mh();vh=jn(e=>{e.setDate(1),e.setHours(0,0,0,0)},(e,t)=>{e.setMonth(e.getMonth()+t)},(e,t)=>t.getMonth()-e.getMonth()+(t.getFullYear()-e.getFullYear())*12,e=>e.getMonth()),wYe=vh.range,pE=jn(e=>{e.setUTCDate(1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCMonth(e.getUTCMonth()+t)},(e,t)=>t.getUTCMonth()-e.getUTCMonth()+(t.getUTCFullYear()-e.getUTCFullYear())*12,e=>e.getUTCMonth()),kYe=pE.range});var Vo,EYe,tc,AYe,aN=F(()=>{"use strict";mh();Vo=jn(e=>{e.setMonth(0,1),e.setHours(0,0,0,0)},(e,t)=>{e.setFullYear(e.getFullYear()+t)},(e,t)=>t.getFullYear()-e.getFullYear(),e=>e.getFullYear());Vo.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:jn(t=>{t.setFullYear(Math.floor(t.getFullYear()/e)*e),t.setMonth(0,1),t.setHours(0,0,0,0)},(t,r)=>{t.setFullYear(t.getFullYear()+r*e)});EYe=Vo.range,tc=jn(e=>{e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCFullYear(e.getUTCFullYear()+t)},(e,t)=>t.getUTCFullYear()-e.getUTCFullYear(),e=>e.getUTCFullYear());tc.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:jn(t=>{t.setUTCFullYear(Math.floor(t.getUTCFullYear()/e)*e),t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCFullYear(t.getUTCFullYear()+r*e)});AYe=tc.range});function Tne(e,t,r,n,i,a){let o=[[zo,1,1e3],[zo,5,5*1e3],[zo,15,15*1e3],[zo,30,30*1e3],[a,1,6e4],[a,5,5*6e4],[a,15,15*6e4],[a,30,30*6e4],[i,1,36e5],[i,3,3*36e5],[i,6,6*36e5],[i,12,12*36e5],[n,1,864e5],[n,2,2*864e5],[r,1,6048e5],[t,1,2592e6],[t,3,3*2592e6],[e,1,31536e6]];function l(h,d,f){let p=dv).right(o,p);if(m===o.length)return e.every(iy(h/31536e6,d/31536e6,f));if(m===0)return jc.every(Math.max(iy(h,d,f),1));let[g,y]=o[p/o[m-1][2]{"use strict";Bd();QM();JM();eN();tN();rN();nN();iN();aN();s(Tne,"ticker");[_Ye,LYe]=Tne(tc,pE,fm,cE,lE,oE),[sN,oN]=Tne(Vo,vh,ec,gl,yh,gh)});var mE=F(()=>{"use strict";QM();JM();eN();tN();rN();nN();iN();aN();Cne()});function lN(e){if(0<=e.y&&e.y<100){var t=new Date(-1,e.m,e.d,e.H,e.M,e.S,e.L);return t.setFullYear(e.y),t}return new Date(e.y,e.m,e.d,e.H,e.M,e.S,e.L)}function cN(e){if(0<=e.y&&e.y<100){var t=new Date(Date.UTC(-1,e.m,e.d,e.H,e.M,e.S,e.L));return t.setUTCFullYear(e.y),t}return new Date(Date.UTC(e.y,e.m,e.d,e.H,e.M,e.S,e.L))}function Gb(e,t,r){return{y:e,m:t,d:r,H:0,M:0,S:0,L:0}}function uN(e){var t=e.dateTime,r=e.date,n=e.time,i=e.periods,a=e.days,o=e.shortDays,l=e.months,u=e.shortMonths,h=zb(i),d=Vb(i),f=zb(a),p=Vb(a),m=zb(o),g=Vb(o),y=zb(l),v=Vb(l),x=zb(u),b=Vb(u),T={a:B,A:O,b:$,B:W,c:null,d:Rne,e:Rne,f:JYe,g:cje,G:hje,H:KYe,I:ZYe,j:QYe,L:Mne,m:eje,M:tje,p:V,q:z,Q:Dne,s:Ine,S:rje,u:nje,U:ije,V:aje,w:sje,W:oje,x:null,X:null,y:lje,Y:uje,Z:dje,"%":Lne},w={a:H,A:K,b:J,B:ne,c:null,d:_ne,e:_ne,f:gje,g:Eje,G:Rje,H:fje,I:pje,j:mje,L:Pne,m:yje,M:vje,p:Z,q:pe,Q:Dne,s:Ine,S:xje,u:bje,U:Tje,V:Cje,w:Sje,W:wje,x:null,X:null,y:kje,Y:Aje,Z:_je,"%":Lne},C={a:P,A:N,b:_,B:A,c:I,d:Ene,e:Ene,f:UYe,g:kne,G:wne,H:Ane,I:Ane,j:VYe,L:HYe,m:zYe,M:WYe,p:M,q:GYe,Q:jYe,s:XYe,S:qYe,u:PYe,U:OYe,V:BYe,w:NYe,W:$Ye,x:D,X:L,y:kne,Y:wne,Z:FYe,"%":YYe};T.x=S(r,T),T.X=S(n,T),T.c=S(t,T),w.x=S(r,w),w.X=S(n,w),w.c=S(t,w);function S(te,fe){return function(ce){var de=[],Ee=-1,we=0,Fe=te.length,Ye,He,ye;for(ce instanceof Date||(ce=new Date(+ce));++Ee53)return null;"w"in de||(de.w=1),"Z"in de?(we=cN(Gb(de.y,0,1)),Fe=we.getUTCDay(),we=Fe>4||Fe===0?Ty.ceil(we):Ty(we),we=Fb.offset(we,(de.V-1)*7),de.y=we.getUTCFullYear(),de.m=we.getUTCMonth(),de.d=we.getUTCDate()+(de.w+6)%7):(we=lN(Gb(de.y,0,1)),Fe=we.getDay(),we=Fe>4||Fe===0?Hd.ceil(we):Hd(we),we=gl.offset(we,(de.V-1)*7),de.y=we.getFullYear(),de.m=we.getMonth(),de.d=we.getDate()+(de.w+6)%7)}else("W"in de||"U"in de)&&("w"in de||(de.w="u"in de?de.u%7:"W"in de?1:0),Fe="Z"in de?cN(Gb(de.y,0,1)).getUTCDay():lN(Gb(de.y,0,1)).getDay(),de.m=0,de.d="W"in de?(de.w+6)%7+de.W*7-(Fe+5)%7:de.w+de.U*7-(Fe+6)%7);return"Z"in de?(de.H+=de.Z/100|0,de.M+=de.Z%100,cN(de)):lN(de)}}s(k,"newParse");function R(te,fe,ce,de){for(var Ee=0,we=fe.length,Fe=ce.length,Ye,He;Ee=Fe)return-1;if(Ye=fe.charCodeAt(Ee++),Ye===37){if(Ye=fe.charAt(Ee++),He=C[Ye in Sne?fe.charAt(Ee++):Ye],!He||(de=He(te,ce,de))<0)return-1}else if(Ye!=ce.charCodeAt(de++))return-1}return de}s(R,"parseSpecifier");function M(te,fe,ce){var de=h.exec(fe.slice(ce));return de?(te.p=d.get(de[0].toLowerCase()),ce+de[0].length):-1}s(M,"parsePeriod");function P(te,fe,ce){var de=m.exec(fe.slice(ce));return de?(te.w=g.get(de[0].toLowerCase()),ce+de[0].length):-1}s(P,"parseShortWeekday");function N(te,fe,ce){var de=f.exec(fe.slice(ce));return de?(te.w=p.get(de[0].toLowerCase()),ce+de[0].length):-1}s(N,"parseWeekday");function _(te,fe,ce){var de=x.exec(fe.slice(ce));return de?(te.m=b.get(de[0].toLowerCase()),ce+de[0].length):-1}s(_,"parseShortMonth");function A(te,fe,ce){var de=y.exec(fe.slice(ce));return de?(te.m=v.get(de[0].toLowerCase()),ce+de[0].length):-1}s(A,"parseMonth");function I(te,fe,ce){return R(te,t,fe,ce)}s(I,"parseLocaleDateTime");function D(te,fe,ce){return R(te,r,fe,ce)}s(D,"parseLocaleDate");function L(te,fe,ce){return R(te,n,fe,ce)}s(L,"parseLocaleTime");function B(te){return o[te.getDay()]}s(B,"formatShortWeekday");function O(te){return a[te.getDay()]}s(O,"formatWeekday");function $(te){return u[te.getMonth()]}s($,"formatShortMonth");function W(te){return l[te.getMonth()]}s(W,"formatMonth");function V(te){return i[+(te.getHours()>=12)]}s(V,"formatPeriod");function z(te){return 1+~~(te.getMonth()/3)}s(z,"formatQuarter");function H(te){return o[te.getUTCDay()]}s(H,"formatUTCShortWeekday");function K(te){return a[te.getUTCDay()]}s(K,"formatUTCWeekday");function J(te){return u[te.getUTCMonth()]}s(J,"formatUTCShortMonth");function ne(te){return l[te.getUTCMonth()]}s(ne,"formatUTCMonth");function Z(te){return i[+(te.getUTCHours()>=12)]}s(Z,"formatUTCPeriod");function pe(te){return 1+~~(te.getUTCMonth()/3)}return s(pe,"formatUTCQuarter"),{format:s(function(te){var fe=S(te+="",T);return fe.toString=function(){return te},fe},"format"),parse:s(function(te){var fe=k(te+="",!1);return fe.toString=function(){return te},fe},"parse"),utcFormat:s(function(te){var fe=S(te+="",w);return fe.toString=function(){return te},fe},"utcFormat"),utcParse:s(function(te){var fe=k(te+="",!0);return fe.toString=function(){return te},fe},"utcParse")}}function bn(e,t,r){var n=e<0?"-":"",i=(n?-e:e)+"",a=i.length;return n+(a[t.toLowerCase(),r]))}function NYe(e,t,r){var n=Ia.exec(t.slice(r,r+1));return n?(e.w=+n[0],r+n[0].length):-1}function PYe(e,t,r){var n=Ia.exec(t.slice(r,r+1));return n?(e.u=+n[0],r+n[0].length):-1}function OYe(e,t,r){var n=Ia.exec(t.slice(r,r+2));return n?(e.U=+n[0],r+n[0].length):-1}function BYe(e,t,r){var n=Ia.exec(t.slice(r,r+2));return n?(e.V=+n[0],r+n[0].length):-1}function $Ye(e,t,r){var n=Ia.exec(t.slice(r,r+2));return n?(e.W=+n[0],r+n[0].length):-1}function wne(e,t,r){var n=Ia.exec(t.slice(r,r+4));return n?(e.y=+n[0],r+n[0].length):-1}function kne(e,t,r){var n=Ia.exec(t.slice(r,r+2));return n?(e.y=+n[0]+(+n[0]>68?1900:2e3),r+n[0].length):-1}function FYe(e,t,r){var n=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(t.slice(r,r+6));return n?(e.Z=n[1]?0:-(n[2]+(n[3]||"00")),r+n[0].length):-1}function GYe(e,t,r){var n=Ia.exec(t.slice(r,r+1));return n?(e.q=n[0]*3-3,r+n[0].length):-1}function zYe(e,t,r){var n=Ia.exec(t.slice(r,r+2));return n?(e.m=n[0]-1,r+n[0].length):-1}function Ene(e,t,r){var n=Ia.exec(t.slice(r,r+2));return n?(e.d=+n[0],r+n[0].length):-1}function VYe(e,t,r){var n=Ia.exec(t.slice(r,r+3));return n?(e.m=0,e.d=+n[0],r+n[0].length):-1}function Ane(e,t,r){var n=Ia.exec(t.slice(r,r+2));return n?(e.H=+n[0],r+n[0].length):-1}function WYe(e,t,r){var n=Ia.exec(t.slice(r,r+2));return n?(e.M=+n[0],r+n[0].length):-1}function qYe(e,t,r){var n=Ia.exec(t.slice(r,r+2));return n?(e.S=+n[0],r+n[0].length):-1}function HYe(e,t,r){var n=Ia.exec(t.slice(r,r+3));return n?(e.L=+n[0],r+n[0].length):-1}function UYe(e,t,r){var n=Ia.exec(t.slice(r,r+6));return n?(e.L=Math.floor(n[0]/1e3),r+n[0].length):-1}function YYe(e,t,r){var n=DYe.exec(t.slice(r,r+1));return n?r+n[0].length:-1}function jYe(e,t,r){var n=Ia.exec(t.slice(r));return n?(e.Q=+n[0],r+n[0].length):-1}function XYe(e,t,r){var n=Ia.exec(t.slice(r));return n?(e.s=+n[0],r+n[0].length):-1}function Rne(e,t){return bn(e.getDate(),t,2)}function KYe(e,t){return bn(e.getHours(),t,2)}function ZYe(e,t){return bn(e.getHours()%12||12,t,2)}function QYe(e,t){return bn(1+gl.count(Vo(e),e),t,3)}function Mne(e,t){return bn(e.getMilliseconds(),t,3)}function JYe(e,t){return Mne(e,t)+"000"}function eje(e,t){return bn(e.getMonth()+1,t,2)}function tje(e,t){return bn(e.getMinutes(),t,2)}function rje(e,t){return bn(e.getSeconds(),t,2)}function nje(e){var t=e.getDay();return t===0?7:t}function ije(e,t){return bn(ec.count(Vo(e)-1,e),t,2)}function Nne(e){var t=e.getDay();return t>=4||t===0?Kc(e):Kc.ceil(e)}function aje(e,t){return e=Nne(e),bn(Kc.count(Vo(e),e)+(Vo(e).getDay()===4),t,2)}function sje(e){return e.getDay()}function oje(e,t){return bn(Hd.count(Vo(e)-1,e),t,2)}function lje(e,t){return bn(e.getFullYear()%100,t,2)}function cje(e,t){return e=Nne(e),bn(e.getFullYear()%100,t,2)}function uje(e,t){return bn(e.getFullYear()%1e4,t,4)}function hje(e,t){var r=e.getDay();return e=r>=4||r===0?Kc(e):Kc.ceil(e),bn(e.getFullYear()%1e4,t,4)}function dje(e){var t=e.getTimezoneOffset();return(t>0?"-":(t*=-1,"+"))+bn(t/60|0,"0",2)+bn(t%60,"0",2)}function _ne(e,t){return bn(e.getUTCDate(),t,2)}function fje(e,t){return bn(e.getUTCHours(),t,2)}function pje(e,t){return bn(e.getUTCHours()%12||12,t,2)}function mje(e,t){return bn(1+Fb.count(tc(e),e),t,3)}function Pne(e,t){return bn(e.getUTCMilliseconds(),t,3)}function gje(e,t){return Pne(e,t)+"000"}function yje(e,t){return bn(e.getUTCMonth()+1,t,2)}function vje(e,t){return bn(e.getUTCMinutes(),t,2)}function xje(e,t){return bn(e.getUTCSeconds(),t,2)}function bje(e){var t=e.getUTCDay();return t===0?7:t}function Tje(e,t){return bn(fm.count(tc(e)-1,e),t,2)}function One(e){var t=e.getUTCDay();return t>=4||t===0?Ud(e):Ud.ceil(e)}function Cje(e,t){return e=One(e),bn(Ud.count(tc(e),e)+(tc(e).getUTCDay()===4),t,2)}function Sje(e){return e.getUTCDay()}function wje(e,t){return bn(Ty.count(tc(e)-1,e),t,2)}function kje(e,t){return bn(e.getUTCFullYear()%100,t,2)}function Eje(e,t){return e=One(e),bn(e.getUTCFullYear()%100,t,2)}function Aje(e,t){return bn(e.getUTCFullYear()%1e4,t,4)}function Rje(e,t){var r=e.getUTCDay();return e=r>=4||r===0?Ud(e):Ud.ceil(e),bn(e.getUTCFullYear()%1e4,t,4)}function _je(){return"+0000"}function Lne(){return"%"}function Dne(e){return+e}function Ine(e){return Math.floor(+e/1e3)}var Sne,Ia,DYe,IYe,Bne=F(()=>{"use strict";mE();s(lN,"localDate");s(cN,"utcDate");s(Gb,"newDate");s(uN,"formatLocale");Sne={"-":"",_:" ",0:"0"},Ia=/^\s*\d+/,DYe=/^%/,IYe=/[\\^$*+?|[\]().{}]/g;s(bn,"pad");s(MYe,"requote");s(zb,"formatRe");s(Vb,"formatLookup");s(NYe,"parseWeekdayNumberSunday");s(PYe,"parseWeekdayNumberMonday");s(OYe,"parseWeekNumberSunday");s(BYe,"parseWeekNumberISO");s($Ye,"parseWeekNumberMonday");s(wne,"parseFullYear");s(kne,"parseYear");s(FYe,"parseZone");s(GYe,"parseQuarter");s(zYe,"parseMonthNumber");s(Ene,"parseDayOfMonth");s(VYe,"parseDayOfYear");s(Ane,"parseHour24");s(WYe,"parseMinutes");s(qYe,"parseSeconds");s(HYe,"parseMilliseconds");s(UYe,"parseMicroseconds");s(YYe,"parseLiteralPercent");s(jYe,"parseUnixTimestamp");s(XYe,"parseUnixTimestampSeconds");s(Rne,"formatDayOfMonth");s(KYe,"formatHour24");s(ZYe,"formatHour12");s(QYe,"formatDayOfYear");s(Mne,"formatMilliseconds");s(JYe,"formatMicroseconds");s(eje,"formatMonthNumber");s(tje,"formatMinutes");s(rje,"formatSeconds");s(nje,"formatWeekdayNumberMonday");s(ije,"formatWeekNumberSunday");s(Nne,"dISO");s(aje,"formatWeekNumberISO");s(sje,"formatWeekdayNumberSunday");s(oje,"formatWeekNumberMonday");s(lje,"formatYear");s(cje,"formatYearISO");s(uje,"formatFullYear");s(hje,"formatFullYearISO");s(dje,"formatZone");s(_ne,"formatUTCDayOfMonth");s(fje,"formatUTCHour24");s(pje,"formatUTCHour12");s(mje,"formatUTCDayOfYear");s(Pne,"formatUTCMilliseconds");s(gje,"formatUTCMicroseconds");s(yje,"formatUTCMonthNumber");s(vje,"formatUTCMinutes");s(xje,"formatUTCSeconds");s(bje,"formatUTCWeekdayNumberMonday");s(Tje,"formatUTCWeekNumberSunday");s(One,"UTCdISO");s(Cje,"formatUTCWeekNumberISO");s(Sje,"formatUTCWeekdayNumberSunday");s(wje,"formatUTCWeekNumberMonday");s(kje,"formatUTCYear");s(Eje,"formatUTCYearISO");s(Aje,"formatUTCFullYear");s(Rje,"formatUTCFullYearISO");s(_je,"formatUTCZone");s(Lne,"formatLiteralPercent");s(Dne,"formatUnixTimestamp");s(Ine,"formatUnixTimestampSeconds")});function hN(e){return Cy=uN(e),pm=Cy.format,$ne=Cy.parse,Fne=Cy.utcFormat,Gne=Cy.utcParse,Cy}var Cy,pm,$ne,Fne,Gne,zne=F(()=>{"use strict";Bne();hN({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});s(hN,"defaultLocale")});var dN=F(()=>{"use strict";zne()});function Lje(e){return new Date(e)}function Dje(e){return e instanceof Date?+e:+new Date(+e)}function Vne(e,t,r,n,i,a,o,l,u,h){var d=Bb(),f=d.invert,p=d.domain,m=h(".%L"),g=h(":%S"),y=h("%I:%M"),v=h("%I %p"),x=h("%a %d"),b=h("%b %d"),T=h("%B"),w=h("%Y");function C(S){return(u(S){"use strict";mE();dN();YM();Ob();une();s(Lje,"date");s(Dje,"number");s(Vne,"calendar");s(gE,"time")});var qne=F(()=>{"use strict";ine();cne();WM();Wne()});function fN(e){for(var t=e.length/6|0,r=new Array(t),n=0;n{"use strict";s(fN,"default")});var pN,Une=F(()=>{"use strict";Hne();pN=fN("4e79a7f28e2ce1575976b7b259a14fedc949af7aa1ff9da79c755fbab0ab")});var Yne=F(()=>{"use strict";Une()});function oi(e){return s(function(){return e},"constant")}var yE=F(()=>{"use strict";s(oi,"default")});function Xne(e){return e>1?0:e<-1?Sy:Math.acos(e)}function gN(e){return e>=1?Wb:e<=-1?-Wb:Math.asin(e)}var mN,Za,Yd,jne,vE,rc,mm,Ma,Sy,Wb,wy,xE=F(()=>{"use strict";mN=Math.abs,Za=Math.atan2,Yd=Math.cos,jne=Math.max,vE=Math.min,rc=Math.sin,mm=Math.sqrt,Ma=1e-12,Sy=Math.PI,Wb=Sy/2,wy=2*Sy;s(Xne,"acos");s(gN,"asin")});function bE(e){let t=3;return e.digits=function(r){if(!arguments.length)return t;if(r==null)t=null;else{let n=Math.floor(r);if(!(n>=0))throw new RangeError(`invalid digits: ${r}`);t=n}return e},()=>new sm(t)}var yN=F(()=>{"use strict";fM();s(bE,"withPath")});function Ije(e){return e.innerRadius}function Mje(e){return e.outerRadius}function Nje(e){return e.startAngle}function Pje(e){return e.endAngle}function Oje(e){return e&&e.padAngle}function Bje(e,t,r,n,i,a,o,l){var u=r-e,h=n-t,d=o-i,f=l-a,p=f*u-d*h;if(!(p*pI*I+D*D&&(R=P,M=N),{cx:R,cy:M,x01:-d,y01:-f,x11:R*(i/C-1),y11:M*(i/C-1)}}function nc(){var e=Ije,t=Mje,r=oi(0),n=null,i=Nje,a=Pje,o=Oje,l=null,u=bE(h);function h(){var d,f,p=+e.apply(this,arguments),m=+t.apply(this,arguments),g=i.apply(this,arguments)-Wb,y=a.apply(this,arguments)-Wb,v=mN(y-g),x=y>g;if(l||(l=d=u()),mMa))l.moveTo(0,0);else if(v>wy-Ma)l.moveTo(m*Yd(g),m*rc(g)),l.arc(0,0,m,g,y,!x),p>Ma&&(l.moveTo(p*Yd(y),p*rc(y)),l.arc(0,0,p,y,g,x));else{var b=g,T=y,w=g,C=y,S=v,k=v,R=o.apply(this,arguments)/2,M=R>Ma&&(n?+n.apply(this,arguments):mm(p*p+m*m)),P=vE(mN(m-p)/2,+r.apply(this,arguments)),N=P,_=P,A,I;if(M>Ma){var D=gN(M/p*rc(R)),L=gN(M/m*rc(R));(S-=D*2)>Ma?(D*=x?1:-1,w+=D,C-=D):(S=0,w=C=(g+y)/2),(k-=L*2)>Ma?(L*=x?1:-1,b+=L,T-=L):(k=0,b=T=(g+y)/2)}var B=m*Yd(b),O=m*rc(b),$=p*Yd(C),W=p*rc(C);if(P>Ma){var V=m*Yd(T),z=m*rc(T),H=p*Yd(w),K=p*rc(w),J;if(vMa?_>Ma?(A=TE(H,K,B,O,m,_,x),I=TE(V,z,$,W,m,_,x),l.moveTo(A.cx+A.x01,A.cy+A.y01),_Ma)||!(S>Ma)?l.lineTo($,W):N>Ma?(A=TE($,W,V,z,p,-N,x),I=TE(B,O,H,K,p,-N,x),l.lineTo(A.cx+A.x01,A.cy+A.y01),N{"use strict";yE();xE();yN();s(Ije,"arcInnerRadius");s(Mje,"arcOuterRadius");s(Nje,"arcStartAngle");s(Pje,"arcEndAngle");s(Oje,"arcPadAngle");s(Bje,"intersect");s(TE,"cornerTangents");s(nc,"default")});function qb(e){return typeof e=="object"&&"length"in e?e:Array.from(e)}var EZt,vN=F(()=>{"use strict";EZt=Array.prototype.slice;s(qb,"default")});function Zne(e){this._context=e}function Zc(e){return new Zne(e)}var xN=F(()=>{"use strict";s(Zne,"Linear");Zne.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._point=0},"lineStart"),lineEnd:s(function(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},"lineEnd"),point:s(function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:this._context.lineTo(e,t);break}},"point")};s(Zc,"default")});function Qne(e){return e[0]}function Jne(e){return e[1]}var eie=F(()=>{"use strict";s(Qne,"x");s(Jne,"y")});function xh(e,t){var r=oi(!0),n=null,i=Zc,a=null,o=bE(l);e=typeof e=="function"?e:e===void 0?Qne:oi(e),t=typeof t=="function"?t:t===void 0?Jne:oi(t);function l(u){var h,d=(u=qb(u)).length,f,p=!1,m;for(n==null&&(a=i(m=o())),h=0;h<=d;++h)!(h{"use strict";vN();yE();xN();yN();eie();s(xh,"default")});function bN(e,t){return te?1:t>=e?0:NaN}var rie=F(()=>{"use strict";s(bN,"default")});function TN(e){return e}var nie=F(()=>{"use strict";s(TN,"default")});function CE(){var e=TN,t=bN,r=null,n=oi(0),i=oi(wy),a=oi(0);function o(l){var u,h=(l=qb(l)).length,d,f,p=0,m=new Array(h),g=new Array(h),y=+n.apply(this,arguments),v=Math.min(wy,Math.max(-wy,i.apply(this,arguments)-y)),x,b=Math.min(Math.abs(v)/h,a.apply(this,arguments)),T=b*(v<0?-1:1),w;for(u=0;u0&&(p+=w);for(t!=null?m.sort(function(C,S){return t(g[C],g[S])}):r!=null&&m.sort(function(C,S){return r(l[C],l[S])}),u=0,f=p?(v-h*T)/p:0;u0?w*f:0)+T,g[d]={data:l[d],index:u,value:w,startAngle:y,endAngle:x,padAngle:b};return g}return s(o,"pie"),o.value=function(l){return arguments.length?(e=typeof l=="function"?l:oi(+l),o):e},o.sortValues=function(l){return arguments.length?(t=l,r=null,o):t},o.sort=function(l){return arguments.length?(r=l,t=null,o):r},o.startAngle=function(l){return arguments.length?(n=typeof l=="function"?l:oi(+l),o):n},o.endAngle=function(l){return arguments.length?(i=typeof l=="function"?l:oi(+l),o):i},o.padAngle=function(l){return arguments.length?(a=typeof l=="function"?l:oi(+l),o):a},o}var iie=F(()=>{"use strict";vN();yE();rie();nie();xE();s(CE,"default")});function Hb(e){return new SE(e,!0)}function Ub(e){return new SE(e,!1)}var SE,aie=F(()=>{"use strict";SE=class{static{s(this,"Bump")}constructor(t,r){this._context=t,this._x=r}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line}point(t,r){switch(t=+t,r=+r,this._point){case 0:{this._point=1,this._line?this._context.lineTo(t,r):this._context.moveTo(t,r);break}case 1:this._point=2;default:{this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,r,t,r):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+r)/2,t,this._y0,t,r);break}}this._x0=t,this._y0=r}};s(Hb,"bumpX");s(Ub,"bumpY")});function Wo(){}var Yb=F(()=>{"use strict";s(Wo,"default")});function ky(e,t,r){e._context.bezierCurveTo((2*e._x0+e._x1)/3,(2*e._y0+e._y1)/3,(e._x0+2*e._x1)/3,(e._y0+2*e._y1)/3,(e._x0+4*e._x1+t)/6,(e._y0+4*e._y1+r)/6)}function jb(e){this._context=e}function bh(e){return new jb(e)}var Xb=F(()=>{"use strict";s(ky,"point");s(jb,"Basis");jb.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},"lineStart"),lineEnd:s(function(){switch(this._point){case 3:ky(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1);break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},"lineEnd"),point:s(function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:ky(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t},"point")};s(bh,"default")});function sie(e){this._context=e}function wE(e){return new sie(e)}var oie=F(()=>{"use strict";Yb();Xb();s(sie,"BasisClosed");sie.prototype={areaStart:Wo,areaEnd:Wo,lineStart:s(function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},"lineStart"),lineEnd:s(function(){switch(this._point){case 1:{this._context.moveTo(this._x2,this._y2),this._context.closePath();break}case 2:{this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break}case 3:{this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4);break}}},"lineEnd"),point:s(function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._x2=e,this._y2=t;break;case 1:this._point=2,this._x3=e,this._y3=t;break;case 2:this._point=3,this._x4=e,this._y4=t,this._context.moveTo((this._x0+4*this._x1+e)/6,(this._y0+4*this._y1+t)/6);break;default:ky(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t},"point")};s(wE,"default")});function lie(e){this._context=e}function kE(e){return new lie(e)}var cie=F(()=>{"use strict";Xb();s(lie,"BasisOpen");lie.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},"lineStart"),lineEnd:s(function(){(this._line||this._line!==0&&this._point===3)&&this._context.closePath(),this._line=1-this._line},"lineEnd"),point:s(function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var r=(this._x0+4*this._x1+e)/6,n=(this._y0+4*this._y1+t)/6;this._line?this._context.lineTo(r,n):this._context.moveTo(r,n);break;case 3:this._point=4;default:ky(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t},"point")};s(kE,"default")});function uie(e,t){this._basis=new jb(e),this._beta=t}var CN,hie=F(()=>{"use strict";Xb();s(uie,"Bundle");uie.prototype={lineStart:s(function(){this._x=[],this._y=[],this._basis.lineStart()},"lineStart"),lineEnd:s(function(){var e=this._x,t=this._y,r=e.length-1;if(r>0)for(var n=e[0],i=t[0],a=e[r]-n,o=t[r]-i,l=-1,u;++l<=r;)u=l/r,this._basis.point(this._beta*e[l]+(1-this._beta)*(n+u*a),this._beta*t[l]+(1-this._beta)*(i+u*o));this._x=this._y=null,this._basis.lineEnd()},"lineEnd"),point:s(function(e,t){this._x.push(+e),this._y.push(+t)},"point")};CN=s((function e(t){function r(n){return t===1?new jb(n):new uie(n,t)}return s(r,"bundle"),r.beta=function(n){return e(+n)},r}),"custom")(.85)});function Ey(e,t,r){e._context.bezierCurveTo(e._x1+e._k*(e._x2-e._x0),e._y1+e._k*(e._y2-e._y0),e._x2+e._k*(e._x1-t),e._y2+e._k*(e._y1-r),e._x2,e._y2)}function EE(e,t){this._context=e,this._k=(1-t)/6}var Kb,Zb=F(()=>{"use strict";s(Ey,"point");s(EE,"Cardinal");EE.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},"lineStart"),lineEnd:s(function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Ey(this,this._x1,this._y1);break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},"lineEnd"),point:s(function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2,this._x1=e,this._y1=t;break;case 2:this._point=3;default:Ey(this,e,t);break}this._x0=this._x1,this._x1=this._x2,this._x2=e,this._y0=this._y1,this._y1=this._y2,this._y2=t},"point")};Kb=s((function e(t){function r(n){return new EE(n,t)}return s(r,"cardinal"),r.tension=function(n){return e(+n)},r}),"custom")(0)});function AE(e,t){this._context=e,this._k=(1-t)/6}var SN,wN=F(()=>{"use strict";Yb();Zb();s(AE,"CardinalClosed");AE.prototype={areaStart:Wo,areaEnd:Wo,lineStart:s(function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},"lineStart"),lineEnd:s(function(){switch(this._point){case 1:{this._context.moveTo(this._x3,this._y3),this._context.closePath();break}case 2:{this._context.lineTo(this._x3,this._y3),this._context.closePath();break}case 3:{this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5);break}}},"lineEnd"),point:s(function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._x3=e,this._y3=t;break;case 1:this._point=2,this._context.moveTo(this._x4=e,this._y4=t);break;case 2:this._point=3,this._x5=e,this._y5=t;break;default:Ey(this,e,t);break}this._x0=this._x1,this._x1=this._x2,this._x2=e,this._y0=this._y1,this._y1=this._y2,this._y2=t},"point")};SN=s((function e(t){function r(n){return new AE(n,t)}return s(r,"cardinal"),r.tension=function(n){return e(+n)},r}),"custom")(0)});function RE(e,t){this._context=e,this._k=(1-t)/6}var kN,EN=F(()=>{"use strict";Zb();s(RE,"CardinalOpen");RE.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},"lineStart"),lineEnd:s(function(){(this._line||this._line!==0&&this._point===3)&&this._context.closePath(),this._line=1-this._line},"lineEnd"),point:s(function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Ey(this,e,t);break}this._x0=this._x1,this._x1=this._x2,this._x2=e,this._y0=this._y1,this._y1=this._y2,this._y2=t},"point")};kN=s((function e(t){function r(n){return new RE(n,t)}return s(r,"cardinal"),r.tension=function(n){return e(+n)},r}),"custom")(0)});function Qb(e,t,r){var n=e._x1,i=e._y1,a=e._x2,o=e._y2;if(e._l01_a>Ma){var l=2*e._l01_2a+3*e._l01_a*e._l12_a+e._l12_2a,u=3*e._l01_a*(e._l01_a+e._l12_a);n=(n*l-e._x0*e._l12_2a+e._x2*e._l01_2a)/u,i=(i*l-e._y0*e._l12_2a+e._y2*e._l01_2a)/u}if(e._l23_a>Ma){var h=2*e._l23_2a+3*e._l23_a*e._l12_a+e._l12_2a,d=3*e._l23_a*(e._l23_a+e._l12_a);a=(a*h+e._x1*e._l23_2a-t*e._l12_2a)/d,o=(o*h+e._y1*e._l23_2a-r*e._l12_2a)/d}e._context.bezierCurveTo(n,i,a,o,e._x2,e._y2)}function die(e,t){this._context=e,this._alpha=t}var Jb,_E=F(()=>{"use strict";xE();Zb();s(Qb,"point");s(die,"CatmullRom");die.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},"lineStart"),lineEnd:s(function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2);break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},"lineEnd"),point:s(function(e,t){if(e=+e,t=+t,this._point){var r=this._x2-e,n=this._y2-t;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(r*r+n*n,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3;default:Qb(this,e,t);break}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=e,this._y0=this._y1,this._y1=this._y2,this._y2=t},"point")};Jb=s((function e(t){function r(n){return t?new die(n,t):new EE(n,0)}return s(r,"catmullRom"),r.alpha=function(n){return e(+n)},r}),"custom")(.5)});function fie(e,t){this._context=e,this._alpha=t}var AN,pie=F(()=>{"use strict";wN();Yb();_E();s(fie,"CatmullRomClosed");fie.prototype={areaStart:Wo,areaEnd:Wo,lineStart:s(function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},"lineStart"),lineEnd:s(function(){switch(this._point){case 1:{this._context.moveTo(this._x3,this._y3),this._context.closePath();break}case 2:{this._context.lineTo(this._x3,this._y3),this._context.closePath();break}case 3:{this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5);break}}},"lineEnd"),point:s(function(e,t){if(e=+e,t=+t,this._point){var r=this._x2-e,n=this._y2-t;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(r*r+n*n,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=e,this._y3=t;break;case 1:this._point=2,this._context.moveTo(this._x4=e,this._y4=t);break;case 2:this._point=3,this._x5=e,this._y5=t;break;default:Qb(this,e,t);break}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=e,this._y0=this._y1,this._y1=this._y2,this._y2=t},"point")};AN=s((function e(t){function r(n){return t?new fie(n,t):new AE(n,0)}return s(r,"catmullRom"),r.alpha=function(n){return e(+n)},r}),"custom")(.5)});function mie(e,t){this._context=e,this._alpha=t}var RN,gie=F(()=>{"use strict";EN();_E();s(mie,"CatmullRomOpen");mie.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},"lineStart"),lineEnd:s(function(){(this._line||this._line!==0&&this._point===3)&&this._context.closePath(),this._line=1-this._line},"lineEnd"),point:s(function(e,t){if(e=+e,t=+t,this._point){var r=this._x2-e,n=this._y2-t;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(r*r+n*n,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Qb(this,e,t);break}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=e,this._y0=this._y1,this._y1=this._y2,this._y2=t},"point")};RN=s((function e(t){function r(n){return t?new mie(n,t):new RE(n,0)}return s(r,"catmullRom"),r.alpha=function(n){return e(+n)},r}),"custom")(.5)});function yie(e){this._context=e}function LE(e){return new yie(e)}var vie=F(()=>{"use strict";Yb();s(yie,"LinearClosed");yie.prototype={areaStart:Wo,areaEnd:Wo,lineStart:s(function(){this._point=0},"lineStart"),lineEnd:s(function(){this._point&&this._context.closePath()},"lineEnd"),point:s(function(e,t){e=+e,t=+t,this._point?this._context.lineTo(e,t):(this._point=1,this._context.moveTo(e,t))},"point")};s(LE,"default")});function xie(e){return e<0?-1:1}function bie(e,t,r){var n=e._x1-e._x0,i=t-e._x1,a=(e._y1-e._y0)/(n||i<0&&-0),o=(r-e._y1)/(i||n<0&&-0),l=(a*i+o*n)/(n+i);return(xie(a)+xie(o))*Math.min(Math.abs(a),Math.abs(o),.5*Math.abs(l))||0}function Tie(e,t){var r=e._x1-e._x0;return r?(3*(e._y1-e._y0)/r-t)/2:t}function _N(e,t,r){var n=e._x0,i=e._y0,a=e._x1,o=e._y1,l=(a-n)/3;e._context.bezierCurveTo(n+l,i+l*t,a-l,o-l*r,a,o)}function DE(e){this._context=e}function Cie(e){this._context=new Sie(e)}function Sie(e){this._context=e}function e2(e){return new DE(e)}function t2(e){return new Cie(e)}var wie=F(()=>{"use strict";s(xie,"sign");s(bie,"slope3");s(Tie,"slope2");s(_N,"point");s(DE,"MonotoneX");DE.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},"lineStart"),lineEnd:s(function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:_N(this,this._t0,Tie(this,this._t0));break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},"lineEnd"),point:s(function(e,t){var r=NaN;if(e=+e,t=+t,!(e===this._x1&&t===this._y1)){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,_N(this,Tie(this,r=bie(this,e,t)),r);break;default:_N(this,this._t0,r=bie(this,e,t));break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t,this._t0=r}},"point")};s(Cie,"MonotoneY");(Cie.prototype=Object.create(DE.prototype)).point=function(e,t){DE.prototype.point.call(this,t,e)};s(Sie,"ReflectContext");Sie.prototype={moveTo:s(function(e,t){this._context.moveTo(t,e)},"moveTo"),closePath:s(function(){this._context.closePath()},"closePath"),lineTo:s(function(e,t){this._context.lineTo(t,e)},"lineTo"),bezierCurveTo:s(function(e,t,r,n,i,a){this._context.bezierCurveTo(t,e,n,r,a,i)},"bezierCurveTo")};s(e2,"monotoneX");s(t2,"monotoneY")});function Eie(e){this._context=e}function kie(e){var t,r=e.length-1,n,i=new Array(r),a=new Array(r),o=new Array(r);for(i[0]=0,a[0]=2,o[0]=e[0]+2*e[1],t=1;t=0;--t)i[t]=(o[t]-i[t+1])/a[t];for(a[r-1]=(e[r]+i[r-1])/2,t=0;t{"use strict";s(Eie,"Natural");Eie.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._x=[],this._y=[]},"lineStart"),lineEnd:s(function(){var e=this._x,t=this._y,r=e.length;if(r)if(this._line?this._context.lineTo(e[0],t[0]):this._context.moveTo(e[0],t[0]),r===2)this._context.lineTo(e[1],t[1]);else for(var n=kie(e),i=kie(t),a=0,o=1;o{"use strict";s(IE,"Step");IE.prototype={areaStart:s(function(){this._line=0},"areaStart"),areaEnd:s(function(){this._line=NaN},"areaEnd"),lineStart:s(function(){this._x=this._y=NaN,this._point=0},"lineStart"),lineEnd:s(function(){0=0&&(this._t=1-this._t,this._line=1-this._line)},"lineEnd"),point:s(function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:{if(this._t<=0)this._context.lineTo(this._x,t),this._context.lineTo(e,t);else{var r=this._x*(1-this._t)+e*this._t;this._context.lineTo(r,this._y),this._context.lineTo(r,t)}break}}this._x=e,this._y=t},"point")};s(Ry,"default");s(r2,"stepBefore");s(n2,"stepAfter")});var _ie=F(()=>{"use strict";Kne();tie();iie();oie();cie();Xb();aie();hie();wN();EN();Zb();pie();gie();_E();vie();xN();wie();Aie();Rie()});var Lie=F(()=>{"use strict"});var Die=F(()=>{"use strict"});function jd(e,t,r){this.k=e,this.x=t,this.y=r}function DN(e){for(;!e.__zoom;)if(!(e=e.parentNode))return LN;return e.__zoom}var LN,IN=F(()=>{"use strict";s(jd,"Transform");jd.prototype={constructor:jd,scale:s(function(e){return e===1?this:new jd(this.k*e,this.x,this.y)},"scale"),translate:s(function(e,t){return e===0&t===0?this:new jd(this.k,this.x+this.k*e,this.y+this.k*t)},"translate"),apply:s(function(e){return[e[0]*this.k+this.x,e[1]*this.k+this.y]},"apply"),applyX:s(function(e){return e*this.k+this.x},"applyX"),applyY:s(function(e){return e*this.k+this.y},"applyY"),invert:s(function(e){return[(e[0]-this.x)/this.k,(e[1]-this.y)/this.k]},"invert"),invertX:s(function(e){return(e-this.x)/this.k},"invertX"),invertY:s(function(e){return(e-this.y)/this.k},"invertY"),rescaleX:s(function(e){return e.copy().domain(e.range().map(this.invertX,this).map(e.invert,e))},"rescaleX"),rescaleY:s(function(e){return e.copy().domain(e.range().map(this.invertY,this).map(e.invert,e))},"rescaleY"),toString:s(function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"},"toString")};LN=new jd(1,0,0);DN.prototype=jd.prototype;s(DN,"transform")});var Iie=F(()=>{"use strict"});var Mie=F(()=>{"use strict";Kk();Lie();Die();IN();Iie()});var Nie=F(()=>{"use strict";Mie();IN()});var Wr=F(()=>{"use strict";Bd();VJ();ore();hre();hy();dre();fre();bI();Mee();pre();lM();mre();yre();kM();Dre();tne();py();fM();rne();gre();nne();qne();Yne();Xl();_ie();mE();dN();qk();Kk();Nie()});var MN,gm,ME,Pie,NE,PE,Os,i2,_y,Xd=F(()=>{"use strict";MN=Rs(ub(),1);Wr();Yr();gm=s((e,t)=>{let r=e.append("rect");if(r.attr("x",t.x),r.attr("y",t.y),r.attr("fill",t.fill),r.attr("stroke",t.stroke),r.attr("width",t.width),r.attr("height",t.height),t.name&&r.attr("name",t.name),t.rx&&r.attr("rx",t.rx),t.ry&&r.attr("ry",t.ry),t.attrs!==void 0)for(let n in t.attrs)r.attr(n,t.attrs[n]);return t.class&&r.attr("class",t.class),r},"drawRect"),ME=s((e,t)=>{let r={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};gm(e,r).lower()},"drawBackgroundRect"),Pie=s((e,t)=>{let r=t.text.replace(Up," "),n=e.append("text");n.attr("x",t.x),n.attr("y",t.y),n.attr("class","legend"),n.style("text-anchor",t.anchor),t.class&&n.attr("class",t.class);let i=n.append("tspan");return i.attr("x",t.x+t.textMargin*2),i.text(r),n},"drawText"),NE=s((e,t,r,n)=>{let i=e.append("image");i.attr("x",t),i.attr("y",r);let a=(0,MN.sanitizeUrl)(n);i.attr("xlink:href",a)},"drawImage"),PE=s((e,t,r,n)=>{let i=e.append("use");i.attr("x",t),i.attr("y",r);let a=(0,MN.sanitizeUrl)(n);i.attr("xlink:href",`#${a}`)},"drawEmbeddedImage"),Os=s(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),i2=s(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj"),_y=s(()=>{let e=ut(".mermaidTooltip");return e.empty()&&(e=ut("body").append("div").attr("class","mermaidTooltip").style("opacity",0).style("position","absolute").style("text-align","center").style("max-width","200px").style("padding","2px").style("font-size","12px").style("background","#ffffde").style("border","1px solid #333").style("border-radius","2px").style("pointer-events","none").style("z-index","100")),e},"createTooltip")});var Oie,$je,Fje,Gje,zje,Vje,Wje,qje,Hje,Uje,a2,Qc,Bie=F(()=>{"use strict";Yr();Xd();Oie=s(function(e,t){return gm(e,t)},"drawRect"),$je=s((e,t,r,n)=>{let i=e.append("g"),a=0;for(let o of t){let l=o.textColor?o.textColor:"#444444",u=o.lineColor?o.lineColor:"#444444",h=o.offsetX?parseInt(String(o.offsetX)):0,d=o.offsetY?parseInt(String(o.offsetY)):0,f="";if(a===0){let g=i.append("line");g.attr("x1",o.startPoint.x),g.attr("y1",o.startPoint.y),g.attr("x2",o.endPoint.x),g.attr("y2",o.endPoint.y),g.attr("stroke-width","1"),g.attr("stroke",u),g.style("fill","none"),o.type!=="rel_b"&&g.attr("marker-end","url("+f+"#"+n+"-arrowhead)"),(o.type==="birel"||o.type==="rel_b")&&g.attr("marker-start","url("+f+"#"+n+"-arrowend)"),a=-1}else{let g=i.append("path");g.attr("fill","none").attr("stroke-width","1").attr("stroke",u).attr("d","Mstartx,starty Qcontrolx,controly stopx,stopy ".replaceAll("startx",o.startPoint.x).replaceAll("starty",o.startPoint.y).replaceAll("controlx",o.startPoint.x+(o.endPoint.x-o.startPoint.x)/2-(o.endPoint.x-o.startPoint.x)/4).replaceAll("controly",o.startPoint.y+(o.endPoint.y-o.startPoint.y)/2).replaceAll("stopx",o.endPoint.x).replaceAll("stopy",o.endPoint.y)),o.type!=="rel_b"&&g.attr("marker-end","url("+f+"#"+n+"-arrowhead)"),(o.type==="birel"||o.type==="rel_b")&&g.attr("marker-start","url("+f+"#"+n+"-arrowend)")}let p=o.label.width,m=r.messageFont();a2(r)(o.label.text,i,Math.min(o.startPoint.x,o.endPoint.x)+Math.abs(o.endPoint.x-o.startPoint.x)/2+h,Math.min(o.startPoint.y,o.endPoint.y)+Math.abs(o.endPoint.y-o.startPoint.y)/2+d,p,o.label.height,{fill:l},m),o.techn&&o.techn.text!==""&&(m=r.messageFont(),a2(r)("["+o.techn.text+"]",i,Math.min(o.startPoint.x,o.endPoint.x)+Math.abs(o.endPoint.x-o.startPoint.x)/2+h,Math.min(o.startPoint.y,o.endPoint.y)+Math.abs(o.endPoint.y-o.startPoint.y)/2+r.messageFontSize+5+d,Math.max(p,o.techn.width),o.techn.height,{fill:l,"font-style":"italic"},m))}},"drawRels"),Fje=s(function(e,t,r){let n=e.append("g"),i=t.bgColor?t.bgColor:"none",a=t.borderColor?t.borderColor:"#444444",o=t.fontColor?t.fontColor:"black",l={"stroke-width":1,"stroke-dasharray":"7.0,7.0"};t.nodeType&&(l={"stroke-width":1});let u={x:t.x,y:t.y,fill:i,stroke:a,width:t.width,height:t.height,rx:2.5,ry:2.5,attrs:l};Oie(n,u);let h=r.boundaryFont();h.fontWeight="bold",h.fontSize=h.fontSize+2,h.fontColor=o,a2(r)(t.label.text,n,t.x,t.y+t.label.Y,t.width,t.height,{fill:"#444444"},h),t.type&&t.type.text!==""&&(h=r.boundaryFont(),h.fontColor=o,a2(r)(t.type.text,n,t.x,t.y+t.type.Y,t.width,t.height,{fill:"#444444"},h)),t.descr&&t.descr.text!==""&&(h=r.boundaryFont(),h.fontSize=h.fontSize-2,h.fontColor=o,a2(r)(t.descr.text,n,t.x,t.y+t.descr.Y,t.width,t.height,{fill:"#444444"},h))},"drawBoundary"),Gje=s(function(e,t){e.append("defs").append("symbol").attr("id",t+"-database").attr("fill-rule","evenodd").attr("clip-rule","evenodd").append("path").attr("transform","scale(.5)").attr("d","M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z")},"insertDatabaseIcon"),zje=s(function(e,t){e.append("defs").append("symbol").attr("id",t+"-computer").attr("width","24").attr("height","24").append("path").attr("transform","scale(.5)").attr("d","M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z")},"insertComputerIcon"),Vje=s(function(e,t){e.append("defs").append("symbol").attr("id",t+"-clock").attr("width","24").attr("height","24").append("path").attr("transform","scale(.5)").attr("d","M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z")},"insertClockIcon"),Wje=s(function(e,t){e.append("defs").append("marker").attr("id",t+"-arrowhead").attr("refX",9).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z")},"insertArrowHead"),qje=s(function(e,t){e.append("defs").append("marker").attr("id",t+"-arrowend").attr("refX",1).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto").append("path").attr("d","M 10 0 L 0 5 L 10 10 z")},"insertArrowEnd"),Hje=s(function(e,t){e.append("defs").append("marker").attr("id",t+"-filled-head").attr("refX",18).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L14,7 L9,1 Z")},"insertArrowFilledHead"),Uje=s(function(e,t){let n=e.append("defs").append("marker").attr("id",t+"-crosshead").attr("markerWidth",15).attr("markerHeight",8).attr("orient","auto").attr("refX",16).attr("refY",4);n.append("path").attr("fill","black").attr("stroke","#000000").style("stroke-dasharray","0, 0").attr("stroke-width","1px").attr("d","M 9,2 V 6 L16,4 Z"),n.append("path").attr("fill","none").attr("stroke","#000000").style("stroke-dasharray","0, 0").attr("stroke-width","1px").attr("d","M 0,1 L 6,7 M 6,1 L 0,7")},"insertArrowCrossHead"),a2=(function(){function e(i,a,o,l,u,h,d){let f=a.append("text").attr("x",o+u/2).attr("y",l+h/2+5).style("text-anchor","middle").text(i);n(f,d)}s(e,"byText");function t(i,a,o,l,u,h,d,f){let{fontSize:p,fontFamily:m,fontWeight:g}=f,y=i.split(Tt.lineBreakRegex);for(let v=0;v=0}var Fie=F(()=>{"use strict";s($ie,"isLength")});function BE(e){return e!=null&&typeof e!="function"&&$ie(e.length)}var NN=F(()=>{"use strict";Fie();s(BE,"isArrayLike")});function Gie(e){return e==="__proto__"}var zie=F(()=>{"use strict";s(Gie,"isUnsafeProperty")});function Kd(e){return e==null||typeof e!="object"&&typeof e!="function"}var s2=F(()=>{"use strict";s(Kd,"isPrimitive")});function $E(e){return Object.getOwnPropertySymbols(e).filter(t=>Object.prototype.propertyIsEnumerable.call(e,t))}var PN=F(()=>{"use strict";s($E,"getSymbols")});function Th(e){return e==null?e===void 0?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}var o2=F(()=>{"use strict";s(Th,"getTag")});var l2,Zd,ym,vm,xm,c2,u2,h2,d2,FE,f2,Ly,p2,GE,zE,VE,WE,qE,HE,UE,YE,jE,XE=F(()=>{"use strict";l2="[object RegExp]",Zd="[object String]",ym="[object Number]",vm="[object Boolean]",xm="[object Arguments]",c2="[object Symbol]",u2="[object Date]",h2="[object Map]",d2="[object Set]",FE="[object Array]",f2="[object ArrayBuffer]",Ly="[object Object]",p2="[object DataView]",GE="[object Uint8Array]",zE="[object Uint8ClampedArray]",VE="[object Uint16Array]",WE="[object Uint32Array]",qE="[object Int8Array]",HE="[object Int16Array]",UE="[object Int32Array]",YE="[object Float32Array]",jE="[object Float64Array]"});function Dy(e){return ArrayBuffer.isView(e)&&!(e instanceof DataView)}var KE=F(()=>{"use strict";s(Dy,"isTypedArray")});function Vie(e,t){return Iy(e,void 0,e,new Map,t)}function Iy(e,t,r,n=new Map,i=void 0){let a=i?.(e,t,r,n);if(a!==void 0)return a;if(Kd(e))return e;if(n.has(e))return n.get(e);if(Array.isArray(e)){let o=new Array(e.length);n.set(e,o);for(let l=0;l{"use strict";PN();o2();XE();s2();KE();s(Vie,"cloneDeepWith");s(Iy,"cloneDeepWithImpl");s(yl,"copyProperties");s(Yje,"isCloneableObject")});function qie(e,t){return Vie(e,(r,n,i,a)=>{let o=t?.(r,n,i,a);if(o!==void 0)return o;if(typeof e=="object"){if(Th(e)===Ly&&typeof e.constructor!="function"){let l={};return a.set(e,l),yl(l,e,i,a),l}switch(Object.prototype.toString.call(e)){case ym:case Zd:case vm:{let l=new e.constructor(e?.valueOf());return yl(l,e),l}case xm:{let l={};return yl(l,e),l.length=e.length,l[Symbol.iterator]=e[Symbol.iterator],l}default:return}}})}var Hie=F(()=>{"use strict";Wie();o2();XE();s(qie,"cloneDeepWith")});function ON(e){return qie(e)}var Uie=F(()=>{"use strict";Hie();s(ON,"cloneDeep")});function m2(e){return e!==null&&typeof e=="object"&&Th(e)==="[object Arguments]"}var BN=F(()=>{"use strict";o2();s(m2,"isArguments")});function g2(e){return typeof e=="object"&&e!==null}var $N=F(()=>{"use strict";s(g2,"isObjectLike")});function Yie(e){return g2(e)&&BE(e)}var jie=F(()=>{"use strict";NN();$N();s(Yie,"isArrayLikeObject")});function Xie(e){return Array.isArray(e)}var Kie=F(()=>{"use strict";s(Xie,"isArray")});function bm(e,t){if(typeof e!="function"||t!=null&&typeof t!="function")throw new TypeError("Expected a function");let r=s(function(...i){let a=t?t.apply(this,i):i[0],o=r.cache;if(o.has(a))return o.get(a);let l=e.apply(this,i);return r.cache=o.set(a,l)||o,l},"memoized"),n=bm.Cache||Map;return r.cache=new n,r}var Zie=F(()=>{"use strict";s(bm,"memoize");bm.Cache=Map});function Qie(){}var Jie=F(()=>{"use strict";s(Qie,"noop")});function eae(e){let t=e?.constructor,r=typeof t=="function"?t.prototype:Object.prototype;return e===r}var tae=F(()=>{"use strict";s(eae,"isPrototype")});function Qd(e){return Dy(e)}var ZE=F(()=>{"use strict";KE();s(Qd,"isTypedArray")});function GN(e){if(Kd(e))return e;let t=Th(e);if(!jje(e))return{};if(Xie(e)){let n=Array.from(e);return e.length>0&&typeof e[0]=="string"&&Object.hasOwn(e,"index")&&(n.index=e.index,n.input=e.input),n}if(Qd(e)){let n=e,i=n.constructor;return new i(n.buffer,n.byteOffset,n.length)}if(t===f2)return new ArrayBuffer(e.byteLength);if(t===p2){let n=e,i=n.buffer,a=n.byteOffset,o=n.byteLength,l=new ArrayBuffer(o),u=new Uint8Array(i,a,o);return new Uint8Array(l).set(u),new DataView(l)}if(t===vm||t===ym||t===Zd){let n=e.constructor,i=new n(e.valueOf());return t===Zd?Kje(i,e):FN(i,e),i}if(t===u2)return new Date(Number(e));if(t===l2){let n=e,i=new RegExp(n.source,n.flags);return i.lastIndex=n.lastIndex,i}if(t===c2)return Object(Symbol.prototype.valueOf.call(e));if(t===h2){let n=e,i=new Map;return n.forEach((a,o)=>{i.set(o,a)}),i}if(t===d2){let n=e,i=new Set;return n.forEach(a=>{i.add(a)}),i}if(t===xm){let n=e,i={};return FN(i,n),i.length=n.length,i[Symbol.iterator]=n[Symbol.iterator],i}let r={};return Zje(r,e),FN(r,e),Xje(r,e),r}function jje(e){switch(Th(e)){case xm:case FE:case f2:case p2:case vm:case u2:case YE:case jE:case qE:case HE:case UE:case h2:case ym:case Ly:case l2:case d2:case Zd:case c2:case GE:case zE:case VE:case WE:return!0;default:return!1}}function FN(e,t){for(let r in t)Object.hasOwn(t,r)&&(e[r]=t[r])}function Xje(e,t){let r=Object.getOwnPropertySymbols(t);for(let n=0;n=r)&&(e[n]=t[n])}function Zje(e,t){let r=Object.getPrototypeOf(t);r!==null&&typeof t.constructor=="function"&&Object.setPrototypeOf(e,r)}var rae=F(()=>{"use strict";s2();o2();XE();Kie();ZE();s(GN,"clone");s(jje,"isCloneableObject");s(FN,"copyOwnProperties");s(Xje,"copySymbolProperties");s(Kje,"cloneStringObjectProperties");s(Zje,"copyPrototype")});function QE(e){if(typeof e!="object"||e==null)return!1;if(Object.getPrototypeOf(e)===null)return!0;if(Object.prototype.toString.call(e)!=="[object Object]"){let r=e[Symbol.toStringTag];return r==null||!Object.getOwnPropertyDescriptor(e,Symbol.toStringTag)?.writable?!1:e.toString()===`[object ${r}]`}let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t}var nae=F(()=>{"use strict";s(QE,"isPlainObject")});function iae(e){if(Kd(e))return e;if(Array.isArray(e)||Dy(e)||e instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&e instanceof SharedArrayBuffer)return e.slice(0);let t=Object.getPrototypeOf(e);if(t==null)return Object.assign(Object.create(t),e);let r=t.constructor;if(e instanceof Date||e instanceof Map||e instanceof Set)return new r(e);if(e instanceof RegExp){let n=new r(e);return n.lastIndex=e.lastIndex,n}if(e instanceof DataView)return new r(e.buffer.slice(0));if(e instanceof Error){let n;return e instanceof AggregateError?n=new r(e.errors,e.message,{cause:e.cause}):n=new r(e.message,{cause:e.cause}),n.stack=e.stack,Object.assign(n,e),n}if(typeof File<"u"&&e instanceof File)return new r([e],e.name,{type:e.type,lastModified:e.lastModified});if(typeof e=="object"){let n=Object.create(t);return Object.assign(n,e)}return e}var aae=F(()=>{"use strict";s2();KE();s(iae,"clone")});function sae(e,...t){let r=t.slice(0,-1),n=t[t.length-1],i=e;for(let a=0;a{"use strict";Uie();zie();aae();s2();PN();BN();jie();$N();nae();ZE();s(sae,"mergeWith");s(JE,"mergeWithDeep")});function zN(e,...t){return sae(e,...t,Qie)}var lae=F(()=>{"use strict";oae();Jie();s(zN,"merge")});function e4(e){if(e==null)return!0;if(BE(e))return typeof e.splice!="function"&&typeof e!="string"&&(typeof Buffer>"u"||!Buffer.isBuffer(e))&&!Qd(e)&&!m2(e)?!1:e.length===0;if(typeof e=="object"){if(e instanceof Map||e instanceof Set)return e.size===0;let t=Object.keys(e);return eae(e)?t.filter(r=>r!=="constructor").length===0:t.length===0}return!0}var cae=F(()=>{"use strict";BN();NN();ZE();tae();s(e4,"isEmpty")});var t4=F(()=>{"use strict";Zie();rae();lae();cae()});function qN(e,t){if(!e)return t;let r=`curve${e.charAt(0).toUpperCase()+e.slice(1)}`;return Qje[r]??t}function rXe(e,t){let r=e.trim();if(r)return t.securityLevel!=="loose"?(0,dae.sanitizeUrl)(r):r}function mae(e,t){return!e||!t?0:Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))}function iXe(e){let t,r=0;e.forEach(i=>{r+=mae(i,t),t=i});let n=r/2;return HN(e,n)}function aXe(e){return e.length===1?e[0]:iXe(e)}function oXe(e,t,r){let n=structuredClone(r);se.info("our points",n),t!=="start_left"&&t!=="start_right"&&n.reverse();let i=25+e,a=HN(n,i),o=10+e*.5,l=Math.atan2(n[0].y-a.y,n[0].x-a.x),u={x:0,y:0};return t==="start_left"?(u.x=Math.sin(l+Math.PI)*o+(n[0].x+a.x)/2,u.y=-Math.cos(l+Math.PI)*o+(n[0].y+a.y)/2):t==="end_right"?(u.x=Math.sin(l-Math.PI)*o+(n[0].x+a.x)/2-5,u.y=-Math.cos(l-Math.PI)*o+(n[0].y+a.y)/2-5):t==="end_left"?(u.x=Math.sin(l)*o+(n[0].x+a.x)/2-5,u.y=-Math.cos(l)*o+(n[0].y+a.y)/2-5):(u.x=Math.sin(l)*o+(n[0].x+a.x)/2,u.y=-Math.cos(l)*o+(n[0].y+a.y)/2),u}function UN(e){let t="",r="";for(let n of e)n!==void 0&&(n.startsWith("color:")||n.startsWith("text-align:")?r=r+n+";":t=t+n+";");return{style:t,labelStyle:r}}function lXe(e){let t="",r="0123456789abcdef",n=r.length;for(let i=0;iMath.round(parseFloat(a)).toString());return i.includes(r.toString())||i.includes(n.toString())}var dae,WN,Qje,Jje,eXe,fae,pae,tXe,nXe,uae,HN,sXe,hae,YN,jN,cXe,uXe,Tm,hXe,y2,VN,r4,dXe,fXe,Bs,or,gae,lo,Jc,er=F(()=>{"use strict";dae=Rs(ub(),1);Wr();Yr();Sw();St();Xp();$0();t4();hk();WN="\u200B",Qje={curveBasis:bh,curveBasisClosed:wE,curveBasisOpen:kE,curveBumpX:Hb,curveBumpY:Ub,curveBundle:CN,curveCardinalClosed:SN,curveCardinalOpen:kN,curveCardinal:Kb,curveCatmullRomClosed:AN,curveCatmullRomOpen:RN,curveCatmullRom:Jb,curveLinear:Zc,curveLinearClosed:LE,curveMonotoneX:e2,curveMonotoneY:t2,curveNatural:Ay,curveStep:Ry,curveStepAfter:n2,curveStepBefore:r2},Jje=/\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi,eXe=s(function(e,t){let r=fae(e,/(?:init\b)|(?:initialize\b)/),n={};if(Array.isArray(r)){let o=r.map(l=>l.args);G0(o),n=ii(n,[...o])}else n=r.args;if(!n)return;let i=ry(e,t),a="config";return n[a]!==void 0&&(i==="flowchart-v2"&&(i="flowchart"),n[i]=n[a],delete n[a]),n},"detectInit"),fae=s(function(e,t=null){try{let r=new RegExp(`[%]{2}(?![{]${Jje.source})(?=[}][%]{2}).* +`,"ig");e=e.trim().replace(r,"").replace(/'/gm,'"'),se.debug(`Detecting diagram directive${t!==null?" type:"+t:""} based on the text:${e}`);let n,i=[];for(;(n=jp.exec(e))!==null;)if(n.index===jp.lastIndex&&jp.lastIndex++,n&&!t||t&&n[1]?.match(t)||t&&n[2]?.match(t)){let a=n[1]?n[1]:n[2],o=n[3]?n[3].trim():n[4]?JSON.parse(n[4].trim()):null;i.push({type:a,args:o})}return i.length===0?{type:e,args:null}:i.length===1?i[0]:i}catch(r){return se.error(`ERROR: ${r.message} - Unable to parse directive type: '${t}' based on the text: '${e}'`),{type:void 0,args:null}}},"detectDirective"),pae=s(function(e){return e.replace(jp,"")},"removeDirectives"),tXe=s(function(e,t){for(let[r,n]of t.entries())if(n.match(e))return r;return-1},"isSubstringInArray");s(qN,"interpolateToCurve");s(rXe,"formatUrl");nXe=s((e,...t)=>{let r=e.split("."),n=r.length-1,i=r[n],a=window;for(let o=0;o{let r=Math.pow(10,t);return Math.round(e*r)/r},"roundNumber"),HN=s((e,t)=>{let r,n=t;for(let i of e){if(r){let a=mae(i,r);if(a===0)return r;if(a=1)return{x:i.x,y:i.y};if(o>0&&o<1)return{x:uae((1-o)*r.x+o*i.x,5),y:uae((1-o)*r.y+o*i.y,5)}}}r=i}throw new Error("Could not find a suitable point for the given distance")},"calculatePoint"),sXe=s((e,t,r)=>{se.info(`our points ${JSON.stringify(t)}`),t[0]!==r&&(t=t.reverse());let i=HN(t,25),a=e?10:5,o=Math.atan2(t[0].y-i.y,t[0].x-i.x),l={x:0,y:0};return l.x=Math.sin(o)*a+(t[0].x+i.x)/2,l.y=-Math.cos(o)*a+(t[0].y+i.y)/2,l},"calcCardinalityPosition");s(oXe,"calcTerminalLabelPosition");s(UN,"getStylesFromArray");hae=0,YN=s(()=>(hae++,"id-"+Math.random().toString(36).substr(2,12)+"-"+hae),"generateId");s(lXe,"makeRandomHex");jN=s(e=>lXe(e.length),"random"),cXe=s(function(){return{x:0,y:0,fill:void 0,anchor:"start",style:"#666",width:100,height:100,textMargin:0,rx:0,ry:0,valign:void 0,text:""}},"getTextObj"),uXe=s(function(e,t){let r=t.text.replace(Tt.lineBreakRegex," "),[,n]=Bs(t.fontSize),i=e.append("text");i.attr("x",t.x),i.attr("y",t.y),i.style("text-anchor",t.anchor),i.style("font-family",t.fontFamily),i.style("font-size",n),i.style("font-weight",t.fontWeight),i.attr("fill",t.fill),t.class!==void 0&&i.attr("class",t.class);let a=i.append("tspan");return a.attr("x",t.x+t.textMargin*2),a.attr("fill",t.fill),a.text(r),i},"drawSimpleText"),Tm=bm((e,t,r)=>{if(!e||(r=Object.assign({fontSize:12,fontWeight:400,fontFamily:"Arial",joinWith:"
"},r),Tt.lineBreakRegex.test(e)))return e;let n=e.split(" ").filter(Boolean),i=[],a="";return n.forEach((o,l)=>{let u=Qa(`${o} `,r),h=Qa(a,r);if(u>t){let{hyphenatedStrings:p,remainingWord:m}=hXe(o,t,"-",r);i.push(a,...p),a=m}else h+u>=t?(i.push(a),a=o):a=[a,o].filter(Boolean).join(" ");l+1===n.length&&i.push(a)}),i.filter(o=>o!=="").join(r.joinWith)},(e,t,r)=>`${e}${t}${r.fontSize}${r.fontWeight}${r.fontFamily}${r.joinWith}`),hXe=bm((e,t,r="-",n)=>{n=Object.assign({fontSize:12,fontWeight:400,fontFamily:"Arial",margin:0},n);let i=[...e],a=[],o="";return i.forEach((l,u)=>{let h=`${o}${l}`;if(Qa(h,n)>=t){let f=u+1,p=i.length===f,m=`${h}${r}`;a.push(p?h:m),o=""}else o=h}),{hyphenatedStrings:a,remainingWord:o}},(e,t,r="-",n)=>`${e}${t}${r}${n.fontSize}${n.fontWeight}${n.fontFamily}`);s(n4,"calculateTextHeight");s(Qa,"calculateTextWidth");y2=bm((e,t)=>{let{fontSize:r=12,fontFamily:n="Arial",fontWeight:i=400}=t;if(!e)return{width:0,height:0};let[,a]=Bs(r),o=["sans-serif",n],l=e.split(Tt.lineBreakRegex),u=[],h=ut("body");if(!h.remove)return{width:0,height:0,lineHeight:0};let d=h.append("svg");for(let p of o){let m=0,g={width:0,height:0,lineHeight:0};for(let y of l){let v=cXe();v.text=y||WN;let x=uXe(d,v).style("font-size",a).style("font-weight",i).style("font-family",p),b=(x._groups||x)[0][0].getBBox();if(b.width===0&&b.height===0)throw new Error("svg element not in render tree");g.width=Math.round(Math.max(g.width,b.width)),m=Math.round(b.height),g.height+=m,g.lineHeight=Math.round(Math.max(g.lineHeight,m))}u.push(g)}d.remove();let f=isNaN(u[1].height)||isNaN(u[1].width)||isNaN(u[1].lineHeight)||u[0].height>u[1].height&&u[0].width>u[1].width&&u[0].lineHeight>u[1].lineHeight?0:1;return u[f]},(e,t)=>`${e}${t.fontSize}${t.fontWeight}${t.fontFamily}`),VN=class{constructor(t=!1,r){this.count=0;this.count=r?r.length:0,this.next=t?()=>this.count++:()=>Date.now()}static{s(this,"InitIDGenerator")}},dXe=s(function(e){return r4=r4||document.createElement("div"),e=escape(e).replace(/%26/g,"&").replace(/%23/g,"#").replace(/%3B/g,";"),r4.innerHTML=e,unescape(r4.textContent)},"entityDecode");s(XN,"isDetailedError");fXe=s((e,t,r,n)=>{if(!n)return;let i=e.node()?.getBBox();i&&e.append("text").text(n).attr("text-anchor","middle").attr("x",i.x+i.width/2).attr("y",-r).attr("class",t)},"insertTitle"),Bs=s(e=>{if(typeof e=="number")return[e,e+"px"];let t=parseInt(e??"",10);return Number.isNaN(t)?[void 0,void 0]:e===String(t)?[t,e+"px"]:[t,e]},"parseFontSize");s(Xr,"cleanAndMerge");or={assignWithDepth:ii,wrapLabel:Tm,calculateTextHeight:n4,calculateTextWidth:Qa,calculateTextDimensions:y2,cleanAndMerge:Xr,detectInit:eXe,detectDirective:fae,isSubstringInArray:tXe,interpolateToCurve:qN,calcLabelPosition:aXe,calcCardinalityPosition:sXe,calcTerminalLabelPosition:oXe,formatUrl:rXe,getStylesFromArray:UN,generateId:YN,random:jN,runFunc:nXe,entityDecode:dXe,insertTitle:fXe,isLabelCoordinateInPath:pXe,parseFontSize:Bs,InitIDGenerator:VN},gae=s(function(e){let t=e;return t=t.replace(/style.*:\S*#.*;/g,function(r){return r.substring(0,r.length-1)}),t=t.replace(/classDef.*:\S*#.*;/g,function(r){return r.substring(0,r.length-1)}),t=t.replace(/#\w+;/g,function(r){let n=r.substring(1,r.length-1);return/^\+?\d+$/.test(n)?"\uFB02\xB0\xB0"+n+"\xB6\xDF":"\uFB02\xB0"+n+"\xB6\xDF"}),t},"encodeEntities"),lo=s(function(e){return e.replace(/fl°°/g,"&#").replace(/fl°/g,"&").replace(/¶ß/g,";")},"decodeEntities"),Jc=s((e,t,{counter:r=0,prefix:n,suffix:i},a)=>a||`${n?`${n}_`:""}${e}_${t}_${r}${i?`_${i}`:""}`,"getEdgeId");s(un,"handleUndefinedAttr");s(pXe,"isLabelCoordinateInPath")});var yae,vae=F(()=>{"use strict";Wr();yae=s((e,t)=>{if(t==="sandbox"){let n=ut("#i"+e).node()?.contentDocument;if(!n)throw new Error(`Sandbox iframe #i${e} is missing its content document`);return{root:ut(n.body),doc:n}}return{root:ut("body"),doc:document}},"getDiagramRoot")});var drr,My=F(()=>{"use strict";drr=typeof performance<"u"&&typeof performance.now=="function";globalThis.injected??={includeLargeFeatures:!0,profiling:!1,version:"0.0.0"}});function JN(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function wae(e){Sm=e}function Nn(e,t=""){let r=typeof e=="string"?e:e.source,n={replace:s((i,a)=>{let o=typeof a=="string"?a:a.source;return o=o.replace($s.caret,"$1"),r=r.replace(i,o),n},"replace"),getRegex:s(()=>new RegExp(r,t),"getRegex")};return n}function eu(e,t){if(t){if($s.escapeTest.test(e))return e.replace($s.escapeReplace,bae)}else if($s.escapeTestNoEncode.test(e))return e.replace($s.escapeReplaceNoEncode,bae);return e}function Tae(e){try{e=encodeURI(e).replace($s.percentDecode,"%")}catch{return null}return e}function Cae(e,t){let r=e.replace($s.findPipe,(a,o,l)=>{let u=!1,h=o;for(;--h>=0&&l[h]==="\\";)u=!u;return u?"|":" |"}),n=r.split($s.splitPipe),i=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0?-2:-1}function Sae(e,t,r,n,i){let a=t.href,o=t.title||null,l=e[1].replace(i.other.outputLinkReplace,"$1");n.state.inLink=!0;let u={type:e[0].charAt(0)==="!"?"image":"link",raw:r,href:a,title:o,text:l,tokens:n.inlineTokens(l)};return n.state.inLink=!1,u}function XXe(e,t,r){let n=e.match(r.other.indentCodeCompensation);if(n===null)return t;let i=n[1];return t.split(` +`).map(a=>{let o=a.match(r.other.beginningSpace);if(o===null)return a;let[l]=o;return l.length>=i.length?a.slice(i.length):a}).join(` +`)}function _n(e,t){return Cm.parse(e,t)}var Sm,T2,$s,mXe,gXe,yXe,C2,vXe,eP,kae,Eae,xXe,tP,bXe,rP,TXe,CXe,l4,nP,SXe,Aae,wXe,iP,xae,kXe,EXe,AXe,RXe,Rae,_Xe,c4,aP,_ae,LXe,Lae,DXe,IXe,MXe,Dae,NXe,PXe,Iae,OXe,BXe,$Xe,FXe,GXe,zXe,VXe,a4,WXe,Mae,Nae,qXe,sP,HXe,KN,UXe,i4,v2,YXe,bae,s4,Ch,o4,oP,Sh,b2,KXe,Cm,mrr,grr,yrr,vrr,xrr,brr,Trr,Pae=F(()=>{"use strict";s(JN,"L");Sm=JN();s(wae,"G");T2={exec:s(()=>null,"exec")};s(Nn,"h");$s={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:s(e=>new RegExp(`^( {0,3}${e})((?:[ ][^\\n]*)?(?:\\n|$))`),"listItemRegex"),nextBulletRegex:s(e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),"nextBulletRegex"),hrRegex:s(e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),"hrRegex"),fencesBeginRegex:s(e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),"fencesBeginRegex"),headingBeginRegex:s(e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),"headingBeginRegex"),htmlBeginRegex:s(e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i"),"htmlBeginRegex")},mXe=/^(?:[ \t]*(?:\n|$))+/,gXe=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,yXe=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,C2=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,vXe=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,eP=/(?:[*+-]|\d{1,9}[.)])/,kae=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,Eae=Nn(kae).replace(/bull/g,eP).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),xXe=Nn(kae).replace(/bull/g,eP).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),tP=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,bXe=/^[^\n]+/,rP=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,TXe=Nn(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",rP).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),CXe=Nn(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,eP).getRegex(),l4="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",nP=/|$))/,SXe=Nn("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",nP).replace("tag",l4).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Aae=Nn(tP).replace("hr",C2).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",l4).getRegex(),wXe=Nn(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",Aae).getRegex(),iP={blockquote:wXe,code:gXe,def:TXe,fences:yXe,heading:vXe,hr:C2,html:SXe,lheading:Eae,list:CXe,newline:mXe,paragraph:Aae,table:T2,text:bXe},xae=Nn("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",C2).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",l4).getRegex(),kXe={...iP,lheading:xXe,table:xae,paragraph:Nn(tP).replace("hr",C2).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",xae).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",l4).getRegex()},EXe={...iP,html:Nn(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",nP).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:T2,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:Nn(tP).replace("hr",C2).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",Eae).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},AXe=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,RXe=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,Rae=/^( {2,}|\\)\n(?!\s*$)/,_Xe=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,Dae=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,NXe=Nn(Dae,"u").replace(/punct/g,c4).getRegex(),PXe=Nn(Dae,"u").replace(/punct/g,Lae).getRegex(),Iae="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",OXe=Nn(Iae,"gu").replace(/notPunctSpace/g,_ae).replace(/punctSpace/g,aP).replace(/punct/g,c4).getRegex(),BXe=Nn(Iae,"gu").replace(/notPunctSpace/g,IXe).replace(/punctSpace/g,DXe).replace(/punct/g,Lae).getRegex(),$Xe=Nn("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,_ae).replace(/punctSpace/g,aP).replace(/punct/g,c4).getRegex(),FXe=Nn(/\\(punct)/,"gu").replace(/punct/g,c4).getRegex(),GXe=Nn(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),zXe=Nn(nP).replace("(?:-->|$)","-->").getRegex(),VXe=Nn("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",zXe).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),a4=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`[^`]*`|[^\[\]\\`])*?/,WXe=Nn(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",a4).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Mae=Nn(/^!?\[(label)\]\[(ref)\]/).replace("label",a4).replace("ref",rP).getRegex(),Nae=Nn(/^!?\[(ref)\](?:\[\])?/).replace("ref",rP).getRegex(),qXe=Nn("reflink|nolink(?!\\()","g").replace("reflink",Mae).replace("nolink",Nae).getRegex(),sP={_backpedal:T2,anyPunctuation:FXe,autolink:GXe,blockSkip:MXe,br:Rae,code:RXe,del:T2,emStrongLDelim:NXe,emStrongRDelimAst:OXe,emStrongRDelimUnd:$Xe,escape:AXe,link:WXe,nolink:Nae,punctuation:LXe,reflink:Mae,reflinkSearch:qXe,tag:VXe,text:_Xe,url:T2},HXe={...sP,link:Nn(/^!?\[(label)\]\((.*?)\)/).replace("label",a4).getRegex(),reflink:Nn(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",a4).getRegex()},KN={...sP,emStrongRDelimAst:BXe,emStrongLDelim:PXe,url:Nn(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},bae=s(e=>YXe[e],"ke");s(eu,"w");s(Tae,"J");s(Cae,"V");s(x2,"z");s(jXe,"ge");s(Sae,"fe");s(XXe,"Je");s4=class{static{s(this,"y")}options;rules;lexer;constructor(e){this.options=e||Sm}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let r=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?r:x2(r,` +`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let r=t[0],n=XXe(r,t[3]||"",this.rules);return{type:"code",raw:r,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let r=t[2].trim();if(this.rules.other.endingHash.test(r)){let n=x2(r,"#");(this.options.pedantic||!n||this.rules.other.endingSpaceChar.test(n))&&(r=n.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:r,tokens:this.lexer.inline(r)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:x2(t[0],` +`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let r=x2(t[0],` +`).split(` +`),n="",i="",a=[];for(;r.length>0;){let o=!1,l=[],u;for(u=0;u1,i={type:"list",raw:"",ordered:n,start:n?+r.slice(0,-1):"",loose:!1,items:[]};r=n?`\\d{1,9}\\${r.slice(-1)}`:`\\${r}`,this.options.pedantic&&(r=n?r:"[*+-]");let a=this.rules.other.listItemRegex(r),o=!1;for(;e;){let u=!1,h="",d="";if(!(t=a.exec(e))||this.rules.block.hr.test(e))break;h=t[0],e=e.substring(h.length);let f=t[2].split(` +`,1)[0].replace(this.rules.other.listReplaceTabs,x=>" ".repeat(3*x.length)),p=e.split(` +`,1)[0],m=!f.trim(),g=0;if(this.options.pedantic?(g=2,d=f.trimStart()):m?g=t[1].length+1:(g=t[2].search(this.rules.other.nonSpaceChar),g=g>4?1:g,d=f.slice(g),g+=t[1].length),m&&this.rules.other.blankLine.test(p)&&(h+=p+` +`,e=e.substring(p.length+1),u=!0),!u){let x=this.rules.other.nextBulletRegex(g),b=this.rules.other.hrRegex(g),T=this.rules.other.fencesBeginRegex(g),w=this.rules.other.headingBeginRegex(g),C=this.rules.other.htmlBeginRegex(g);for(;e;){let S=e.split(` +`,1)[0],k;if(p=S,this.options.pedantic?(p=p.replace(this.rules.other.listReplaceNesting," "),k=p):k=p.replace(this.rules.other.tabCharGlobal," "),T.test(p)||w.test(p)||C.test(p)||x.test(p)||b.test(p))break;if(k.search(this.rules.other.nonSpaceChar)>=g||!p.trim())d+=` +`+k.slice(g);else{if(m||f.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||T.test(f)||w.test(f)||b.test(f))break;d+=` +`+p}!m&&!p.trim()&&(m=!0),h+=S+` +`,e=e.substring(S.length+1),f=k.slice(g)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(h)&&(o=!0));let y=null,v;this.options.gfm&&(y=this.rules.other.listIsTask.exec(d),y&&(v=y[0]!=="[ ] ",d=d.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:h,task:!!y,checked:v,loose:!1,text:d,tokens:[]}),i.raw+=h}let l=i.items.at(-1);if(l)l.raw=l.raw.trimEnd(),l.text=l.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let u=0;uf.type==="space"),d=h.length>0&&h.some(f=>this.rules.other.anyLine.test(f.raw));i.loose=d}if(i.loose)for(let u=0;u({text:l,tokens:this.lexer.inline(l),header:!1,align:a.align[u]})));return a}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let r=t[1].charAt(t[1].length-1)===` +`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:r,tokens:this.lexer.inline(r)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let r=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(r)){if(!this.rules.other.endAngleBracket.test(r))return;let a=x2(r.slice(0,-1),"\\");if((r.length-a.length)%2===0)return}else{let a=jXe(t[2],"()");if(a===-2)return;if(a>-1){let o=(t[0].indexOf("!")===0?5:4)+t[1].length+a;t[2]=t[2].substring(0,a),t[0]=t[0].substring(0,o).trim(),t[3]=""}}let n=t[2],i="";if(this.options.pedantic){let a=this.rules.other.pedanticHrefTitle.exec(n);a&&(n=a[1],i=a[3])}else i=t[3]?t[3].slice(1,-1):"";return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(r)?n=n.slice(1):n=n.slice(1,-1)),Sae(t,{href:n&&n.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let r;if((r=this.rules.inline.reflink.exec(e))||(r=this.rules.inline.nolink.exec(e))){let n=(r[2]||r[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[n.toLowerCase()];if(!i){let a=r[0].charAt(0);return{type:"text",raw:a,text:a}}return Sae(r,i,r[0],this.lexer,this.rules)}}emStrong(e,t,r=""){let n=this.rules.inline.emStrongLDelim.exec(e);if(!(!n||n[3]&&r.match(this.rules.other.unicodeAlphaNumeric))&&(!(n[1]||n[2])||!r||this.rules.inline.punctuation.exec(r))){let i=[...n[0]].length-1,a,o,l=i,u=0,h=n[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(h.lastIndex=0,t=t.slice(-1*e.length+i);(n=h.exec(t))!=null;){if(a=n[1]||n[2]||n[3]||n[4]||n[5]||n[6],!a)continue;if(o=[...a].length,n[3]||n[4]){l+=o;continue}else if((n[5]||n[6])&&i%3&&!((i+o)%3)){u+=o;continue}if(l-=o,l>0)continue;o=Math.min(o,o+l+u);let d=[...n[0]][0].length,f=e.slice(0,i+n.index+d+o);if(Math.min(i,o)%2){let m=f.slice(1,-1);return{type:"em",raw:f,text:m,tokens:this.lexer.inlineTokens(m)}}let p=f.slice(2,-2);return{type:"strong",raw:f,text:p,tokens:this.lexer.inlineTokens(p)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let r=t[2].replace(this.rules.other.newLineCharGlobal," "),n=this.rules.other.nonSpaceChar.test(r),i=this.rules.other.startingSpaceChar.test(r)&&this.rules.other.endingSpaceChar.test(r);return n&&i&&(r=r.substring(1,r.length-1)),{type:"codespan",raw:t[0],text:r}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let r,n;return t[2]==="@"?(r=t[1],n="mailto:"+r):(r=t[1],n=r),{type:"link",raw:t[0],text:r,href:n,tokens:[{type:"text",raw:r,text:r}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let r,n;if(t[2]==="@")r=t[0],n="mailto:"+r;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);r=t[0],t[1]==="www."?n="http://"+t[0]:n=t[0]}return{type:"link",raw:t[0],text:r,href:n,tokens:[{type:"text",raw:r,text:r}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let r=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:r}}}},Ch=class ZN{static{s(this,"l")}tokens;options;state;tokenizer;inlineQueue;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||Sm,this.options.tokenizer=this.options.tokenizer||new s4,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let r={other:$s,block:i4.normal,inline:v2.normal};this.options.pedantic?(r.block=i4.pedantic,r.inline=v2.pedantic):this.options.gfm&&(r.block=i4.gfm,this.options.breaks?r.inline=v2.breaks:r.inline=v2.gfm),this.tokenizer.rules=r}static get rules(){return{block:i4,inline:v2}}static lex(t,r){return new ZN(r).lex(t)}static lexInline(t,r){return new ZN(r).inlineTokens(t)}lex(t){t=t.replace($s.carriageReturn,` +`),this.blockTokens(t,this.tokens);for(let r=0;r(i=o.call({lexer:this},t,r))?(t=t.substring(i.raw.length),r.push(i),!0):!1))continue;if(i=this.tokenizer.space(t)){t=t.substring(i.raw.length);let o=r.at(-1);i.raw.length===1&&o!==void 0?o.raw+=` +`:r.push(i);continue}if(i=this.tokenizer.code(t)){t=t.substring(i.raw.length);let o=r.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+i.raw,o.text+=` +`+i.text,this.inlineQueue.at(-1).src=o.text):r.push(i);continue}if(i=this.tokenizer.fences(t)){t=t.substring(i.raw.length),r.push(i);continue}if(i=this.tokenizer.heading(t)){t=t.substring(i.raw.length),r.push(i);continue}if(i=this.tokenizer.hr(t)){t=t.substring(i.raw.length),r.push(i);continue}if(i=this.tokenizer.blockquote(t)){t=t.substring(i.raw.length),r.push(i);continue}if(i=this.tokenizer.list(t)){t=t.substring(i.raw.length),r.push(i);continue}if(i=this.tokenizer.html(t)){t=t.substring(i.raw.length),r.push(i);continue}if(i=this.tokenizer.def(t)){t=t.substring(i.raw.length);let o=r.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+i.raw,o.text+=` +`+i.raw,this.inlineQueue.at(-1).src=o.text):this.tokens.links[i.tag]||(this.tokens.links[i.tag]={href:i.href,title:i.title},r.push(i));continue}if(i=this.tokenizer.table(t)){t=t.substring(i.raw.length),r.push(i);continue}if(i=this.tokenizer.lheading(t)){t=t.substring(i.raw.length),r.push(i);continue}let a=t;if(this.options.extensions?.startBlock){let o=1/0,l=t.slice(1),u;this.options.extensions.startBlock.forEach(h=>{u=h.call({lexer:this},l),typeof u=="number"&&u>=0&&(o=Math.min(o,u))}),o<1/0&&o>=0&&(a=t.substring(0,o+1))}if(this.state.top&&(i=this.tokenizer.paragraph(a))){let o=r.at(-1);n&&o?.type==="paragraph"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+i.raw,o.text+=` +`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):r.push(i),n=a.length!==t.length,t=t.substring(i.raw.length);continue}if(i=this.tokenizer.text(t)){t=t.substring(i.raw.length);let o=r.at(-1);o?.type==="text"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+i.raw,o.text+=` +`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):r.push(i);continue}if(t){let o="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(o);break}else throw new Error(o)}}return this.state.top=!0,r}inline(t,r=[]){return this.inlineQueue.push({src:t,tokens:r}),r}inlineTokens(t,r=[]){let n=t,i=null;if(this.tokens.links){let l=Object.keys(this.tokens.links);if(l.length>0)for(;(i=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)l.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(i=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,i.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(i=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);n=this.options.hooks?.emStrongMask?.call({lexer:this},n)??n;let a=!1,o="";for(;t;){a||(o=""),a=!1;let l;if(this.options.extensions?.inline?.some(h=>(l=h.call({lexer:this},t,r))?(t=t.substring(l.raw.length),r.push(l),!0):!1))continue;if(l=this.tokenizer.escape(t)){t=t.substring(l.raw.length),r.push(l);continue}if(l=this.tokenizer.tag(t)){t=t.substring(l.raw.length),r.push(l);continue}if(l=this.tokenizer.link(t)){t=t.substring(l.raw.length),r.push(l);continue}if(l=this.tokenizer.reflink(t,this.tokens.links)){t=t.substring(l.raw.length);let h=r.at(-1);l.type==="text"&&h?.type==="text"?(h.raw+=l.raw,h.text+=l.text):r.push(l);continue}if(l=this.tokenizer.emStrong(t,n,o)){t=t.substring(l.raw.length),r.push(l);continue}if(l=this.tokenizer.codespan(t)){t=t.substring(l.raw.length),r.push(l);continue}if(l=this.tokenizer.br(t)){t=t.substring(l.raw.length),r.push(l);continue}if(l=this.tokenizer.del(t)){t=t.substring(l.raw.length),r.push(l);continue}if(l=this.tokenizer.autolink(t)){t=t.substring(l.raw.length),r.push(l);continue}if(!this.state.inLink&&(l=this.tokenizer.url(t))){t=t.substring(l.raw.length),r.push(l);continue}let u=t;if(this.options.extensions?.startInline){let h=1/0,d=t.slice(1),f;this.options.extensions.startInline.forEach(p=>{f=p.call({lexer:this},d),typeof f=="number"&&f>=0&&(h=Math.min(h,f))}),h<1/0&&h>=0&&(u=t.substring(0,h+1))}if(l=this.tokenizer.inlineText(u)){t=t.substring(l.raw.length),l.raw.slice(-1)!=="_"&&(o=l.raw.slice(-1)),a=!0;let h=r.at(-1);h?.type==="text"?(h.raw+=l.raw,h.text+=l.text):r.push(l);continue}if(t){let h="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(h);break}else throw new Error(h)}}return r}},o4=class{static{s(this,"P")}options;parser;constructor(e){this.options=e||Sm}space(e){return""}code({text:e,lang:t,escaped:r}){let n=(t||"").match($s.notSpaceStart)?.[0],i=e.replace($s.endingNewline,"")+` +`;return n?'
'+(r?i:eu(i,!0))+`
+`:"
"+(r?i:eu(i,!0))+`
+`}blockquote({tokens:e}){return`
+${this.parser.parse(e)}
+`}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} +`}hr(e){return`
+`}list(e){let t=e.ordered,r=e.start,n="";for(let o=0;o +`+n+" +`}listitem(e){let t="";if(e.task){let r=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=r+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=r+" "+eu(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:r+" ",text:r+" ",escaped:!0}):t+=r+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • +`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    +`}table(e){let t="",r="";for(let i=0;i${n}`),` + +`+t+` +`+n+`
    +`}tablerow({text:e}){return` +${e} +`}tablecell(e){let t=this.parser.parseInline(e.tokens),r=e.header?"th":"td";return(e.align?`<${r} align="${e.align}">`:`<${r}>`)+t+` +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${eu(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:r}){let n=this.parser.parseInline(r),i=Tae(e);if(i===null)return n;e=i;let a='
    ",a}image({href:e,title:t,text:r,tokens:n}){n&&(r=this.parser.parseInline(n,this.parser.textRenderer));let i=Tae(e);if(i===null)return eu(r);e=i;let a=`${r}{let o=i[a].flat(1/0);r=r.concat(this.walkTokens(o,t))}):i.tokens&&(r=r.concat(this.walkTokens(i.tokens,t)))}}return r}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(r=>{let n={...r};if(n.async=this.defaults.async||n.async||!1,r.extensions&&(r.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let a=t.renderers[i.name];a?t.renderers[i.name]=function(...o){let l=i.renderer.apply(this,o);return l===!1&&(l=a.apply(this,o)),l}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let a=t[i.level];a?a.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),n.extensions=t),r.renderer){let i=this.defaults.renderer||new o4(this.defaults);for(let a in r.renderer){if(!(a in i))throw new Error(`renderer '${a}' does not exist`);if(["options","parser"].includes(a))continue;let o=a,l=r.renderer[o],u=i[o];i[o]=(...h)=>{let d=l.apply(i,h);return d===!1&&(d=u.apply(i,h)),d||""}}n.renderer=i}if(r.tokenizer){let i=this.defaults.tokenizer||new s4(this.defaults);for(let a in r.tokenizer){if(!(a in i))throw new Error(`tokenizer '${a}' does not exist`);if(["options","rules","lexer"].includes(a))continue;let o=a,l=r.tokenizer[o],u=i[o];i[o]=(...h)=>{let d=l.apply(i,h);return d===!1&&(d=u.apply(i,h)),d}}n.tokenizer=i}if(r.hooks){let i=this.defaults.hooks||new b2;for(let a in r.hooks){if(!(a in i))throw new Error(`hook '${a}' does not exist`);if(["options","block"].includes(a))continue;let o=a,l=r.hooks[o],u=i[o];b2.passThroughHooks.has(a)?i[o]=h=>{if(this.defaults.async&&b2.passThroughHooksRespectAsync.has(a))return Promise.resolve(l.call(i,h)).then(f=>u.call(i,f));let d=l.call(i,h);return u.call(i,d)}:i[o]=(...h)=>{let d=l.apply(i,h);return d===!1&&(d=u.apply(i,h)),d}}n.hooks=i}if(r.walkTokens){let i=this.defaults.walkTokens,a=r.walkTokens;n.walkTokens=function(o){let l=[];return l.push(a.call(this,o)),i&&(l=l.concat(i.call(this,o))),l}}this.defaults={...this.defaults,...n}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return Ch.lex(e,t??this.defaults)}parser(e,t){return Sh.parse(e,t??this.defaults)}parseMarkdown(e){return(t,r)=>{let n={...r},i={...this.defaults,...n},a=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&n.async===!1)return a(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof t>"u"||t===null)return a(new Error("marked(): input parameter is undefined or null"));if(typeof t!="string")return a(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));i.hooks&&(i.hooks.options=i,i.hooks.block=e);let o=i.hooks?i.hooks.provideLexer():e?Ch.lex:Ch.lexInline,l=i.hooks?i.hooks.provideParser():e?Sh.parse:Sh.parseInline;if(i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(t):t).then(u=>o(u,i)).then(u=>i.hooks?i.hooks.processAllTokens(u):u).then(u=>i.walkTokens?Promise.all(this.walkTokens(u,i.walkTokens)).then(()=>u):u).then(u=>l(u,i)).then(u=>i.hooks?i.hooks.postprocess(u):u).catch(a);try{i.hooks&&(t=i.hooks.preprocess(t));let u=o(t,i);i.hooks&&(u=i.hooks.processAllTokens(u)),i.walkTokens&&this.walkTokens(u,i.walkTokens);let h=l(u,i);return i.hooks&&(h=i.hooks.postprocess(h)),h}catch(u){return a(u)}}}onError(e,t){return r=>{if(r.message+=` +Please report this to https://github.com/markedjs/marked.`,e){let n="

    An error occurred:

    "+eu(r.message+"",!0)+"
    ";return t?Promise.resolve(n):n}if(t)return Promise.reject(r);throw r}}},Cm=new KXe;s(_n,"d");_n.options=_n.setOptions=function(e){return Cm.setOptions(e),_n.defaults=Cm.defaults,wae(_n.defaults),_n};_n.getDefaults=JN;_n.defaults=Sm;_n.use=function(...e){return Cm.use(...e),_n.defaults=Cm.defaults,wae(_n.defaults),_n};_n.walkTokens=function(e,t){return Cm.walkTokens(e,t)};_n.parseInline=Cm.parseInline;_n.Parser=Sh;_n.parser=Sh.parse;_n.Renderer=o4;_n.TextRenderer=oP;_n.Lexer=Ch;_n.lexer=Ch.lex;_n.Tokenizer=s4;_n.Hooks=b2;_n.parse=_n;mrr=_n.options,grr=_n.setOptions,yrr=_n.use,vrr=_n.walkTokens,xrr=_n.parseInline,brr=Sh.parse,Trr=Ch.lex});function ZXe(e,{markdownAutoWrap:t}){let n=e.replace(//g,` +`).replace(/\n{2,}/g,` +`);return ck(n)}function Oae(e){return e.split(/\\n|\n|/gi).map(t=>t.trim().match(/<[^>]+>|[^\s<>]+/g)?.map(r=>({content:r,type:"normal"}))??[])}function Bae(e,t={}){let r=ZXe(e,t),n=_n.lexer(r),i=[[]],a=0;function o(l,u="normal"){l.type==="text"?l.text.split(` +`).forEach((d,f)=>{f!==0&&(a++,i.push([])),d.split(" ").forEach(p=>{p=p.replace(/'/g,"'"),p&&i[a].push({content:p,type:u})})}):l.type==="strong"||l.type==="em"?l.tokens.forEach(h=>{o(h,l.type)}):l.type==="html"&&i[a].push({content:l.text,type:"normal"})}return s(o,"processNode"),n.forEach(l=>{l.type==="paragraph"?l.tokens?.forEach(u=>{o(u)}):l.type==="html"?i[a].push({content:l.text,type:"normal"}):i[a].push({content:l.raw,type:"normal"})}),i}function $ae(e){return e?`

    ${e.replace(/\\n|\n/g,"
    ")}

    `:""}function Fae(e,{markdownAutoWrap:t}={}){let r=_n.lexer(e);function n(i){return i.type==="text"?t===!1?i.text.replace(/\n */g,"
    ").replace(/ /g," "):i.text.replace(/\n */g,"
    "):i.type==="strong"?`${i.tokens?.map(n).join("")}`:i.type==="em"?`${i.tokens?.map(n).join("")}`:i.type==="paragraph"?`

    ${i.tokens?.map(n).join("")}

    `:i.type==="space"?"":i.type==="html"?`${i.text}`:i.type==="escape"?i.text:(se.warn(`Unsupported markdown: ${i.type}`),i.raw)}return s(n,"output"),r.map(n).join("")}var Gae=F(()=>{"use strict";Pae();Y7();St();s(ZXe,"preprocessMarkdown");s(Oae,"nonMarkdownToLines");s(Bae,"markdownToLines");s($ae,"nonMarkdownToHTML");s(Fae,"markdownToHTML")});var zae=As((lP,cP)=>{"use strict";(function(e){"use strict";var t=function(){},r=e.requestAnimationFrame||e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.msRequestAnimationFrame||function(h){return setTimeout(h,16)};function n(){var h=this;h.reads=[],h.writes=[],h.raf=r.bind(e),t("initialized",h)}s(n,"FastDom"),n.prototype={constructor:n,runTasks:s(function(h){t("run tasks");for(var d;d=h.shift();)d()},"runTasks"),measure:s(function(h,d){t("measure");var f=d?h.bind(d):h;return this.reads.push(f),i(this),f},"measure"),mutate:s(function(h,d){t("mutate");var f=d?h.bind(d):h;return this.writes.push(f),i(this),f},"mutate"),clear:s(function(h){return t("clear",h),o(this.reads,h)||o(this.writes,h)},"clear"),extend:s(function(h){if(t("extend",h),typeof h!="object")throw new Error("expected object");var d=Object.create(this);return l(d,h),d.fastdom=this,d.initialize&&d.initialize(),d},"extend"),catch:null};function i(h){h.scheduled||(h.scheduled=!0,h.raf(a.bind(null,h)),t("flush scheduled"))}s(i,"scheduleFlush");function a(h){t("flush");var d=h.writes,f=h.reads,p;try{t("flushing reads",f.length),h.runTasks(f),t("flushing writes",d.length),h.runTasks(d)}catch(m){p=m}if(h.scheduled=!1,(f.length||d.length)&&i(h),p)if(t("task errored",p.message),h.catch)h.catch(p);else throw p}s(a,"flush");function o(h,d){var f=h.indexOf(d);return!!~f&&!!h.splice(f,1)}s(o,"remove");function l(h,d){for(var f in d)d.hasOwnProperty(f)&&(h[f]=d[f])}s(l,"mixin");var u=e.fastdom=e.fastdom||new n;typeof define=="function"?define(function(){return u}):typeof cP=="object"&&(cP.exports=u)})(typeof window<"u"?window:typeof lP<"u"?lP:globalThis)});var Vae=As((Prr,uP)=>{"use strict";(function(){var e={initialize:s(function(){this._tasks=new Map},"initialize"),mutate:s(function(r,n){return t(this,"mutate",r,n)},"mutate"),measure:s(function(r,n){return t(this,"measure",r,n)},"measure"),clear:s(function(r){var n=this._tasks,i=n.get(r);this.fastdom.clear(i),n.delete(r)},"clear")};function t(r,n,i,a){var o=r._tasks,l=r.fastdom,u,h=new Promise(function(d,f){u=l[n](function(){o.delete(h);try{d(a?i.call(a):i())}catch(p){f(p)}},a)});return o.set(h,u),h}s(t,"create"),(typeof define)[0]=="f"?define(function(){return e}):(typeof uP)[0]=="o"?uP.exports=e:window.fastdomPromised=e})()});var Wae,qae,QXe,tu,u4=F(()=>{"use strict";Wae=Rs(zae(),1),qae=Rs(Vae(),1),QXe=Wae.default.extend({raf(e){typeof queueMicrotask=="function"?queueMicrotask(e):setTimeout(e,0)}}).extend(qae.default),tu=QXe});function JXe(e){return Intl.Segmenter?[...new Intl.Segmenter().segment(e)].map(t=>t.segment):[...e]}function eKe(e,t){let r=JXe(t.content);return Hae(e,[],r,t.type)}function Hae(e,t,r,n){if(r.length===0)return[{content:t.join(""),type:n},{content:"",type:n}];let[i,...a]=r,o=[...t,i];return e([{content:o.join(""),type:n}])?Hae(e,o,a,n):(t.length===0&&i&&(t.push(i),r.shift()),[{content:t.join(""),type:n},{content:r.join(""),type:n}])}function Uae(e,t){if(e.some(({content:r})=>r.includes(` +`)))throw new Error("splitLineToFitWidth does not support newlines in the line");return hP(e,t)}function hP(e,t,r=[],n=[]){if(e.length===0)return n.length>0&&r.push(n),r.length>0?r:[];let i="";e[0].content===" "&&(i=" ",e.shift());let a=e.shift()??{content:" ",type:"normal"},o=[...n];if(i!==""&&o.push({content:i,type:"normal"}),o.push(a),t(o))return hP(e,t,r,o);if(n.length>0)r.push(n),e.unshift(a);else if(a.content){let[l,u]=eKe(t,a);r.push([l]),u.content&&e.unshift(u)}return hP(e,t,r)}var Yae=F(()=>{"use strict";s(JXe,"splitTextToChars");s(eKe,"splitWordToFitWidth");s(Hae,"splitWordToFitWidthRecursion");s(Uae,"splitLineToFitWidth");s(hP,"splitLineToFitWidthRecursion")});function jae(e,t){t&&e.attr("style",t)}async function tKe(e,t,r,n,i=!1,a=Lt()){let o=e.append("foreignObject");o.attr("width",`${Math.min(10*r,Xae)}px`),o.attr("height",`${Math.min(10*r,Xae)}px`);let l=o.append("xhtml:div"),u=si(t.label)?await J0(t.label.replace(Tt.lineBreakRegex,` +`),a):xr(t.label,a),h=t.isNode?"nodeLabel":"edgeLabel",d=l.append("span");return d.html(u),jae(d,t.labelStyle),d.attr("class",`${h} ${n}`),jae(l,t.labelStyle),l.style("display","table-cell"),l.style("white-space","nowrap"),l.style("line-height","1.5"),r!==Number.POSITIVE_INFINITY&&(l.style("max-width",r+"px"),l.style("text-align","center")),l.attr("xmlns","http://www.w3.org/1999/xhtml"),i&&l.attr("class","labelBkg"),(await tu.measure(()=>l.node().getBoundingClientRect())).width===r&&(l.style("display","table"),l.style("white-space","break-spaces"),l.style("width",r+"px")),o.node()}function dP(e,t,r,n=!1){let i=e.append("tspan").attr("class","text-outer-tspan").attr("x",0).attr("y",t*r-.1+"em").attr("dy",r+"em");return n&&i.attr("text-anchor","middle"),i}function rKe(e,t,r){let n=e.append("text"),i=dP(n,1,t);fP(i,r);let a=i.node().getComputedTextLength();return n.remove(),a}function Zae(e,t,r){let n=e.append("text"),i=dP(n,1,t);fP(i,[{content:r,type:"normal"}]);let a=i.node()?.getBoundingClientRect();return a&&n.remove(),a}function nKe(e,t,r,n=!1,i=!1){let o=t.append("g"),l=o.insert("rect").attr("class","background").attr("style","stroke: none"),u=o.append("text").attr("y","-10.1");i&&u.attr("text-anchor","middle");let h=0;for(let d of r){let f=s(m=>rKe(o,1.1,m)<=e,"checkWidth"),p=f(d)?[d]:Uae(d,f);for(let m of p){let g=dP(u,h,1.1,i);fP(g,m),h++}}if(n){let d=u.node().getBBox(),f=2;return l.attr("x",d.x-f).attr("y",d.y-f).attr("width",d.width+2*f).attr("height",d.height+2*f),o.node()}else return u.node()}function Kae(e){let t=/&(amp|lt|gt);/g;return e.replace(t,(r,n)=>{switch(n){case"amp":return"&";case"lt":return"<";case"gt":return">";default:return r}})}function fP(e,t){e.text(""),t.forEach((r,n)=>{let i=e.append("tspan").attr("font-style",r.type==="em"?"italic":"normal").attr("class","text-inner-tspan").attr("font-weight",r.type==="strong"?"bold":"normal");n===0?i.text(Kae(r.content)):i.text(" "+Kae(r.content))})}async function iKe(e,t={}){let r=[];e.replace(/(fa[bklrs]?):fa-([\w-]+)/g,(i,a,o)=>(r.push((async()=>{let l=`${a}:${o}`;return await pJ(l)?await ps(l,void 0,{class:"label-icon"}):``})()),i));let n=await Promise.all(r);return e.replace(/(fa[bklrs]?):fa-([\w-]+)/g,()=>n.shift()??"")}var Xae,li,qo=F(()=>{"use strict";Wr();Yr();St();My();Gae();er();u4();Hl();Yae();br();s(jae,"applyStyle");Xae=16384;s(tKe,"addHtmlSpan");s(dP,"createTspan");s(rKe,"computeWidthOfText");s(Zae,"computeDimensionOfText");s(nKe,"createFormattedText");s(Kae,"decodeHTMLEntities");s(fP,"updateTextContentAndStyles");s(iKe,"replaceIconSubstring");li=s(async(e,t="",{style:r="",isTitle:n=!1,classes:i="",useHtmlLabels:a=!0,markdown:o=!0,isNode:l=!0,width:u=200,addSvgBackground:h=!1}={},d)=>{if(se.debug("XYZ createText",t,r,n,i,a,l,"addSvgBackground: ",h),a){let f=o?Fae(t,d):$ae(t),p=await iKe(lo(f),d),m=t.replace(/\\\\/g,"\\"),g={isNode:l,label:si(t)?m:p,labelStyle:r.replace("fill:","color:")};return await tKe(e,g,u,i,h,d)}else{let f=lo(t.replace(//g,"
    ")),p=o?Bae(f.replace("
    ","
    "),d):Oae(f),m=nKe(u,e,p,t?h:!1,!l);if(l){/stroke:/.exec(r)&&(r=r.replace("stroke:","lineColor:"));let g=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/color:/g,"fill:");ut(m).attr("style",g)}else{let g=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/background:/g,"fill:");ut(m).select("rect").attr("style",g.replace(/background:/g,"fill:"));let y=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/color:/g,"fill:");ut(m).select("text").attr("style",y)}return n?ut(m).selectAll("tspan.text-outer-tspan").classed("title-row",!0):ut(m).selectAll("tspan.text-outer-tspan").classed("row",!0),m}},"createText")});async function h4(e){let t=e.getElementsByTagName("img");if(!t||t.length===0)return;let r=!Qae(e);await Promise.all([...t].map(n=>new Promise(i=>{function a(){if(n.style.display="flex",n.style.flexDirection="column",r){let o=Pe().fontSize?Pe().fontSize:window.getComputedStyle(document.body).fontSize,l=5,[u=mr.fontSize]=Bs(o),h=u*l+"px";n.style.minWidth=h,n.style.maxWidth=h}else n.style.width="100%";i(n)}s(a,"setupImage"),setTimeout(()=>{n.complete&&a()}),n.addEventListener("error",a),n.addEventListener("load",a)})))}function Qae(e){return e.nodeType===sKe?e.textContent?.trim()!=="":e.nodeType!==aKe||e.tagName.toLowerCase()==="img"?!1:[...e.childNodes].some(Qae)}var aKe,sKe,pP=F(()=>{"use strict";Jt();Qi();er();aKe=1,sKe=3;s(h4,"configureLabelImages");s(Qae,"hasTextBesidesImages")});var Jae,oKe,ese,tse=F(()=>{"use strict";Wr();qo();Jt();Yr();er();Jae=3,oKe=32,ese=s(async(e,t,r)=>{let n=Pe(),i=e.insert("g").attr("class",r??"node default").attr("id",t.domId||t.id),a=i.insert("g").attr("class","label").attr("style",un(t.labelStyle)),l=[{text:typeof t.label=="string"?t.label:t.label?.[0]??"",cssClass:"c4-name"},{text:t.stereotype,cssClass:"c4-type"},...(t.description??[]).map(x=>({text:x,cssClass:"c4-descr"}))].filter(x=>x.text),u=t.width?Math.max(t.width-2*(t.padding??0),oKe):Pe().flowchart?.wrappingWidth??200,d=n.c4?.wrap??!0?u:Number.POSITIVE_INFINITY,f=await Promise.all(l.map(async x=>{let b=a.append("g").attr("class",x.cssClass),T=await li(b,xr(lo(x.text??""),n),{useHtmlLabels:!1,markdown:!1,isNode:!0,width:d,style:t.labelStyle},n);return ut(T).selectAll("tspan.text-outer-tspan").attr("text-anchor","middle"),ut(T).selectAll("tspan.text-inner-tspan").attr("font-weight",null).attr("font-style",null),{el:b,box:b.node().getBBox()}})),p=Math.max(...f.map(({box:x})=>x.width),0),m=0;for(let{el:x,box:b}of f)x.attr("transform",`translate(${p/2-b.x-b.width/2}, ${m-b.y})`),m+=b.height+Jae;let g=f.length>0?m-Jae:0;a.insert("rect",":first-child"),a.attr("transform",`translate(${-p/2}, ${-g/2})`);let y=a.node().getBBox(),v=(t.padding??0)/2;return{shapeSvg:i,bbox:y,halfPadding:v,label:a}},"c4LabelHelper")});function hr(e){let t=e.map((r,n)=>`${n===0?"M":"L"}${r.x},${r.y}`);return t.push("Z"),t.join(" ")}function vl(e,t,r,n,i,a){let o=[],u=r-e,h=n-t,d=u/a,f=2*Math.PI/d,p=t+h/2;for(let m=0;m<=50;m++){let g=m/50,y=e+g*u,v=p+i*Math.sin(f*(y-e));o.push({x:y,y:v})}return o}function Ho(e,t,r,n,i,a){let o=[],l=i*Math.PI/180,d=(a*Math.PI/180-l)/(n-1);for(let f=0;fu.tagName==="path"),r=document.createElementNS("http://www.w3.org/2000/svg","path"),n=t.map(u=>u.getAttribute("d")).filter(u=>u!==null).join(" ");r.setAttribute("d",n);let i=t.find(u=>u.getAttribute("fill")!=="none"),a=t.find(u=>u.getAttribute("stroke")!=="none"),o=s((u,h)=>u?.getAttribute(h)??void 0,"getAttr");if(i){let u={fill:o(i,"fill"),"fill-opacity":o(i,"fill-opacity")??"1"};Object.entries(u).forEach(([h,d])=>{d&&r.setAttribute(h,d)})}if(a){let u={stroke:o(a,"stroke"),"stroke-width":o(a,"stroke-width")??"1","stroke-opacity":o(a,"stroke-opacity")??"1"};Object.entries(u).forEach(([h,d])=>{d&&r.setAttribute(h,d)})}let l=document.createElementNS("http://www.w3.org/2000/svg","g");return l.appendChild(r),l}var yt,d4,ht,vt,Ht=F(()=>{"use strict";qo();u4();Jt();br();Wr();Yr();er();pP();My();tse();yt=s(async(e,t,r)=>{if(t.stereotype!==void 0)return ese(e,t,r);let n,i=t.useHtmlLabels||Aa(Pe()?.htmlLabels);r?n=r:n="node default";let a=e.insert("g").attr("class",n).attr("id",t.domId||t.id),o=a.insert("g").attr("class","label").attr("style",un(t.labelStyle)),l;t.label===void 0?l="":l=typeof t.label=="string"?t.label:t.label[0];let u=!!t.icon||!!t.img,h=t.labelType==="markdown",d=await li(o,xr(lo(l),Pe()),{useHtmlLabels:i,width:t.width||t.wrappingWidth||Pe().flowchart?.wrappingWidth,classes:h?"markdown-node-label":"",style:t.labelStyle,addSvgBackground:u,markdown:h},Pe()),f=(t?.padding??0)/2,p;if(i){let m=d.children[0],g=ut(d);await h4(m),p=await tu.measure(()=>m.getBoundingClientRect()),g.attr("width",p.width),g.attr("height",p.height)}else p=await tu.measure(()=>d.getBBox());return i?o.attr("transform","translate("+-p.width/2+", "+-p.height/2+")"):o.attr("transform","translate(0, "+-p.height/2+")"),t.centerLabel&&o.attr("transform","translate("+-p.width/2+", "+-p.height/2+")"),o.insert("rect",":first-child"),{shapeSvg:a,bbox:p,halfPadding:f,label:o}},"labelHelper"),d4=s(async(e,t,r)=>{let n=r.useHtmlLabels??ai(Pe()),i=e.insert("g").attr("class","label").attr("style",r.labelStyle||""),a=await li(i,xr(lo(t),Pe()),{useHtmlLabels:n,width:r.width||Pe()?.flowchart?.wrappingWidth,style:r.labelStyle,addSvgBackground:!!r.icon||!!r.img}),o=r.padding/2,l;if(ai(Pe())){let u=a.children[0],h=ut(a);l=await tu.measure(()=>u.getBoundingClientRect()),h.attr("width",l.width),h.attr("height",l.height)}else l=await tu.measure(()=>a.getBBox());return n?i.attr("transform","translate("+-l.width/2+", "+-l.height/2+")"):i.attr("transform","translate(0, "+-l.height/2+")"),r.centerLabel&&i.attr("transform","translate("+-l.width/2+", "+-l.height/2+")"),i.insert("rect",":first-child"),{shapeSvg:e,bbox:l,halfPadding:o,label:i}},"insertLabel"),ht=s((e,t,r)=>{if(r){e.width=r.width,e.height=r.height;return}let n=t.node().getBBox();e.width=n.width,e.height=n.height},"updateNodeBounds"),vt=s((e,t)=>(e.look==="handDrawn"?"rough-node":"node")+" "+e.cssClasses+" "+(t||""),"getNodeClasses");s(hr,"createPathFromPoints");s(vl,"generateFullSineWavePoints");s(Ho,"generateCirclePoints");s(mP,"mergePaths")});function lKe(e,t){return e.intersect(t)}var rse,nse=F(()=>{"use strict";s(lKe,"intersectNode");rse=lKe});function cKe(e,t,r,n){var i=e.x,a=e.y,o=i-n.x,l=a-n.y,u=Math.sqrt(t*t*l*l+r*r*o*o),h=Math.abs(t*r*o/u);n.x{"use strict";s(cKe,"intersectEllipse");f4=cKe});function uKe(e,t,r){return f4(e,t,t,r)}var ise,ase=F(()=>{"use strict";gP();s(uKe,"intersectCircle");ise=uKe});function hKe(e,t,r,n){{let i=t.y-e.y,a=e.x-t.x,o=t.x*e.y-e.x*t.y,l=i*r.x+a*r.y+o,u=i*n.x+a*n.y+o,h=1e-6;if(l!==0&&u!==0&&sse(l,u))return;let d=n.y-r.y,f=r.x-n.x,p=n.x*r.y-r.x*n.y,m=d*e.x+f*e.y+p,g=d*t.x+f*t.y+p;if(Math.abs(m)0}var ose,lse=F(()=>{"use strict";s(hKe,"intersectLine");s(sse,"sameSign");ose=hKe});function dKe(e,t,r){let n=e.x,i=e.y,a=[],o=Number.POSITIVE_INFINITY,l=Number.POSITIVE_INFINITY;typeof t.forEach=="function"?t.forEach(function(d){o=Math.min(o,d.x),l=Math.min(l,d.y)}):(o=Math.min(o,t.x),l=Math.min(l,t.y));let u=n-e.width/2-o,h=i-e.height/2-l;for(let d=0;d1&&a.sort(function(d,f){let p=d.x-r.x,m=d.y-r.y,g=Math.sqrt(p*p+m*m),y=f.x-r.x,v=f.y-r.y,x=Math.sqrt(y*y+v*v);return g{"use strict";lse();s(dKe,"intersectPolygon");cse=dKe});var fKe,ru,p4=F(()=>{"use strict";fKe=s((e,t)=>{var r=e.x,n=e.y,i=t.x-r,a=t.y-n,o=e.width/2,l=e.height/2,u,h;return Math.abs(a)*o>Math.abs(i)*l?(a<0&&(l=-l),u=a===0?0:l*i/a,h=l):(i<0&&(o=-o),u=o,h=i===0?0:o*a/i),{x:r+u,y:n+h}},"intersectRect"),ru=fKe});var it,Zt=F(()=>{"use strict";nse();ase();gP();use();p4();it={node:rse,circle:ise,ellipse:f4,polygon:cse,rect:ru}});var hse,pKe,nu,mKe,S2,st,ft,gKe,Xt=F(()=>{"use strict";Jt();hse=s(e=>{let{handDrawnSeed:t}=Pe();return{fill:e,hachureAngle:120,hachureGap:4,fillWeight:2,roughness:.7,stroke:e,seed:t}},"solidStateFill"),pKe=s(e=>Array.isArray(e)?e:e?e.split(";").map(t=>t.trim()).filter(Boolean):[],"normalizeStyleList"),nu=s(e=>{let t=mKe([...e.cssCompiledStyles||[],...e.cssStyles||[],...pKe(e.labelStyle)]);return{stylesMap:t,stylesArray:[...t]}},"compileStyles"),mKe=s(e=>{let t=new Map;return e.forEach(r=>{let[n,i]=r.split(":");t.set(n.trim(),i?.trim())}),t},"styles2Map"),S2=s(e=>e==="color"||e==="font-size"||e==="font-family"||e==="font-weight"||e==="font-style"||e==="text-decoration"||e==="text-align"||e==="text-transform"||e==="line-height"||e==="letter-spacing"||e==="word-spacing"||e==="text-shadow"||e==="text-overflow"||e==="white-space"||e==="word-wrap"||e==="word-break"||e==="overflow-wrap"||e==="hyphens","isLabelStyle"),st=s(e=>{let{stylesArray:t}=nu(e),r=[],n=[],i=[],a=[];return t.forEach(o=>{let l=o[0];S2(l)?r.push(o.join(":")+" !important"):(n.push(o.join(":")+" !important"),l.includes("stroke")&&i.push(o.join(":")+" !important"),l==="fill"&&a.push(o.join(":")+" !important"))}),{labelStyles:r.join(";"),nodeStyles:n.join(";"),stylesArray:t,borderStyles:i,backgroundStyles:a}},"styles2String"),ft=s((e,t)=>{let{themeVariables:r,handDrawnSeed:n}=Pe(),{nodeBorder:i,mainBkg:a}=r,{stylesMap:o}=nu(e);return Object.assign({roughness:.7,fill:o.get("fill")||a,fillStyle:"hachure",fillWeight:4,hachureGap:5.2,stroke:o.get("stroke")||i,seed:n,strokeWidth:o.get("stroke-width")?.replace("px","")||1.3,fillLineDash:[0,0],strokeLineDash:gKe(o.get("stroke-dasharray"))},t)},"userNodeOverrides"),gKe=s(e=>{if(!e)return[0,0];let t=e.trim().split(/\s+/).map(Number);if(t.length===1){let i=isNaN(t[0])?0:t[0];return[i,i]}let r=isNaN(t[0])?0:t[0],n=isNaN(t[1])?0:t[1];return[r,n]},"getStrokeDashArray")});function yP(e,t,r){if(e&&e.length){let[n,i]=t,a=Math.PI/180*r,o=Math.cos(a),l=Math.sin(a);for(let u of e){let[h,d]=u;u[0]=(h-n)*o-(d-i)*l+n,u[1]=(h-n)*l+(d-i)*o+i}}}function yKe(e,t){return e[0]===t[0]&&e[1]===t[1]}function vKe(e,t,r,n=1){let i=r,a=Math.max(t,.1),o=e[0]&&e[0][0]&&typeof e[0][0]=="number"?[e]:e,l=[0,0];if(i)for(let h of o)yP(h,l,i);let u=(function(h,d,f){let p=[];for(let b of h){let T=[...b];yKe(T[0],T[T.length-1])||T.push([T[0][0],T[0][1]]),T.length>2&&p.push(T)}let m=[];d=Math.max(d,.1);let g=[];for(let b of p)for(let T=0;Tb.yminT.ymin?1:b.xT.x?1:b.ymax===T.ymax?0:(b.ymax-T.ymax)/Math.abs(b.ymax-T.ymax))),!g.length)return m;let y=[],v=g[0].ymin,x=0;for(;y.length||g.length;){if(g.length){let b=-1;for(let T=0;Tv);T++)b=T;g.splice(0,b+1).forEach((T=>{y.push({s:v,edge:T})}))}if(y=y.filter((b=>!(b.edge.ymax<=v))),y.sort(((b,T)=>b.edge.x===T.edge.x?0:(b.edge.x-T.edge.x)/Math.abs(b.edge.x-T.edge.x))),(f!==1||x%d==0)&&y.length>1)for(let b=0;b=y.length)break;let w=y[b].edge,C=y[T].edge;m.push([[Math.round(w.x),v],[Math.round(C.x),v]])}v+=f,y.forEach((b=>{b.edge.x=b.edge.x+f*b.edge.islope})),x++}return m})(o,a,n);if(i){for(let h of o)yP(h,l,-i);(function(h,d,f){let p=[];h.forEach((m=>p.push(...m))),yP(p,d,f)})(u,l,-i)}return u}function A2(e,t){var r;let n=t.hachureAngle+90,i=t.hachureGap;i<0&&(i=4*t.strokeWidth),i=Math.round(Math.max(i,.1));let a=1;return t.roughness>=1&&(((r=t.randomizer)===null||r===void 0?void 0:r.next())||Math.random())>.7&&(a=i),vKe(e,i,n,a||1)}function S4(e){let t=e[0],r=e[1];return Math.sqrt(Math.pow(t[0]-r[0],2)+Math.pow(t[1]-r[1],2))}function xP(e,t){return e.type===t}function MP(e){let t=[],r=(function(o){let l=new Array;for(;o!=="";)if(o.match(/^([ \t\r\n,]+)/))o=o.substr(RegExp.$1.length);else if(o.match(/^([aAcChHlLmMqQsStTvVzZ])/))l[l.length]={type:xKe,text:RegExp.$1},o=o.substr(RegExp.$1.length);else{if(!o.match(/^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/))return[];l[l.length]={type:vP,text:`${parseFloat(RegExp.$1)}`},o=o.substr(RegExp.$1.length)}return l[l.length]={type:dse,text:""},l})(e),n="BOD",i=0,a=r[i];for(;!xP(a,dse);){let o=0,l=[];if(n==="BOD"){if(a.text!=="M"&&a.text!=="m")return MP("M0,0"+e);i++,o=m4[a.text],n=a.text}else xP(a,vP)?o=m4[n]:(i++,o=m4[a.text],n=a.text);if(!(i+od%2?h+r:h+t));a.push({key:"C",data:u}),t=u[4],r=u[5];break}case"Q":a.push({key:"Q",data:[...l]}),t=l[2],r=l[3];break;case"q":{let u=l.map(((h,d)=>d%2?h+r:h+t));a.push({key:"Q",data:u}),t=u[2],r=u[3];break}case"A":a.push({key:"A",data:[...l]}),t=l[5],r=l[6];break;case"a":t+=l[5],r+=l[6],a.push({key:"A",data:[l[0],l[1],l[2],l[3],l[4],t,r]});break;case"H":a.push({key:"H",data:[...l]}),t=l[0];break;case"h":t+=l[0],a.push({key:"H",data:[t]});break;case"V":a.push({key:"V",data:[...l]}),r=l[0];break;case"v":r+=l[0],a.push({key:"V",data:[r]});break;case"S":a.push({key:"S",data:[...l]}),t=l[2],r=l[3];break;case"s":{let u=l.map(((h,d)=>d%2?h+r:h+t));a.push({key:"S",data:u}),t=u[2],r=u[3];break}case"T":a.push({key:"T",data:[...l]}),t=l[0],r=l[1];break;case"t":t+=l[0],r+=l[1],a.push({key:"T",data:[t,r]});break;case"Z":case"z":a.push({key:"Z",data:[]}),t=n,r=i}return a}function Tse(e){let t=[],r="",n=0,i=0,a=0,o=0,l=0,u=0;for(let{key:h,data:d}of e){switch(h){case"M":t.push({key:"M",data:[...d]}),[n,i]=d,[a,o]=d;break;case"C":t.push({key:"C",data:[...d]}),n=d[4],i=d[5],l=d[2],u=d[3];break;case"L":t.push({key:"L",data:[...d]}),[n,i]=d;break;case"H":n=d[0],t.push({key:"L",data:[n,i]});break;case"V":i=d[0],t.push({key:"L",data:[n,i]});break;case"S":{let f=0,p=0;r==="C"||r==="S"?(f=n+(n-l),p=i+(i-u)):(f=n,p=i),t.push({key:"C",data:[f,p,...d]}),l=d[0],u=d[1],n=d[2],i=d[3];break}case"T":{let[f,p]=d,m=0,g=0;r==="Q"||r==="T"?(m=n+(n-l),g=i+(i-u)):(m=n,g=i);let y=n+2*(m-n)/3,v=i+2*(g-i)/3,x=f+2*(m-f)/3,b=p+2*(g-p)/3;t.push({key:"C",data:[y,v,x,b,f,p]}),l=m,u=g,n=f,i=p;break}case"Q":{let[f,p,m,g]=d,y=n+2*(f-n)/3,v=i+2*(p-i)/3,x=m+2*(f-m)/3,b=g+2*(p-g)/3;t.push({key:"C",data:[y,v,x,b,m,g]}),l=f,u=p,n=m,i=g;break}case"A":{let f=Math.abs(d[0]),p=Math.abs(d[1]),m=d[2],g=d[3],y=d[4],v=d[5],x=d[6];f===0||p===0?(t.push({key:"C",data:[n,i,v,x,v,x]}),n=v,i=x):(n!==v||i!==x)&&(Cse(n,i,v,x,f,p,m,g,y).forEach((function(b){t.push({key:"C",data:b})})),n=v,i=x);break}case"Z":t.push({key:"Z",data:[]}),n=a,i=o}r=h}return t}function w2(e,t,r){return[e*Math.cos(r)-t*Math.sin(r),e*Math.sin(r)+t*Math.cos(r)]}function Cse(e,t,r,n,i,a,o,l,u,h){let d=(f=o,Math.PI*f/180);var f;let p=[],m=0,g=0,y=0,v=0;if(h)[m,g,y,v]=h;else{[e,t]=w2(e,t,-d),[r,n]=w2(r,n,-d);let A=(e-r)/2,I=(t-n)/2,D=A*A/(i*i)+I*I/(a*a);D>1&&(D=Math.sqrt(D),i*=D,a*=D);let L=i*i,B=a*a,O=L*B-L*I*I-B*A*A,$=L*I*I+B*A*A,W=(l===u?-1:1)*Math.sqrt(Math.abs(O/$));y=W*i*I/a+(e+r)/2,v=W*-a*A/i+(t+n)/2,m=Math.asin(parseFloat(((t-v)/a).toFixed(9))),g=Math.asin(parseFloat(((n-v)/a).toFixed(9))),eg&&(m-=2*Math.PI),!u&&g>m&&(g-=2*Math.PI)}let x=g-m;if(Math.abs(x)>120*Math.PI/180){let A=g,I=r,D=n;g=u&&g>m?m+120*Math.PI/180*1:m+120*Math.PI/180*-1,p=Cse(r=y+i*Math.cos(g),n=v+a*Math.sin(g),I,D,i,a,o,0,u,[g,A,y,v])}x=g-m;let b=Math.cos(m),T=Math.sin(m),w=Math.cos(g),C=Math.sin(g),S=Math.tan(x/4),k=4/3*i*S,R=4/3*a*S,M=[e,t],P=[e+k*T,t-R*b],N=[r+k*C,n-R*w],_=[r,n];if(P[0]=2*M[0]-P[0],P[1]=2*M[1]-P[1],h)return[P,N,_].concat(p);{p=[P,N,_].concat(p);let A=[];for(let I=0;I2){let i=[];for(let a=0;a2*Math.PI&&(m=0,g=2*Math.PI);let y=2*Math.PI/u.curveStepCount,v=Math.min(y/2,(g-m)/2),x=vse(v,h,d,f,p,m,g,1,u);if(!u.disableMultiStroke){let b=vse(v,h,d,f,p,m,g,1.5,u);x.push(...b)}return o&&(l?x.push(...Jd(h,d,h+f*Math.cos(m),d+p*Math.sin(m),u),...Jd(h,d,h+f*Math.cos(g),d+p*Math.sin(g),u)):x.push({op:"lineTo",data:[h,d]},{op:"lineTo",data:[h+f*Math.cos(m),d+p*Math.sin(m)]})),{type:"path",ops:x}}function mse(e,t){let r=Tse(bse(MP(e))),n=[],i=[0,0],a=[0,0];for(let{key:o,data:l}of r)switch(o){case"M":a=[l[0],l[1]],i=[l[0],l[1]];break;case"L":n.push(...Jd(a[0],a[1],l[0],l[1],t)),a=[l[0],l[1]];break;case"C":{let[u,h,d,f,p,m]=l;n.push(...CKe(u,h,d,f,p,m,a,t)),a=[p,m];break}case"Z":n.push(...Jd(a[0],a[1],i[0],i[1],t)),a=[i[0],i[1]]}return{type:"path",ops:n}}function bP(e,t){let r=[];for(let n of e)if(n.length){let i=t.maxRandomnessOffset||0,a=n.length;if(a>2){r.push({op:"move",data:[n[0][0]+Ar(i,t),n[0][1]+Ar(i,t)]});for(let o=1;o500?.4:-.0016668*u+1.233334;let d=i.maxRandomnessOffset||0;d*d*100>l&&(d=u/10);let f=d/2,p=.2+.2*kse(i),m=i.bowing*i.maxRandomnessOffset*(n-t)/200,g=i.bowing*i.maxRandomnessOffset*(e-r)/200;m=Ar(m,i,h),g=Ar(g,i,h);let y=[],v=s(()=>Ar(f,i,h),"M"),x=s(()=>Ar(d,i,h),"k"),b=i.preserveVertices;return a&&(o?y.push({op:"move",data:[e+(b?0:v()),t+(b?0:v())]}):y.push({op:"move",data:[e+(b?0:Ar(d,i,h)),t+(b?0:Ar(d,i,h))]})),o?y.push({op:"bcurveTo",data:[m+e+(r-e)*p+v(),g+t+(n-t)*p+v(),m+e+2*(r-e)*p+v(),g+t+2*(n-t)*p+v(),r+(b?0:v()),n+(b?0:v())]}):y.push({op:"bcurveTo",data:[m+e+(r-e)*p+x(),g+t+(n-t)*p+x(),m+e+2*(r-e)*p+x(),g+t+2*(n-t)*p+x(),r+(b?0:x()),n+(b?0:x())]}),y}function g4(e,t,r){if(!e.length)return[];let n=[];n.push([e[0][0]+Ar(t,r),e[0][1]+Ar(t,r)]),n.push([e[0][0]+Ar(t,r),e[0][1]+Ar(t,r)]);for(let i=1;i3){let a=[],o=1-r.curveTightness;i.push({op:"move",data:[e[1][0],e[1][1]]});for(let l=1;l+21&&i.push(l)):i.push(l),i.push(e[t+3])}else{let u=e[t+0],h=e[t+1],d=e[t+2],f=e[t+3],p=wm(u,h,.5),m=wm(h,d,.5),g=wm(d,f,.5),y=wm(p,m,.5),v=wm(m,g,.5),x=wm(y,v,.5);LP([u,p,y,x],0,r,i),LP([x,v,g,f],0,r,i)}var a,o;return i}function wKe(e,t){return C4(e,0,e.length,t)}function C4(e,t,r,n,i){let a=i||[],o=e[t],l=e[r-1],u=0,h=1;for(let d=t+1;du&&(u=f,h=d)}return Math.sqrt(u)>n?(C4(e,t,h+1,n,a),C4(e,h,r,n,a)):(a.length||a.push(o),a.push(l)),a}function TP(e,t=.15,r){let n=[],i=(e.length-1)/3;for(let a=0;a0?C4(n,0,n.length,r):n}var E2,CP,SP,wP,kP,EP,co,AP,xKe,vP,dse,m4,bKe,Uo,Py,DP,y4,IP,ot,Kt=F(()=>{"use strict";s(yP,"t");s(yKe,"e");s(vKe,"s");s(A2,"n");E2=class{static{s(this,"o")}constructor(t){this.helper=t}fillPolygons(t,r){return this._fillPolygons(t,r)}_fillPolygons(t,r){let n=A2(t,r);return{type:"fillSketch",ops:this.renderLines(n,r)}}renderLines(t,r){let n=[];for(let i of t)n.push(...this.helper.doubleLineOps(i[0][0],i[0][1],i[1][0],i[1][1],r));return n}};s(S4,"a");CP=class extends E2{static{s(this,"h")}fillPolygons(t,r){let n=r.hachureGap;n<0&&(n=4*r.strokeWidth),n=Math.max(n,.1);let i=A2(t,Object.assign({},r,{hachureGap:n})),a=Math.PI/180*r.hachureAngle,o=[],l=.5*n*Math.cos(a),u=.5*n*Math.sin(a);for(let[h,d]of i)S4([h,d])&&o.push([[h[0]-l,h[1]+u],[...d]],[[h[0]+l,h[1]-u],[...d]]);return{type:"fillSketch",ops:this.renderLines(o,r)}}},SP=class extends E2{static{s(this,"r")}fillPolygons(t,r){let n=this._fillPolygons(t,r),i=Object.assign({},r,{hachureAngle:r.hachureAngle+90}),a=this._fillPolygons(t,i);return n.ops=n.ops.concat(a.ops),n}},wP=class{static{s(this,"i")}constructor(t){this.helper=t}fillPolygons(t,r){let n=A2(t,r=Object.assign({},r,{hachureAngle:0}));return this.dotsOnLines(n,r)}dotsOnLines(t,r){let n=[],i=r.hachureGap;i<0&&(i=4*r.strokeWidth),i=Math.max(i,.1);let a=r.fillWeight;a<0&&(a=r.strokeWidth/2);let o=i/4;for(let l of t){let u=S4(l),h=u/i,d=Math.ceil(h)-1,f=u-d*i,p=(l[0][0]+l[1][0])/2-i/4,m=Math.min(l[0][1],l[1][1]);for(let g=0;g{let l=S4(o),u=Math.floor(l/(n+i)),h=(l+i-u*(n+i))/2,d=o[0],f=o[1];d[0]>f[0]&&(d=o[1],f=o[0]);let p=Math.atan((f[1]-d[1])/(f[0]-d[0]));for(let m=0;m{let o=S4(a),l=Math.round(o/(2*r)),u=a[0],h=a[1];u[0]>h[0]&&(u=a[1],h=a[0]);let d=Math.atan((h[1]-u[1])/(h[0]-u[0]));for(let f=0;f2*Math.PI&&(k=0,R=2*Math.PI);let M=(R-k)/b.curveStepCount,P=[];for(let N=k;N<=R;N+=M)P.push([T+C*Math.cos(N),w+S*Math.sin(N)]);return P.push([T+C*Math.cos(R),w+S*Math.sin(R)]),P.push([T,w]),Ny([P],b)})(t,r,n,i,a,o,h));return h.stroke!==Uo&&d.push(f),this._d("arc",d,h)}curve(t,r){let n=this._o(r),i=[],a=fse(t,n);if(n.fill&&n.fill!==Uo)if(n.fillStyle==="solid"){let o=fse(t,Object.assign(Object.assign({},n),{disableMultiStroke:!0,roughness:n.roughness?n.roughness+n.fillShapeRoughnessGain:0}));i.push({type:"fillPath",ops:this._mergedShape(o.ops)})}else{let o=[],l=t;if(l.length){let u=typeof l[0][0]=="number"?[l]:l;for(let h of u)h.length<3?o.push(...h):h.length===3?o.push(...TP(xse([h[0],h[0],h[1],h[2]]),10,(1+n.roughness)/2)):o.push(...TP(xse(h),10,(1+n.roughness)/2))}o.length&&i.push(Ny([o],n))}return n.stroke!==Uo&&i.push(a),this._d("curve",i,n)}polygon(t,r){let n=this._o(r),i=[],a=v4(t,!0,n);return n.fill&&(n.fillStyle==="solid"?i.push(bP([t],n)):i.push(Ny([t],n))),n.stroke!==Uo&&i.push(a),this._d("polygon",i,n)}path(t,r){let n=this._o(r),i=[];if(!t)return this._d("path",i,n);t=(t||"").replace(/\n/g," ").replace(/(-\s)/g,"-").replace("/(ss)/g"," ");let a=n.fill&&n.fill!=="transparent"&&n.fill!==Uo,o=n.stroke!==Uo,l=!!(n.simplification&&n.simplification<1),u=(function(d,f,p){let m=Tse(bse(MP(d))),g=[],y=[],v=[0,0],x=[],b=s(()=>{x.length>=4&&y.push(...TP(x,f)),x=[]},"i"),T=s(()=>{b(),y.length&&(g.push(y),y=[])},"c");for(let{key:C,data:S}of m)switch(C){case"M":T(),v=[S[0],S[1]],y.push(v);break;case"L":b(),y.push([S[0],S[1]]);break;case"C":if(!x.length){let k=y.length?y[y.length-1]:v;x.push([k[0],k[1]])}x.push([S[0],S[1]]),x.push([S[2],S[3]]),x.push([S[4],S[5]]);break;case"Z":b(),y.push([v[0],v[1]])}if(T(),!p)return g;let w=[];for(let C of g){let S=wKe(C,p);S.length&&w.push(S)}return w})(t,1,l?4-4*(n.simplification||1):(1+n.roughness)/2),h=mse(t,n);if(a)if(n.fillStyle==="solid")if(u.length===1){let d=mse(t,Object.assign(Object.assign({},n),{disableMultiStroke:!0,roughness:n.roughness?n.roughness+n.fillShapeRoughnessGain:0}));i.push({type:"fillPath",ops:this._mergedShape(d.ops)})}else i.push(bP(u,n));else i.push(Ny(u,n));return o&&(l?u.forEach((d=>{i.push(v4(d,!1,n))})):i.push(h)),this._d("path",i,n)}opsToPath(t,r){let n="";for(let i of t.ops){let a=typeof r=="number"&&r>=0?i.data.map((o=>+o.toFixed(r))):i.data;switch(i.op){case"move":n+=`M${a[0]} ${a[1]} `;break;case"bcurveTo":n+=`C${a[0]} ${a[1]}, ${a[2]} ${a[3]}, ${a[4]} ${a[5]} `;break;case"lineTo":n+=`L${a[0]} ${a[1]} `}}return n.trim()}toPaths(t){let r=t.sets||[],n=t.options||this.defaultOptions,i=[];for(let a of r){let o=null;switch(a.type){case"path":o={d:this.opsToPath(a),stroke:n.stroke,strokeWidth:n.strokeWidth,fill:Uo};break;case"fillPath":o={d:this.opsToPath(a),stroke:Uo,strokeWidth:0,fill:n.fill||Uo};break;case"fillSketch":o=this.fillSketch(a,n)}o&&i.push(o)}return i}fillSketch(t,r){let n=r.fillWeight;return n<0&&(n=r.strokeWidth/2),{d:this.opsToPath(t),stroke:r.fill||Uo,strokeWidth:n,fill:Uo}}_mergedShape(t){return t.filter(((r,n)=>n===0||r.op!=="move"))}},DP=class{static{s(this,"st")}constructor(t,r){this.canvas=t,this.ctx=this.canvas.getContext("2d"),this.gen=new Py(r)}draw(t){let r=t.sets||[],n=t.options||this.getDefaultOptions(),i=this.ctx,a=t.options.fixedDecimalPlaceDigits;for(let o of r)switch(o.type){case"path":i.save(),i.strokeStyle=n.stroke==="none"?"transparent":n.stroke,i.lineWidth=n.strokeWidth,n.strokeLineDash&&i.setLineDash(n.strokeLineDash),n.strokeLineDashOffset&&(i.lineDashOffset=n.strokeLineDashOffset),this._drawToContext(i,o,a),i.restore();break;case"fillPath":{i.save(),i.fillStyle=n.fill||"";let l=t.shape==="curve"||t.shape==="polygon"||t.shape==="path"?"evenodd":"nonzero";this._drawToContext(i,o,a,l),i.restore();break}case"fillSketch":this.fillSketch(i,o,n)}}fillSketch(t,r,n){let i=n.fillWeight;i<0&&(i=n.strokeWidth/2),t.save(),n.fillLineDash&&t.setLineDash(n.fillLineDash),n.fillLineDashOffset&&(t.lineDashOffset=n.fillLineDashOffset),t.strokeStyle=n.fill||"",t.lineWidth=i,this._drawToContext(t,r,n.fixedDecimalPlaceDigits),t.restore()}_drawToContext(t,r,n,i="nonzero"){t.beginPath();for(let a of r.ops){let o=typeof n=="number"&&n>=0?a.data.map((l=>+l.toFixed(n))):a.data;switch(a.op){case"move":t.moveTo(o[0],o[1]);break;case"bcurveTo":t.bezierCurveTo(o[0],o[1],o[2],o[3],o[4],o[5]);break;case"lineTo":t.lineTo(o[0],o[1])}}r.type==="fillPath"?t.fill(i):t.stroke()}get generator(){return this.gen}getDefaultOptions(){return this.gen.defaultOptions}line(t,r,n,i,a){let o=this.gen.line(t,r,n,i,a);return this.draw(o),o}rectangle(t,r,n,i,a){let o=this.gen.rectangle(t,r,n,i,a);return this.draw(o),o}ellipse(t,r,n,i,a){let o=this.gen.ellipse(t,r,n,i,a);return this.draw(o),o}circle(t,r,n,i){let a=this.gen.circle(t,r,n,i);return this.draw(a),a}linearPath(t,r){let n=this.gen.linearPath(t,r);return this.draw(n),n}polygon(t,r){let n=this.gen.polygon(t,r);return this.draw(n),n}arc(t,r,n,i,a,o,l=!1,u){let h=this.gen.arc(t,r,n,i,a,o,l,u);return this.draw(h),h}curve(t,r){let n=this.gen.curve(t,r);return this.draw(n),n}path(t,r){let n=this.gen.path(t,r);return this.draw(n),n}},y4="http://www.w3.org/2000/svg",IP=class{static{s(this,"ot")}constructor(t,r){this.svg=t,this.gen=new Py(r)}draw(t){let r=t.sets||[],n=t.options||this.getDefaultOptions(),i=this.svg.ownerDocument||window.document,a=i.createElementNS(y4,"g"),o=t.options.fixedDecimalPlaceDigits;for(let l of r){let u=null;switch(l.type){case"path":u=i.createElementNS(y4,"path"),u.setAttribute("d",this.opsToPath(l,o)),u.setAttribute("stroke",n.stroke),u.setAttribute("stroke-width",n.strokeWidth+""),u.setAttribute("fill","none"),n.strokeLineDash&&u.setAttribute("stroke-dasharray",n.strokeLineDash.join(" ").trim()),n.strokeLineDashOffset&&u.setAttribute("stroke-dashoffset",`${n.strokeLineDashOffset}`);break;case"fillPath":u=i.createElementNS(y4,"path"),u.setAttribute("d",this.opsToPath(l,o)),u.setAttribute("stroke","none"),u.setAttribute("stroke-width","0"),u.setAttribute("fill",n.fill||""),t.shape!=="curve"&&t.shape!=="polygon"||u.setAttribute("fill-rule","evenodd");break;case"fillSketch":u=this.fillSketch(i,l,n)}u&&a.appendChild(u)}return a}fillSketch(t,r,n){let i=n.fillWeight;i<0&&(i=n.strokeWidth/2);let a=t.createElementNS(y4,"path");return a.setAttribute("d",this.opsToPath(r,n.fixedDecimalPlaceDigits)),a.setAttribute("stroke",n.fill||""),a.setAttribute("stroke-width",i+""),a.setAttribute("fill","none"),n.fillLineDash&&a.setAttribute("stroke-dasharray",n.fillLineDash.join(" ").trim()),n.fillLineDashOffset&&a.setAttribute("stroke-dashoffset",`${n.fillLineDashOffset}`),a}get generator(){return this.gen}getDefaultOptions(){return this.gen.defaultOptions}opsToPath(t,r){return this.gen.opsToPath(t,r)}line(t,r,n,i,a){let o=this.gen.line(t,r,n,i,a);return this.draw(o)}rectangle(t,r,n,i,a){let o=this.gen.rectangle(t,r,n,i,a);return this.draw(o)}ellipse(t,r,n,i,a){let o=this.gen.ellipse(t,r,n,i,a);return this.draw(o)}circle(t,r,n,i){let a=this.gen.circle(t,r,n,i);return this.draw(a)}linearPath(t,r){let n=this.gen.linearPath(t,r);return this.draw(n)}polygon(t,r){let n=this.gen.polygon(t,r);return this.draw(n)}arc(t,r,n,i,a,o,l=!1,u){let h=this.gen.arc(t,r,n,i,a,o,l,u);return this.draw(h)}curve(t,r){let n=this.gen.curve(t,r);return this.draw(n)}path(t,r){let n=this.gen.path(t,r);return this.draw(n)}},ot={canvas:s((e,t)=>new DP(e,t),"canvas"),svg:s((e,t)=>new IP(e,t),"svg"),generator:s(e=>new Py(e),"generator"),newSeed:s(()=>Py.newSeed(),"newSeed")}});function Ese(e,t){let{labelStyles:r}=st(t);t.labelStyle=r;let n=vt(t),i=n;n||(i="anchor");let a=e.insert("g").attr("class",i).attr("id",t.domId||t.id),o=1,{cssStyles:l}=t,u=ot.svg(a),h=ft(t,{fill:"black",stroke:"none",fillStyle:"solid"});t.look!=="handDrawn"&&(h.roughness=0);let d=u.circle(0,0,o*2,h),f=a.insert(()=>d,":first-child");return f.attr("class","anchor").attr("style",un(l)),ht(t,f),t.intersect=function(p){return se.info("Circle intersect",t,o,p),it.circle(t,o,p)},a}var Ase=F(()=>{"use strict";St();Ht();Zt();Xt();Kt();er();s(Ese,"anchor")});function Rse(e,t,r,n,i,a,o){let u=(e+r)/2,h=(t+n)/2,d=Math.atan2(n-t,r-e),f=(r-e)/2,p=(n-t)/2,m=f/i,g=p/a,y=Math.sqrt(m**2+g**2);if(y>1)throw new Error("The given radii are too small to create an arc between the points.");let v=Math.sqrt(1-y**2),x=u+v*a*Math.sin(d)*(o?-1:1),b=h-v*i*Math.cos(d)*(o?-1:1),T=Math.atan2((t-b)/a,(e-x)/i),C=Math.atan2((n-b)/a,(r-x)/i)-T;o&&C<0&&(C+=2*Math.PI),!o&&C>0&&(C-=2*Math.PI);let S=[];for(let k=0;k<20;k++){let R=k/19,M=T+R*C,P=x+i*Math.cos(M),N=b+a*Math.sin(M);S.push({x:P,y:N})}return S}function kKe(e,t,r){let[n,i]=[t,r].sort((a,o)=>o-a);return i*(1-Math.sqrt(1-(e/n/2)**2))}async function _se(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?12:i,l=s(M=>M+o,"calcTotalHeight"),u=s(M=>{let P=M/2;return[P/(2.5+M/50),P]},"calcEllipseRadius"),{shapeSvg:h,bbox:d}=await yt(e,t,vt(t)),f=l(t?.height?t?.height:d.height),[p,m]=u(f),g=kKe(f,p,m),v=(t?.width?t?.width:d.width)+a*2+g-g,x=f,{cssStyles:b}=t,T=[{x:v/2,y:-x/2},{x:-v/2,y:-x/2},...Rse(-v/2,-x/2,-v/2,x/2,p,m,!1),{x:v/2,y:x/2},...Rse(v/2,x/2,v/2,-x/2,p,m,!0)],w=ot.svg(h),C=ft(t,{});t.look!=="handDrawn"&&(C.roughness=0,C.fillStyle="solid");let S=hr(T),k=w.path(S,C),R=h.insert(()=>k,":first-child");return R.attr("class","basic label-container outer-path"),b&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",b),n&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",n),R.attr("transform",`translate(${p/2}, 0)`),ht(t,R),t.intersect=function(M){return it.polygon(t,T,M)},h}var Lse=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(Rse,"generateArcPoints");s(kKe,"calculateArcSagitta");s(_se,"bowTieRect")});async function Dse(e,t,{config:{themeVariables:r}}){let{labelStyles:n,nodeStyles:i}=st(t);t.labelStyle=n;let{shapeSvg:a,bbox:o,label:l}=await yt(e,t,vt(t)),u=r?.nodeBorder??r?.lineColor??"currentColor",h=t.padding??12,d=Math.max(o.width+h*2,t.width??0,80),f=Math.max(Math.min(d*.08,12),5),p=Math.max(o.height+h*2+f,t.height??0),m=-p/2+f,g=p/2,y=d*.72,v=[`M${-d/2},${m}`,`L${-y/2},${g}`,`A${y/2},${f} 0 0 0 ${y/2},${g}`,`L${d/2},${m}`,`A${d/2},${f} 0 0 0 ${-d/2},${m}`,"Z"].join(" "),{cssStyles:x}=t,b=a.insert("g",":first-child").attr("class","basic label-container");if(t.look==="handDrawn"){let R=ot.svg(a).path(v,ft(t,{}));b.node()?.appendChild(R),x&&b.attr("style",x)}else b.append("path").attr("d",v).attr("style",i);b.append("ellipse").attr("cx",0).attr("cy",m).attr("rx",d/2).attr("ry",f).attr("style",`fill:none;stroke:${u};stroke-width:1px`),ht(t,b);let T=m+(g-m)/2;l.attr("transform",`translate(${-(o.width/2)-(o.x-(o.left??0))}, ${T-o.height/2-(o.y-(o.top??0))})`);let w=12,C=s((k,R,M)=>Array.from({length:w+1},(P,N)=>{let _=Math.PI-N*Math.PI/w;return{x:k*Math.cos(_),y:R+M*f*Math.sin(_)}}),"arc"),S=[...C(d/2,m,-1),...C(y/2,g,1).reverse()];return t.intersect=function(k){return it.polygon(t,S,k)},a}var Ise=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(Dse,"bucket")});var Mi,ic=F(()=>{"use strict";Mi=s((e,t,r,n,i)=>["M",e+i,t,"H",e+r-i,"A",i,i,0,0,1,e+r,t+i,"V",t+n-i,"A",i,i,0,0,1,e+r-i,t+n,"H",e+i,"A",i,i,0,0,1,e,t+n-i,"V",t+i,"A",i,i,0,0,1,e+i,t,"Z"].join(" "),"createRoundedRectPathD")});async function Mse(e,t){let{themeVariables:r}=Pe(),n=r.clusterBkg,i=r.clusterBorder,{nodeStyles:a}=st(t),{shapeSvg:o,bbox:l}=await yt(e,t,vt(t)),u=t.padding??8,h=l.height,d=Math.max(l.width+u*2,EKe,t?.width??0),f=Math.max(h+PP+NP+u*2,t?.height??0),p=-d/2,m=-f/2,g=-(PP+NP)/2,y=o.select(".label");y&&(t.useHtmlLabels??ai(Pe())?y.attr("transform",`translate(${-l.width/2}, ${-l.height/2+g})`):y.attr("transform",`translate(0, ${-l.height/2+g})`));let v;if(t.look==="handDrawn"){let C=ot.svg(o),S=ft(t,{fill:n,stroke:i,fillStyle:"solid"}),k=C.path(Mi(p,m,d,f,OP),S);v=o.insert(()=>k,":first-child"),v.attr("class","basic label-container collapsed-group").attr("style",un(t.cssStyles))}else v=o.insert("rect",":first-child"),v.attr("class","basic label-container collapsed-group").attr("style",a).attr("rx",OP).attr("ry",OP).attr("x",p).attr("y",m).attr("width",d).attr("height",f).attr("fill",n).attr("stroke",i);let x=m+u+h+PP;o.append("line").attr("class","collapsed-separator").attr("x1",p+8).attr("y1",x).attr("x2",p+d-8).attr("y2",x).attr("stroke",i).attr("stroke-dasharray","3, 3");let b=x+NP/2,T=2.5,w=10;for(let C=-1;C<=1;C++)o.append("circle").attr("class","collapsed-indicator").attr("cx",C*w).attr("cy",b).attr("r",T).attr("fill",i);return ht(t,v),t.calcIntersect=function(C,S){return it.rect(C,S)},t.intersect=function(C){return it.rect(t,C)},o}var NP,PP,EKe,OP,Nse=F(()=>{"use strict";Jt();br();Ht();Zt();ic();Xt();Kt();er();NP=20,PP=8,EKe=80,OP=8;s(Mse,"collapsedGroup")});function Na(e,t,r,n){return e.insert("polygon",":first-child").attr("points",n.map(function(i){return i.x+","+i.y}).join(" ")).attr("class","label-container").attr("transform","translate("+-t/2+","+r/2+")")}var iu=F(()=>{"use strict";s(Na,"insertPolygonShape")});async function Ose(e,t){let r=t,{shapeSvg:n,bbox:i}=await yt(e,r,vt(r)),a=r.padding??0,o=i.height+2*a,l=o/2,u=i.width+2*l+a,h=r.width??0,f=r.positioned&&(r.widthInColumns??1)>1&&h>u?h:u,p=LKe(r.directions??[],i,r,f),m=Na(n,f,o,p);return m.attr("style",r.style??null),ht(r,m),r.intersect=function(g){return it.polygon(r,p,g)},n}var AKe,BP,RKe,_Ke,Pse,LKe,Bse=F(()=>{"use strict";Zt();iu();Ht();AKe=["right","left","up","down"],BP="point",RKe=s(e=>{let t=new Set;for(let r of e)switch(r){case"x":t.add("right"),t.add("left");break;case"y":t.add("up"),t.add("down");break;default:t.add(r);break}return t},"expandAndDeduplicateDirections"),_Ke=s(e=>AKe.filter(t=>e.has(t)).join("|")||BP,"getDirectionKey"),Pse={"right|left|up|down":s(({height:e,midpoint:t,padding:r,width:n})=>[{x:0,y:0},{x:t,y:0},{x:n/2,y:2*r},{x:n-t,y:0},{x:n,y:0},{x:n,y:-e/3},{x:n+2*r,y:-e/2},{x:n,y:-2*e/3},{x:n,y:-e},{x:n-t,y:-e},{x:n/2,y:-e-2*r},{x:t,y:-e},{x:0,y:-e},{x:0,y:-2*e/3},{x:-2*r,y:-e/2},{x:0,y:-e/3}],"right|left|up|down"),"right|left|up":s(({height:e,midpoint:t,width:r})=>[{x:t,y:0},{x:r-t,y:0},{x:r,y:-e/2},{x:r-t,y:-e},{x:t,y:-e},{x:0,y:-e/2}],"right|left|up"),"right|left|down":s(({height:e,midpoint:t,width:r})=>[{x:0,y:0},{x:t,y:-e},{x:r-t,y:-e},{x:r,y:0}],"right|left|down"),"right|up|down":s(({height:e,midpoint:t,width:r})=>[{x:0,y:0},{x:r,y:-t},{x:r,y:-e+t},{x:0,y:-e}],"right|up|down"),"left|up|down":s(({height:e,midpoint:t,width:r})=>[{x:r,y:0},{x:0,y:-t},{x:0,y:-e+t},{x:r,y:-e}],"left|up|down"),"right|left":s(({height:e,midpoint:t,padding:r,width:n})=>[{x:t,y:0},{x:t,y:-r},{x:n-t,y:-r},{x:n-t,y:0},{x:n,y:-e/2},{x:n-t,y:-e},{x:n-t,y:-e+r},{x:t,y:-e+r},{x:t,y:-e},{x:0,y:-e/2}],"right|left"),"up|down":s(({height:e,midpoint:t,padding:r,width:n})=>[{x:n/2,y:0},{x:0,y:-r},{x:t,y:-r},{x:t,y:-e+r},{x:0,y:-e+r},{x:n/2,y:-e},{x:n,y:-e+r},{x:n-t,y:-e+r},{x:n-t,y:-r},{x:n,y:-r}],"up|down"),"right|up":s(({height:e,midpoint:t,width:r})=>[{x:0,y:0},{x:r,y:-t},{x:0,y:-e}],"right|up"),"right|down":s(({height:e,width:t})=>[{x:0,y:0},{x:t,y:0},{x:0,y:-e}],"right|down"),"left|up":s(({height:e,midpoint:t,width:r})=>[{x:r,y:0},{x:0,y:-t},{x:r,y:-e}],"left|up"),"left|down":s(({height:e,width:t})=>[{x:t,y:0},{x:0,y:0},{x:t,y:-e}],"left|down"),right:s(({height:e,midpoint:t,padding:r,width:n})=>[{x:t,y:-r},{x:t,y:-r},{x:n-t,y:-r},{x:n-t,y:0},{x:n,y:-e/2},{x:n-t,y:-e},{x:n-t,y:-e+r},{x:t,y:-e+r},{x:t,y:-e+r}],"right"),left:s(({height:e,midpoint:t,padding:r,width:n})=>[{x:t,y:0},{x:t,y:-r},{x:n-t,y:-r},{x:n-t,y:-e+r},{x:t,y:-e+r},{x:t,y:-e},{x:0,y:-e/2}],"left"),up:s(({height:e,midpoint:t,padding:r,width:n})=>[{x:t,y:-r},{x:t,y:-e+r},{x:0,y:-e+r},{x:n/2,y:-e},{x:n,y:-e+r},{x:n-t,y:-e+r},{x:n-t,y:-r}],"up"),down:s(({height:e,midpoint:t,padding:r,width:n})=>[{x:n/2,y:0},{x:0,y:-r},{x:t,y:-r},{x:t,y:-e+r},{x:n-t,y:-e+r},{x:n-t,y:-r},{x:n,y:-r}],"down"),[BP]:()=>[{x:0,y:0}]},LKe=s((e,t,r,n)=>{let i=RKe(e),a=(r.padding??0)/2,o=t.height+4*a,l=o/2,u=n??t.width+2*l+2*a,h=_Ke(i);return(Pse[h]??Pse[BP])({height:o,midpoint:l,padding:a,width:u})},"getArrowPoints");s(Ose,"block_arrow")});async function $se(e,t,{config:{themeVariables:r}}){let{labelStyles:n,nodeStyles:i}=st(t);t.labelStyle=n;let{shapeSvg:a,bbox:o,label:l}=await yt(e,t,vt(t)),u=r?.nodeBorder??r?.lineColor??"currentColor",h=t.padding??12,d=18,f=12,p=Math.max(o.width+h*2,t.width??0,90),m=Math.max(o.height+h*2+d,t.height??0),g=-m/2,{cssStyles:y}=t,v=a.insert("g",":first-child").attr("class","basic label-container");if(t.look==="handDrawn"){let T=ot.svg(a).path(Mi(-p/2,g,p,m,f),ft(t,{}));v.node()?.appendChild(T),y&&v.attr("style",y)}else v.append("rect").attr("x",-p/2).attr("y",g).attr("width",p).attr("height",m).attr("rx",f).attr("ry",f).attr("style",i);v.append("line").attr("x1",-p/2).attr("y1",g+d).attr("x2",p/2).attr("y2",g+d).attr("style",`stroke:${u};stroke-width:1px`);for(let b=0;b<3;b++)v.append("circle").attr("cx",-p/2+12+b*9).attr("cy",g+d/2).attr("r",2.5).attr("style",`fill:${u};stroke:none`);v.append("rect").attr("class","browser-address-bar").attr("x",-p/2+44).attr("y",g+4).attr("width",Math.max(p-56,10)).attr("height",d-8).attr("rx",3).attr("ry",3).attr("style",`fill:none;stroke:${u};stroke-width:1px;opacity:0.6`),ht(t,v);let x=g+d+(m-d)/2;return l.attr("transform",`translate(${-(o.width/2)-(o.x-(o.left??0))}, ${x-o.height/2-(o.y-(o.top??0))})`),t.intersect=function(b){return it.rect(t,b)},a}var Fse=F(()=>{"use strict";Ht();Zt();ic();Xt();Kt();s($se,"browser")});async function Gse(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?28:i,o=t.look==="neo"?24:i,{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=(t?.width??u.width)+(t.look==="neo"?a*2:a+w4),d=(t?.height??u.height)+(t.look==="neo"?o*2:o),f=0,p=h,m=-d,g=0,y=[{x:f+w4,y:m},{x:p,y:m},{x:p,y:g},{x:f,y:g},{x:f,y:m+w4},{x:f+w4,y:m}],v,{cssStyles:x}=t;if(t.look==="handDrawn"){let b=ot.svg(l),T=ft(t,{}),w=hr(y),C=b.path(w,T);v=l.insert(()=>C,":first-child").attr("transform",`translate(${-h/2}, ${d/2})`),x&&v.attr("style",x)}else v=Na(l,h,d,y);return n&&v.attr("style",n),ht(t,v),t.intersect=function(b){return it.polygon(t,y,b)},l}var w4,zse=F(()=>{"use strict";Ht();Zt();Xt();Kt();iu();Ht();w4=12;s(Gse,"card")});function Vse(e,t){let{nodeStyles:r}=st(t);t.label="";let n=e.insert("g").attr("class",vt(t)).attr("id",t.domId??t.id),{cssStyles:i}=t,a=Math.max(28,t.width??0),o=[{x:0,y:a/2},{x:a/2,y:0},{x:0,y:-a/2},{x:-a/2,y:0}],l=ot.svg(n),u=ft(t,{});t.look!=="handDrawn"&&(u.roughness=0,u.fillStyle="solid");let h=hr(o),d=l.path(h,u),f=n.insert(()=>d,":first-child");return i&&t.look!=="handDrawn"&&f.selectAll("path").attr("style",i),r&&t.look!=="handDrawn"&&f.selectAll("path").attr("style",r),t.width=28,t.height=28,t.intersect=function(p){return it.polygon(t,o,p)},n}var Wse=F(()=>{"use strict";Zt();Kt();Xt();Ht();s(Vse,"choice")});async function k4(e,t,r){let{labelStyles:n,nodeStyles:i}=st(t);t.labelStyle=n;let{shapeSvg:a,bbox:o,halfPadding:l}=await yt(e,t,vt(t)),u=16,h=r?.padding??l,d=t.look==="neo"?o.width/2+u*2:o.width/2+h,f,{cssStyles:p}=t;if(t.look==="handDrawn"){let m=ot.svg(a),g=ft(t,{}),y=m.circle(0,0,d*2,g);f=a.insert(()=>y,":first-child"),f.attr("class","basic label-container").attr("style",un(p))}else f=a.insert("circle",":first-child").attr("class","basic label-container").attr("style",i).attr("r",d).attr("cx",0).attr("cy",0);return ht(t,f),t.calcIntersect=function(m,g){let y=m.width/2;return it.circle(m,y,g)},t.intersect=function(m){return se.info("Circle intersect",t,d,m),it.circle(t,d,m)},a}var $P=F(()=>{"use strict";Kt();St();er();Zt();Xt();Ht();s(k4,"circle")});async function qse(e,t){let r=t,n=["node",r.cssClasses,r.class].filter(Boolean).join(" "),{shapeSvg:i,bbox:a,halfPadding:o}=await yt(e,r,n),l=i.insert("rect",":first-child"),u=r.padding??0,h=r.positioned?r.width??0:a.width+u,d=r.positioned?r.height??0:a.height+u,f=r.positioned?-h/2:-a.width/2-o,p=r.positioned?-d/2:-a.height/2-o;return l.attr("class","basic cluster composite label-container").attr("style",r.style??null).attr("rx",r.rx??null).attr("ry",r.ry??null).attr("x",f).attr("y",p).attr("width",h).attr("height",d),ht(r,l),r.intersect=function(m){return it.rect(r,m)},i}var Hse=F(()=>{"use strict";Zt();Ht();s(qse,"composite")});async function Use(e,t,{config:{themeVariables:r}}){let{labelStyles:n,nodeStyles:i}=st(t);t.labelStyle=n;let{shapeSvg:a,bbox:o,label:l}=await yt(e,t,vt(t)),u=r?.nodeBorder??r?.lineColor??"currentColor",h=t.padding??12,d=20,f=12,p=Math.max(o.width+h*2,t.width??0,90),m=Math.max(o.height+h*2+d,t.height??0),g=-m/2,{cssStyles:y}=t,v=a.insert("g",":first-child").attr("class","basic label-container");if(t.look==="handDrawn"){let T=ot.svg(a).path(Mi(-p/2,g,p,m,f),ft(t,{}));v.node()?.appendChild(T),y&&v.attr("style",y)}else v.append("rect").attr("x",-p/2).attr("y",g).attr("width",p).attr("height",m).attr("rx",f).attr("ry",f).attr("style",i);v.append("text").attr("x",-p/2+12).attr("y",g+16).attr("class","console-glyph").attr("style",`font-family:monospace;font-weight:bold;font-size:14px;fill:${u}`).text(">_"),ht(t,v);let x=g+d+(m-d)/2;return l.attr("transform",`translate(${-(o.width/2)-(o.x-(o.left??0))}, ${x-o.height/2-(o.y-(o.top??0))})`),t.intersect=function(b){return it.rect(t,b)},a}var Yse=F(()=>{"use strict";Ht();Zt();ic();Xt();Kt();s(Use,"consoleWindow")});function DKe(e){let t=Math.cos(Math.PI/4),r=Math.sin(Math.PI/4),n=e*2,i={x:n/2*t,y:n/2*r},a={x:-(n/2)*t,y:n/2*r},o={x:-(n/2)*t,y:-(n/2)*r},l={x:n/2*t,y:-(n/2)*r};return`M ${a.x},${a.y} L ${l.x},${l.y} + M ${i.x},${i.y} L ${o.x},${o.y}`}function jse(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r,t.label="";let i=e.insert("g").attr("class",vt(t)).attr("id",t.domId??t.id),a=Math.max(30,t?.width??0),{cssStyles:o}=t,l=ot.svg(i),u=ft(t,{});t.look!=="handDrawn"&&(u.roughness=0,u.fillStyle="solid");let h=l.circle(0,0,a*2,u),d=DKe(a),f=l.path(d,u),p=i.insert(()=>h,":first-child");return p.insert(()=>f),p.attr("class","outer-path"),o&&t.look!=="handDrawn"&&p.selectAll("path").attr("style",o),n&&t.look!=="handDrawn"&&p.selectAll("path").attr("style",n),ht(t,p),t.intersect=function(m){return se.info("crossedCircle intersect",t,{radius:a,point:m}),it.circle(t,a,m)},i}var Xse=F(()=>{"use strict";St();Ht();Xt();Kt();Zt();s(DKe,"createLine");s(jse,"crossedCircle")});function ef(e,t,r,n=100,i=0,a=180){let o=[],l=i*Math.PI/180,d=(a*Math.PI/180-l)/(n-1);for(let f=0;fC,":first-child").attr("stroke-opacity",0),S.insert(()=>T,":first-child"),S.attr("class","text"),p&&t.look!=="handDrawn"&&S.selectAll("path").attr("style",p),n&&t.look!=="handDrawn"&&S.selectAll("path").attr("style",n),S.attr("transform",`translate(${f}, 0)`),o.attr("transform",`translate(${-h/2+f-(a.x-(a.left??0))},${-d/2+(t.padding??0)/2-(a.y-(a.top??0))})`),ht(t,S),t.intersect=function(k){return it.polygon(t,g,k)},i}var Zse=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(ef,"generateCirclePoints");s(Kse,"curlyBraceLeft")});function tf(e,t,r,n=100,i=0,a=180){let o=[],l=i*Math.PI/180,d=(a*Math.PI/180-l)/(n-1);for(let f=0;fC,":first-child").attr("stroke-opacity",0),S.insert(()=>T,":first-child"),S.attr("class","text"),p&&t.look!=="handDrawn"&&S.selectAll("path").attr("style",p),n&&t.look!=="handDrawn"&&S.selectAll("path").attr("style",n),S.attr("transform",`translate(${-f}, 0)`),o.attr("transform",`translate(${-h/2+(t.padding??0)/2-(a.x-(a.left??0))},${-d/2+(t.padding??0)/2-(a.y-(a.top??0))})`),ht(t,S),t.intersect=function(k){return it.polygon(t,g,k)},i}var Jse=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(tf,"generateCirclePoints");s(Qse,"curlyBraceRight")});function gs(e,t,r,n=100,i=0,a=180){let o=[],l=i*Math.PI/180,d=(a*Math.PI/180-l)/(n-1);for(let f=0;fM,":first-child").attr("stroke-opacity",0),P.insert(()=>w,":first-child"),P.insert(()=>k,":first-child"),P.attr("class","text"),p&&t.look!=="handDrawn"&&P.selectAll("path").attr("style",p),n&&t.look!=="handDrawn"&&P.selectAll("path").attr("style",n),P.attr("transform",`translate(${f-f/4}, 0)`),o.attr("transform",`translate(${-h/2+(t.padding??0)/2-(a.x-(a.left??0))},${-d/2+(t.padding??0)/2-(a.y-(a.top??0))})`),ht(t,P),t.intersect=function(N){return it.polygon(t,y,N)},i}var toe=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(gs,"generateCirclePoints");s(eoe,"curlyBraces")});async function roe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?12:i,l=20,u=5,{shapeSvg:h,bbox:d}=await yt(e,t,vt(t)),f=Math.max(l,(d.width+a*2)*1.25,t?.width??0),p=Math.max(u,d.height+o*2,t?.height??0),m=p/2,{cssStyles:g}=t,y=ot.svg(h),v=ft(t,{});t.look!=="handDrawn"&&(v.roughness=0,v.fillStyle="solid");let x=f,b=p,T=x-m,w=b/4,C=[{x:T,y:0},{x:w,y:0},{x:0,y:b/2},{x:w,y:b},{x:T,y:b},...Ho(-T,-b/2,m,50,270,90)],S=hr(C),k=y.path(S,v),R=h.insert(()=>k,":first-child");return R.attr("class","basic label-container outer-path"),g&&t.look!=="handDrawn"&&R.selectChildren("path").attr("style",g),n&&t.look!=="handDrawn"&&R.selectChildren("path").attr("style",n),R.attr("transform",`translate(${-f/2}, ${-p/2})`),ht(t,R),t.intersect=function(M){return it.polygon(t,C,M)},h}var noe=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(roe,"curvedTrapezoid")});async function ioe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,label:o}=await yt(e,t,vt(t)),l=t.padding??20,u=Math.max(a.width+l*2,t.width??0,100),h=Math.min(Math.max(u*.23,16),56),d=h*.27,f=Math.max(a.height+l*2,t.height?t.height-(2*h-d):0),p=Math.min(u*.177,f*.45),m=f+2*h-d,g=-m/2,y=g+2*h-d,v=i.insert("g",":first-child").attr("class","basic label-container"),{cssStyles:x}=t;if(t.look==="handDrawn"){let R=ot.svg(i),M=ft(t,{}),P=R.path(Mi(-u/2,y,u,f,p),M),N=R.circle(0,g+h,h*2,M);v.insert(()=>N,":first-child"),v.insert(()=>P,":first-child"),x&&v.attr("style",x)}else v.append("rect").attr("x",-u/2).attr("y",y).attr("width",u).attr("height",f).attr("rx",p).attr("ry",p).attr("style",n),v.append("circle").attr("cx",0).attr("cy",g+h).attr("r",h).attr("style",n);ht(t,v);let b=y+f/2;o.attr("transform",`translate(${-(a.width/2)-(a.x-(a.left??0))}, ${b-a.height/2-(a.y-(a.top??0))})`);let T=g+h,w=Math.asin(Math.min(1,(y-T)/h))*180/Math.PI,k=[...Ho(0,-T,h,24,180+w,-w),...Ho(-(-u/2+p),-(y+p),p,12,90,0),...Ho(-(-u/2+p),-(m/2-p),p,12,360,270),...Ho(-(u/2-p),-(m/2-p),p,12,270,180),...Ho(-(u/2-p),-(y+p),p,12,180,90)];return t.intersect=function(R){return it.polygon(t,k,R)},i}var aoe=F(()=>{"use strict";Ht();Zt();Xt();Kt();ic();s(ioe,"person")});async function loe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?24:i,o=t.look==="neo"?24:i,l=t.width??0;if(t.width&&(t.width=t.width-o,t.widthS,":first-child"),y=u.insert(()=>C,":first-child"),y.attr("class","basic label-container"),v&&y.attr("style",v)}else{let x=IKe(0,0,f,g,p,m);y=u.insert("path",":first-child").attr("d",x).attr("class","basic label-container outer-path").attr("style",un(v)).attr("style",n)}return y.attr("label-offset-y",m),y.attr("transform",`translate(${-f/2}, ${-(g/2+m)})`),ht(t,y),d.attr("transform",`translate(${-(h.width/2)-(h.x-(h.left??0))}, ${-(h.height/2)+(t.padding??0)/1.5-(h.y-(h.top??0))})`),t.intersect=function(x){let b=it.rect(t,x),T=b.x-(t.x??0);if(p!=0&&(Math.abs(T)<(t.width??0)/2||Math.abs(T)==(t.width??0)/2&&Math.abs(b.y-(t.y??0))>(t.height??0)/2-m)){let w=m*m*(1-T*T/(p*p));w>0&&(w=Math.sqrt(w)),w=m-w,x.y-(t.y??0)>0&&(w=-w),b.y+=w}return b},u}var IKe,MKe,NKe,soe,ooe,coe=F(()=>{"use strict";Ht();Zt();Xt();Kt();er();IKe=s((e,t,r,n,i,a)=>[`M${e},${t+a}`,`a${i},${a} 0,0,0 ${r},0`,`a${i},${a} 0,0,0 ${-r},0`,`l0,${n}`,`a${i},${a} 0,0,0 ${r},0`,`l0,${-n}`].join(" "),"createCylinderPathD"),MKe=s((e,t,r,n,i,a)=>[`M${e},${t+a}`,`M${e+r},${t+a}`,`a${i},${a} 0,0,0 ${-r},0`,`l0,${n}`,`a${i},${a} 0,0,0 ${r},0`,`l0,${-n}`].join(" "),"createOuterCylinderPathD"),NKe=s((e,t,r,n,i,a)=>[`M${e-r/2},${-n/2}`,`a${i},${a} 0,0,0 ${r},0`].join(" "),"createInnerCylinderPathD"),soe=8,ooe=8;s(loe,"cylinder")});async function ac(e,t,r){let{labelStyles:n,nodeStyles:i}=st(t);t.labelStyle=n;let{shapeSvg:a,bbox:o}=await yt(e,t,vt(t)),l=Math.max(o.width+r.labelPaddingX*2,t?.width||0),u=Math.max(o.height+r.labelPaddingY*2,t?.height||0),h=-l/2,d=-u/2,f,{rx:p,ry:m}=t,{cssStyles:g}=t;if(r?.rx&&r.ry&&(p=r.rx,m=r.ry),t.look==="handDrawn"){let y=ot.svg(a),v=ft(t,{}),x=p||m?y.path(Mi(h,d,l,u,p||0),v):y.rectangle(h,d,l,u,v);f=a.insert(()=>x,":first-child"),f.attr("class","basic label-container").attr("style",un(g))}else f=a.insert("rect",":first-child"),f.attr("class","basic label-container").attr("style",i).attr("rx",un(p)).attr("ry",un(m)).attr("x",h).attr("y",d).attr("width",l).attr("height",u);return ht(t,f,t.look==="handDrawn"?void 0:{width:l,height:u}),t.calcIntersect=function(y,v){return it.rect(y,v)},t.intersect=function(y){return it.rect(t,y)},a}var km=F(()=>{"use strict";Ht();Zt();ic();Xt();Kt();er();s(ac,"drawRect")});async function uoe(e,t){let{cssClasses:r,labelPaddingX:n,labelPaddingY:i,padding:a,width:o,height:l}=t,u={rx:0,ry:0,classes:r??"",labelPaddingX:n??(a??0)*2,labelPaddingY:i??a??0},h=await ac(e,t,u);if(t.look==="handDrawn"){let m=ot.svg(h),g=ft(t,{}),y=h.select(".basic.label-container > path:nth-child(2)"),v=y.node();if(!v)return h;let x=null;if(v instanceof SVGGraphicsElement)x=v.getBBox();else return h;return h.insert(()=>m.line(x.x,x.y,x.x+x.width,x.y,g),".basic.label-container g.label"),h.insert(()=>m.line(x.x,x.y+x.height,x.x+x.width,x.y+x.height,g),".basic.label-container g.label"),y.remove(),h}let d=h.select(".basic.label-container"),f=(Number(d.attr("width"))||o)??0,p=(Number(d.attr("height"))||l)??0;return f>0&&p>0&&d.attr("stroke-dasharray",`${f} ${p}`),h}var hoe=F(()=>{"use strict";km();Xt();Kt();s(uoe,"datastore")});async function doe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.look==="neo"?16:t.padding??0,a=t.look==="neo"?16:t.padding??0,{shapeSvg:o,bbox:l,label:u}=await yt(e,t,vt(t)),h=l.width+i,d=l.height+a,f=d*.2,p=-h/2,m=-d/2-f/2,{cssStyles:g}=t,y=ot.svg(o),v=ft(t,{});t.look!=="handDrawn"&&(v.roughness=0,v.fillStyle="solid");let x=[{x:p,y:m+f},{x:-p,y:m+f},{x:-p,y:-m},{x:p,y:-m},{x:p,y:m},{x:-p,y:m},{x:-p,y:m+f}],b=y.polygon(x.map(w=>[w.x,w.y]),v),T=o.insert(()=>b,":first-child");return T.attr("class","basic label-container outer-path"),g&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",g),n&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",n),u.attr("transform",`translate(${p+(t.padding??0)/2-(l.x-(l.left??0))}, ${m+f+(t.padding??0)/2-(l.y-(l.top??0))})`),ht(t,T),t.intersect=function(w){return it.rect(t,w)},o}var foe=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(doe,"dividedRectangle")});async function poe(e,t){let{labelStyles:r,nodeStyles:n}=st(t),i=t.look==="neo"?12:5;t.labelStyle=r;let a=t.padding??0,o=t.look==="neo"?16:a,{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=(t?.width?t?.width/2:u.width/2)+(o??0),d=h-i,f,{cssStyles:p}=t;if(t.look==="handDrawn"){let m=ot.svg(l),g=ft(t,{roughness:.2,strokeWidth:2.5}),y=ft(t,{roughness:.2,strokeWidth:1.5}),v=m.circle(0,0,h*2,g),x=m.circle(0,0,d*2,y);f=l.insert("g",":first-child"),f.attr("class",un(t.cssClasses)).attr("style",un(p)),f.node()?.appendChild(v),f.node()?.appendChild(x)}else{f=l.insert("g",":first-child");let m=f.insert("circle",":first-child"),g=f.insert("circle");f.attr("class","basic label-container").attr("style",n),m.attr("class","outer-circle").attr("style",n).attr("r",h).attr("cx",0).attr("cy",0),g.attr("class","inner-circle").attr("style",n).attr("r",d).attr("cx",0).attr("cy",0)}return ht(t,f),t.intersect=function(m){return se.info("DoubleCircle intersect",t,h,m),it.circle(t,h,m)},l}var moe=F(()=>{"use strict";St();Ht();Zt();Xt();Kt();er();s(poe,"doublecircle")});function goe(e,t,{config:{themeVariables:r}}){let{labelStyles:n,nodeStyles:i}=st(t);t.label="",t.labelStyle=n;let a=e.insert("g").attr("class",vt(t)).attr("id",t.domId??t.id),o=7,{cssStyles:l}=t,u=ot.svg(a),{nodeBorder:h}=r,d=ft(t,{fillStyle:"solid"});t.look!=="handDrawn"&&(d.roughness=0);let f=u.circle(0,0,o*2,d),p=a.insert(()=>f,":first-child");return p.selectAll("path").attr("style",`fill: ${h} !important;`),l&&l.length>0&&t.look!=="handDrawn"&&p.selectAll("path").attr("style",l),i&&t.look!=="handDrawn"&&p.selectAll("path").attr("style",i),ht(t,p),t.intersect=function(m){return se.info("filledCircle intersect",t,{radius:o,point:m}),it.circle(t,o,m)},a}var yoe=F(()=>{"use strict";Kt();St();Zt();Xt();Ht();s(goe,"filledCircle")});async function boe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?i*2:i;(t.width||t.height)&&(t.height=t?.height??0,t.heightx,":first-child").attr("transform",`translate(${-d/2}, ${d/2})`).attr("class","outer-path");return m&&t.look!=="handDrawn"&&b.selectChildren("path").attr("style",m),n&&t.look!=="handDrawn"&&b.selectChildren("path").attr("style",n),t.width=h,t.height=d,ht(t,b),u.attr("transform",`translate(${-l.width/2-(l.x-(l.left??0))}, ${-d/2+(t.padding??0)/2+(l.y-(l.top??0))})`),t.intersect=function(T){return se.info("Triangle intersect",t,p,T),it.polygon(t,p,T)},o}var voe,xoe,Toe=F(()=>{"use strict";St();Ht();Zt();Xt();Kt();Ht();voe=10,xoe=10;s(boe,"flippedTriangle")});async function Coe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,label:o}=await yt(e,t,vt(t)),l=t.padding??12,u=Math.max(a.width+l*2,t.width??0,90),h=a.height+l*2,d=Math.max(Math.min(h*.16,14),8),f=Math.max(h+d,t.height??0),p=f-d,m=Math.max(u*.38,28),g=-f/2,y=[{x:-u/2,y:g},{x:-u/2+m,y:g},{x:-u/2+m,y:g+d},{x:u/2,y:g+d},{x:u/2,y:f/2},{x:-u/2,y:f/2}],v=[`M${y[0].x},${y[0].y}`,...y.slice(1).map(w=>`L${w.x},${w.y}`),"Z"].join(" "),{cssStyles:x}=t,b;if(t.look==="handDrawn"){let C=ot.svg(i).path(v,ft(t,{}));b=i.insert(()=>C,":first-child").attr("class","basic label-container"),x&&b.attr("style",x)}else b=i.insert("path",":first-child").attr("d",v).attr("class","basic label-container").attr("style",n);t.look==="handDrawn"?ht(t,b):ht(t,b,{width:u,height:f});let T=g+d+p/2;return o.attr("transform",`translate(${-(a.width/2)-(a.x-(a.left??0))}, ${T-a.height/2-(a.y-(a.top??0))})`),t.intersect=function(w){return it.polygon(t,y,w)},i}var Soe=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(Coe,"folder")});function woe(e,t,{dir:r,config:{state:n,themeVariables:i}}){let{nodeStyles:a}=st(t);t.label="";let o=e.insert("g").attr("class",vt(t)).attr("id",t.domId??t.id),{cssStyles:l}=t,u=Math.max(70,t?.width??0),h=Math.max(10,t?.height??0);r==="LR"&&(u=Math.max(10,t?.width??0),h=Math.max(70,t?.height??0));let d=-1*u/2,f=-1*h/2,p=ot.svg(o),m=ft(t,{stroke:i.lineColor,fill:i.lineColor});t.look!=="handDrawn"&&(m.roughness=0,m.fillStyle="solid");let g=p.rectangle(d,f,u,h,m),y=o.insert(()=>g,":first-child");l&&t.look!=="handDrawn"&&y.selectAll("path").attr("style",l),a&&t.look!=="handDrawn"&&y.selectAll("path").attr("style",a),ht(t,y);let v=n?.padding??0;return t.width&&t.height&&(t.width+=v/2||0,t.height+=v/2||0),t.intersect=function(x){return it.rect(t,x)},o}var koe=F(()=>{"use strict";Kt();Zt();Xt();Ht();s(woe,"forkJoin")});async function Eoe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=15,a=10,o=t.look==="neo"?16:t.padding??0,l=t.look==="neo"?12:t.padding??0;(t.width||t.height)&&(t.height=(t?.height??0)-l*2,t.heightb,":first-child");return T.attr("class","basic label-container outer-path"),m&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",m),n&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",n),ht(t,T),t.intersect=function(w){return se.info("Pill intersect",t,{radius:p,point:w}),it.polygon(t,v,w)},u}var Aoe=F(()=>{"use strict";St();Ht();Zt();Xt();Kt();s(Eoe,"halfRoundedRectangle")});async function Roe(e,t){let{labelStyles:r,nodeStyles:n}=st(t),i=t.look==="neo"?3.5:4;t.labelStyle=r;let a=t.padding??0,o=70,l=32,u=t.look==="neo"?o:a,h=t.look==="neo"?l:a;if(t.width||t.height){let T=(t.height??0)/i;t.width=(t?.width??0)-2*T-h,t.height=(t.height??0)-u}let{shapeSvg:d,bbox:f}=await yt(e,t,vt(t)),p=(t?.height?t?.height:f.height)+u,m=p/i,g=(t?.width?t?.width:f.width)+2*m+h,y=[{x:m,y:0},{x:g-m,y:0},{x:g,y:-p/2},{x:g-m,y:-p},{x:m,y:-p},{x:0,y:-p/2}],v,{cssStyles:x}=t;if(t.look==="handDrawn"){let b=ot.svg(d),T=ft(t,{}),w=PKe(0,0,g,p,m),C=b.path(w,T);v=d.insert(()=>C,":first-child").attr("transform",`translate(${-g/2}, ${p/2})`),x&&v.attr("style",x)}else v=Na(d,g,p,y);return n&&v.attr("style",n),t.width=g,t.height=p,ht(t,v),t.intersect=function(b){return it.polygon(t,y,b)},d}var PKe,_oe=F(()=>{"use strict";Ht();Zt();Xt();Kt();iu();PKe=s((e,t,r,n,i)=>[`M${e+i},${t}`,`L${e+r-i},${t}`,`L${e+r},${t-n/2}`,`L${e+r-i},${t-n}`,`L${e+i},${t-n}`,`L${e},${t-n/2}`,"Z"].join(" "),"createHexagonPathD");s(Roe,"hexagon")});async function Loe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.label="",t.labelStyle=r;let{shapeSvg:i}=await yt(e,t,vt(t)),a=Math.max(30,t?.width??0),o=Math.max(30,t?.height??0),{cssStyles:l}=t,u=ot.svg(i),h=ft(t,{});t.look!=="handDrawn"&&(h.roughness=0,h.fillStyle="solid");let d=[{x:0,y:0},{x:a,y:0},{x:0,y:o},{x:a,y:o}],f=hr(d),p=u.path(f,h),m=i.insert(()=>p,":first-child");return m.attr("class","basic label-container outer-path"),l&&t.look!=="handDrawn"&&m.selectChildren("path").attr("style",l),n&&t.look!=="handDrawn"&&m.selectChildren("path").attr("style",n),m.attr("transform",`translate(${-a/2}, ${-o/2})`),ht(t,m),t.intersect=function(g){return se.info("Pill intersect",t,{points:d}),it.polygon(t,d,g)},i}var Doe=F(()=>{"use strict";St();Ht();Zt();Xt();Kt();s(Loe,"hourglass")});async function Ioe(e,t,{config:{themeVariables:r,flowchart:n}}){let{labelStyles:i}=st(t);t.labelStyle=i;let a=t.assetHeight??48,o=t.assetWidth??48,l=Math.max(a,o),u=n?.wrappingWidth;t.width=Math.max(l,u??0);let{shapeSvg:h,bbox:d,label:f}=await yt(e,t,"icon-shape default"),p=t.pos==="t",m=l,g=l,{nodeBorder:y}=r,{stylesMap:v}=nu(t),x=-g/2,b=-m/2,T=t.label?8:0,w=ot.svg(h),C=ft(t,{stroke:"none",fill:"none"});t.look!=="handDrawn"&&(C.roughness=0,C.fillStyle="solid");let S=w.rectangle(x,b,g,m,C),k=Math.max(g,d.width),R=m+d.height+T,M=w.rectangle(-k/2,-R/2,k,R,{...C,fill:"transparent",stroke:"none"}),P=h.insert(()=>S,":first-child"),N=h.insert(()=>M);if(t.icon){let _=h.append("g");_.html(`${await ps(t.icon,{height:l,width:l,fallbackPrefix:""})}`);let A=_.node().getBBox(),I=A.width,D=A.height,L=A.x,B=A.y;_.attr("transform",`translate(${-I/2-L},${p?d.height/2+T/2-D/2-B:-d.height/2-T/2-D/2-B})`),_.attr("style",`color: ${v.get("stroke")??y};`)}return f.attr("transform",`translate(${-d.width/2-(d.x-(d.left??0))},${p?-R/2:R/2-d.height})`),P.attr("transform",`translate(0,${p?d.height/2+T/2:-d.height/2-T/2})`),ht(t,N),t.intersect=function(_){if(se.info("iconSquare intersect",t,_),!t.label)return it.rect(t,_);let A=t.x??0,I=t.y??0,D=t.height??0,L=[];return p?L=[{x:A-d.width/2,y:I-D/2},{x:A+d.width/2,y:I-D/2},{x:A+d.width/2,y:I-D/2+d.height+T},{x:A+g/2,y:I-D/2+d.height+T},{x:A+g/2,y:I+D/2},{x:A-g/2,y:I+D/2},{x:A-g/2,y:I-D/2+d.height+T},{x:A-d.width/2,y:I-D/2+d.height+T}]:L=[{x:A-g/2,y:I-D/2},{x:A+g/2,y:I-D/2},{x:A+g/2,y:I-D/2+m},{x:A+d.width/2,y:I-D/2+m},{x:A+d.width/2/2,y:I+D/2},{x:A-d.width/2,y:I+D/2},{x:A-d.width/2,y:I-D/2+m},{x:A-g/2,y:I-D/2+m}],it.polygon(t,L,_)},h}var Moe=F(()=>{"use strict";Kt();St();Hl();Zt();Xt();Ht();s(Ioe,"icon")});async function Noe(e,t,{config:{themeVariables:r,flowchart:n}}){let{labelStyles:i}=st(t);t.labelStyle=i;let a=t.assetHeight??48,o=t.assetWidth??48,l=Math.max(a,o),u=n?.wrappingWidth;t.width=Math.max(l,u??0);let{shapeSvg:h,bbox:d,label:f}=await yt(e,t,"icon-shape default"),p=20,m=t.label?8:0,g=t.pos==="t",{nodeBorder:y,mainBkg:v}=r,{stylesMap:x}=nu(t),b=ot.svg(h),T=ft(t,{});t.look!=="handDrawn"&&(T.roughness=0,T.fillStyle="solid");let w=x.get("fill");T.stroke=w??v;let C=h.append("g");t.icon&&C.html(`${await ps(t.icon,{height:l,width:l,fallbackPrefix:""})}`);let S=C.node().getBBox(),k=S.width,R=S.height,M=S.x,P=S.y,N=Math.max(k,R)*Math.SQRT2+p*2,_=b.circle(0,0,N,T),A=Math.max(N,d.width),I=N+d.height+m,D=b.rectangle(-A/2,-I/2,A,I,{...T,fill:"transparent",stroke:"none"}),L=h.insert(()=>_,":first-child"),B=h.insert(()=>D);return C.attr("transform",`translate(${-k/2-M},${g?d.height/2+m/2-R/2-P:-d.height/2-m/2-R/2-P})`),C.attr("style",`color: ${x.get("stroke")??y};`),f.attr("transform",`translate(${-d.width/2-(d.x-(d.left??0))},${g?-I/2:I/2-d.height})`),L.attr("transform",`translate(0,${g?d.height/2+m/2:-d.height/2-m/2})`),ht(t,B),t.intersect=function(O){return se.info("iconSquare intersect",t,O),it.rect(t,O)},h}var Poe=F(()=>{"use strict";Kt();St();Hl();Zt();Xt();Ht();s(Noe,"iconCircle")});async function Ooe(e,t,{config:{themeVariables:r,flowchart:n}}){let{labelStyles:i}=st(t);t.labelStyle=i;let a=t.assetHeight??48,o=t.assetWidth??48,l=Math.max(a,o),u=n?.wrappingWidth;t.width=Math.max(l,u??0);let{shapeSvg:h,bbox:d,halfPadding:f,label:p}=await yt(e,t,"icon-shape default"),m=t.pos==="t",g=l+f*2,y=l+f*2,{nodeBorder:v,mainBkg:x}=r,{stylesMap:b}=nu(t),T=-y/2,w=-g/2,C=t.label?8:0,S=ot.svg(h),k=ft(t,{});t.look!=="handDrawn"&&(k.roughness=0,k.fillStyle="solid");let R=b.get("fill");k.stroke=R??x;let M=S.path(Mi(T,w,y,g,5),k),P=Math.max(y,d.width),N=g+d.height+C,_=S.rectangle(-P/2,-N/2,P,N,{...k,fill:"transparent",stroke:"none"}),A=h.insert(()=>M,":first-child").attr("class","icon-shape2"),I=h.insert(()=>_);if(t.icon){let D=h.append("g");D.html(`${await ps(t.icon,{height:l,width:l,fallbackPrefix:""})}`);let L=D.node().getBBox(),B=L.width,O=L.height,$=L.x,W=L.y;D.attr("transform",`translate(${-B/2-$},${m?d.height/2+C/2-O/2-W:-d.height/2-C/2-O/2-W})`),D.attr("style",`color: ${b.get("stroke")??v};`)}return p.attr("transform",`translate(${-d.width/2-(d.x-(d.left??0))},${m?-N/2:N/2-d.height})`),A.attr("transform",`translate(0,${m?d.height/2+C/2:-d.height/2-C/2})`),ht(t,I),t.intersect=function(D){if(se.info("iconSquare intersect",t,D),!t.label)return it.rect(t,D);let L=t.x??0,B=t.y??0,O=t.height??0,$=[];return m?$=[{x:L-d.width/2,y:B-O/2},{x:L+d.width/2,y:B-O/2},{x:L+d.width/2,y:B-O/2+d.height+C},{x:L+y/2,y:B-O/2+d.height+C},{x:L+y/2,y:B+O/2},{x:L-y/2,y:B+O/2},{x:L-y/2,y:B-O/2+d.height+C},{x:L-d.width/2,y:B-O/2+d.height+C}]:$=[{x:L-y/2,y:B-O/2},{x:L+y/2,y:B-O/2},{x:L+y/2,y:B-O/2+g},{x:L+d.width/2,y:B-O/2+g},{x:L+d.width/2/2,y:B+O/2},{x:L-d.width/2,y:B+O/2},{x:L-d.width/2,y:B-O/2+g},{x:L-y/2,y:B-O/2+g}],it.polygon(t,$,D)},h}var Boe=F(()=>{"use strict";Kt();St();Hl();Zt();Xt();ic();Ht();s(Ooe,"iconRounded")});async function $oe(e,t,{config:{themeVariables:r,flowchart:n}}){let{labelStyles:i}=st(t);t.labelStyle=i;let a=t.assetHeight??48,o=t.assetWidth??48,l=Math.max(a,o),u=n?.wrappingWidth;t.width=Math.max(l,u??0);let{shapeSvg:h,bbox:d,halfPadding:f,label:p}=await yt(e,t,"icon-shape default"),m=t.pos==="t",g=l+f*2,y=l+f*2,{nodeBorder:v,mainBkg:x}=r,{stylesMap:b}=nu(t),T=-y/2,w=-g/2,C=t.label?8:0,S=ot.svg(h),k=ft(t,{});t.look!=="handDrawn"&&(k.roughness=0,k.fillStyle="solid");let R=b.get("fill");k.stroke=R??x;let M=S.path(Mi(T,w,y,g,.1),k),P=Math.max(y,d.width),N=g+d.height+C,_=S.rectangle(-P/2,-N/2,P,N,{...k,fill:"transparent",stroke:"none"}),A=h.insert(()=>M,":first-child"),I=h.insert(()=>_);if(t.icon){let D=h.append("g");D.html(`${await ps(t.icon,{height:l,width:l,fallbackPrefix:""})}`);let L=D.node().getBBox(),B=L.width,O=L.height,$=L.x,W=L.y;D.attr("transform",`translate(${-B/2-$},${m?d.height/2+C/2-O/2-W:-d.height/2-C/2-O/2-W})`),D.attr("style",`color: ${b.get("stroke")??v};`)}return p.attr("transform",`translate(${-d.width/2-(d.x-(d.left??0))},${m?-N/2:N/2-d.height})`),A.attr("transform",`translate(0,${m?d.height/2+C/2:-d.height/2-C/2})`),ht(t,I),t.intersect=function(D){if(se.info("iconSquare intersect",t,D),!t.label)return it.rect(t,D);let L=t.x??0,B=t.y??0,O=t.height??0,$=[];return m?$=[{x:L-d.width/2,y:B-O/2},{x:L+d.width/2,y:B-O/2},{x:L+d.width/2,y:B-O/2+d.height+C},{x:L+y/2,y:B-O/2+d.height+C},{x:L+y/2,y:B+O/2},{x:L-y/2,y:B+O/2},{x:L-y/2,y:B-O/2+d.height+C},{x:L-d.width/2,y:B-O/2+d.height+C}]:$=[{x:L-y/2,y:B-O/2},{x:L+y/2,y:B-O/2},{x:L+y/2,y:B-O/2+g},{x:L+d.width/2,y:B-O/2+g},{x:L+d.width/2/2,y:B+O/2},{x:L-d.width/2,y:B+O/2},{x:L-d.width/2,y:B-O/2+g},{x:L-y/2,y:B-O/2+g}],it.polygon(t,$,D)},h}var Foe=F(()=>{"use strict";Kt();St();Hl();Zt();ic();Xt();Ht();s($oe,"iconSquare")});async function Goe(e,t,{config:{flowchart:r}}){let n=new Image;n.src=t?.img??"",await n.decode();let i=Number(n.naturalWidth.toString().replace("px","")),a=Number(n.naturalHeight.toString().replace("px",""));t.imageAspectRatio=i/a;let{labelStyles:o}=st(t);t.labelStyle=o;let l=r?.wrappingWidth;t.defaultWidth=r?.wrappingWidth;let u=Math.max(t.label?l??0:0,t?.assetWidth??i),h=t.constraint==="on"&&t?.assetHeight?t.assetHeight*t.imageAspectRatio:u,d=t.constraint==="on"?h/t.imageAspectRatio:t?.assetHeight??a;t.width=Math.max(h,l??0);let{shapeSvg:f,bbox:p,label:m}=await yt(e,t,"image-shape default"),g=t.pos==="t",y=-h/2,v=-d/2,x=t.label?8:0,b=ot.svg(f),T=ft(t,{});t.look!=="handDrawn"&&(T.roughness=0,T.fillStyle="solid");let w=b.rectangle(y,v,h,d,T),C=Math.max(h,p.width),S=d+p.height+x,k=b.rectangle(-C/2,-S/2,C,S,{...T,fill:"none",stroke:"none"}),R=f.insert(()=>w,":first-child"),M=f.insert(()=>k);if(t.img){let P=f.append("image");P.attr("href",t.img),P.attr("width",h),P.attr("height",d),P.attr("preserveAspectRatio","none"),P.attr("transform",`translate(${-h/2},${g?S/2-d:-S/2})`)}return m.attr("transform",`translate(${-p.width/2-(p.x-(p.left??0))},${g?-d/2-p.height/2-x/2:d/2-p.height/2+x/2})`),R.attr("transform",`translate(0,${g?p.height/2+x/2:-p.height/2-x/2})`),ht(t,M),t.intersect=function(P){if(se.info("iconSquare intersect",t,P),!t.label)return it.rect(t,P);let N=t.x??0,_=t.y??0,A=t.height??0,I=[];return g?I=[{x:N-p.width/2,y:_-A/2},{x:N+p.width/2,y:_-A/2},{x:N+p.width/2,y:_-A/2+p.height+x},{x:N+h/2,y:_-A/2+p.height+x},{x:N+h/2,y:_+A/2},{x:N-h/2,y:_+A/2},{x:N-h/2,y:_-A/2+p.height+x},{x:N-p.width/2,y:_-A/2+p.height+x}]:I=[{x:N-h/2,y:_-A/2},{x:N+h/2,y:_-A/2},{x:N+h/2,y:_-A/2+d},{x:N+p.width/2,y:_-A/2+d},{x:N+p.width/2/2,y:_+A/2},{x:N-p.width/2,y:_+A/2},{x:N-p.width/2,y:_-A/2+d},{x:N-h/2,y:_-A/2+d}],it.polygon(t,I,P)},f}var zoe=F(()=>{"use strict";Kt();St();Zt();Xt();Ht();s(Goe,"imageSquare")});async function Voe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=i,o=t.look==="neo"?i*2:i,{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=Math.max(u.height+a*2,t.height??0),d=Math.max(u.width+o*2,(t.width??0)-h),f=[{x:0,y:0},{x:d,y:0},{x:d+3*h/6,y:-h},{x:-3*h/6,y:-h}],p,{cssStyles:m}=t;if(t.look==="handDrawn"){let g=ot.svg(l),y=ft(t,{}),v=hr(f),x=g.path(v,y);p=l.insert(()=>x,":first-child").attr("transform",`translate(${-d/2}, ${h/2})`),m&&p.attr("style",m)}else p=Na(l,d,h,f);return n&&p.attr("style",n),t.width=d,t.height=h,ht(t,p),t.intersect=function(g){return it.polygon(t,f,g)},l}var Woe=F(()=>{"use strict";Ht();Zt();Xt();Kt();iu();s(Voe,"inv_trapezoid")});async function qoe(e,t){let{shapeSvg:r,bbox:n,label:i}=await yt(e,t,"label"),a=r.insert("rect",":first-child");return a.attr("width",.1).attr("height",.1),r.attr("class","label edgeLabel"),i.attr("transform",`translate(${-(n.width/2)-(n.x-(n.left??0))}, ${-(n.height/2)-(n.y-(n.top??0))})`),ht(t,a),t.intersect=function(u){return it.rect(t,u)},r}var Hoe=F(()=>{"use strict";km();Ht();Zt();s(qoe,"labelRect")});async function Uoe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=i,o=t.look==="neo"?i*2:i,{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=Math.max(u.height+a,t.height??0),d=Math.max(u.width+o,(t.width??0)-h),f=[{x:0,y:0},{x:d+3*h/6,y:0},{x:d,y:-h},{x:-(3*h)/6,y:-h}],p,{cssStyles:m}=t;if(t.look==="handDrawn"){let g=ot.svg(l),y=ft(t,{}),v=hr(f),x=g.path(v,y);p=l.insert(()=>x,":first-child").attr("transform",`translate(${-d/2}, ${h/2})`),m&&p.attr("style",m)}else p=Na(l,d,h,f);return n&&p.attr("style",n),t.width=d,t.height=h,ht(t,p),t.intersect=function(g){return it.polygon(t,f,g)},l}var Yoe=F(()=>{"use strict";Ht();Zt();Xt();Kt();iu();s(Uoe,"lean_left")});async function joe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=i,o=t.look==="neo"?i*2:i,{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=Math.max(u.height+a,t.height??0),d=Math.max(u.width+o,(t.width??0)-h),f=[{x:-3*h/6,y:0},{x:d,y:0},{x:d+3*h/6,y:-h},{x:0,y:-h}],p,{cssStyles:m}=t;if(t.look==="handDrawn"){let g=ot.svg(l),y=ft(t,{}),v=hr(f),x=g.path(v,y);p=l.insert(()=>x,":first-child").attr("transform",`translate(${-d/2}, ${h/2})`),m&&p.attr("style",m)}else p=Na(l,d,h,f);return n&&p.attr("style",n),t.width=d,t.height=h,ht(t,p),t.intersect=function(g){return it.polygon(t,f,g)},l}var Xoe=F(()=>{"use strict";Ht();Zt();Xt();Kt();iu();s(joe,"lean_right")});function Koe(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.label="",t.labelStyle=r;let i=e.insert("g").attr("class",vt(t)).attr("id",t.domId??t.id),{cssStyles:a}=t,o=Math.max(35,t?.width??0),l=Math.max(35,t?.height??0),u=7,h=[{x:o,y:0},{x:0,y:l+u/2},{x:o-2*u,y:l+u/2},{x:0,y:2*l},{x:o,y:l-u/2},{x:2*u,y:l-u/2}],d=ot.svg(i),f=ft(t,{});t.look!=="handDrawn"&&(f.roughness=0,f.fillStyle="solid");let p=hr(h),m=d.path(p,f),g=i.insert(()=>m,":first-child");return g.attr("class","outer-path"),a&&t.look!=="handDrawn"&&g.selectAll("path").attr("style",a),n&&t.look!=="handDrawn"&&g.selectAll("path").attr("style",n),g.attr("transform",`translate(-${o/2},${-l})`),ht(t,g),t.intersect=function(y){return se.info("lightningBolt intersect",t,y),it.polygon(t,h,y)},i}var Zoe=F(()=>{"use strict";St();Ht();Xt();Kt();Zt();Ht();s(Koe,"lightningBolt")});async function ele(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?24:i;if(t.width||t.height){let x=t.width??0;t.width=(t.width??0)-a,t.widthS,":first-child").attr("class","line"),y=l.insert(()=>C,":first-child"),y.attr("class","basic label-container"),v&&y.attr("style",v)}else{let x=OKe(0,0,d,m,f,p,g);y=l.insert("path",":first-child").attr("d",x).attr("class","basic label-container outer-path").attr("style",un(v)).attr("style",n)}return y.attr("label-offset-y",p),y.attr("transform",`translate(${-d/2}, ${-(m/2+p)})`),ht(t,y),h.attr("transform",`translate(${-(u.width/2)-(u.x-(u.left??0))}, ${-(u.height/2)+p-(u.y-(u.top??0))})`),t.intersect=function(x){let b=it.rect(t,x),T=b.x-(t.x??0);if(f!=0&&(Math.abs(T)<(t.width??0)/2||Math.abs(T)==(t.width??0)/2&&Math.abs(b.y-(t.y??0))>(t.height??0)/2-p)){let w=p*p*(1-T*T/(f*f));w>0&&(w=Math.sqrt(w)),w=p-w,x.y-(t.y??0)>0&&(w=-w),b.y+=w}return b},l}var OKe,BKe,$Ke,Qoe,Joe,tle=F(()=>{"use strict";Ht();Zt();Xt();Kt();er();OKe=s((e,t,r,n,i,a,o)=>[`M${e},${t+a}`,`a${i},${a} 0,0,0 ${r},0`,`a${i},${a} 0,0,0 ${-r},0`,`l0,${n}`,`a${i},${a} 0,0,0 ${r},0`,`l0,${-n}`,`M${e},${t+a+o}`,`a${i},${a} 0,0,0 ${r},0`].join(" "),"createCylinderPathD"),BKe=s((e,t,r,n,i,a,o)=>[`M${e},${t+a}`,`M${e+r},${t+a}`,`a${i},${a} 0,0,0 ${-r},0`,`l0,${n}`,`a${i},${a} 0,0,0 ${r},0`,`l0,${-n}`,`M${e},${t+a+o}`,`a${i},${a} 0,0,0 ${r},0`].join(" "),"createOuterCylinderPathD"),$Ke=s((e,t,r,n,i,a)=>[`M${e-r/2},${-n/2}`,`a${i},${a} 0,0,0 ${r},0`].join(" "),"createInnerCylinderPathD"),Qoe=10,Joe=10;s(ele,"linedCylinder")});async function rle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?12:i;if(t.width||t.height){let w=t.width;t.width=(w??0)*10/11-a*2,t.width<10&&(t.width=10),t.height=(t?.height??0)-o*2,t.height<10&&(t.height=10)}let{shapeSvg:l,bbox:u,label:h}=await yt(e,t,vt(t)),d=(t?.width?t?.width:u.width)+(a??0)*2,f=(t?.height?t?.height:u.height)+(o??0)*2,p=t.look==="neo"?f/4:f/8,m=f+p,{cssStyles:g}=t,y=ot.svg(l),v=ft(t,{});t.look!=="handDrawn"&&(v.roughness=0,v.fillStyle="solid");let x=[{x:-d/2-d/2*.1,y:-m/2},{x:-d/2-d/2*.1,y:m/2},...vl(-d/2-d/2*.1,m/2,d/2+d/2*.1,m/2,p,.8),{x:d/2+d/2*.1,y:-m/2},{x:-d/2-d/2*.1,y:-m/2},{x:-d/2,y:-m/2},{x:-d/2,y:m/2*1.1},{x:-d/2,y:-m/2}],b=y.polygon(x.map(w=>[w.x,w.y]),v),T=l.insert(()=>b,":first-child");return T.attr("class","basic label-container outer-path"),g&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",g),n&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",n),T.attr("transform",`translate(0,${-p/2})`),h.attr("transform",`translate(${-d/2+(t.padding??0)+d/2*.1/2-(u.x-(u.left??0))},${-f/2+(t.padding??0)-p/2-(u.y-(u.top??0))})`),ht(t,T),t.intersect=function(w){return it.polygon(t,x,w)},l}var nle=F(()=>{"use strict";Ht();Zt();Kt();Xt();s(rle,"linedWaveEdgedRect")});async function ile(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?12:i,l=t.look==="neo"?10:5;(t.width||t.height)&&(t.width=Math.max((t?.width??0)-a*2-2*l,10),t.height=Math.max((t?.height??0)-o*2-2*l,10));let{shapeSvg:u,bbox:h,label:d}=await yt(e,t,vt(t)),f=(t?.width?t?.width:h.width)+a*2+2*l,p=(t?.height?t?.height:h.height)+o*2+2*l,m=f-2*l,g=p-2*l,y=-m/2,v=-g/2,{cssStyles:x}=t,b=ot.svg(u),T=ft(t,{}),w=[{x:y-l,y:v+l},{x:y-l,y:v+g+l},{x:y+m-l,y:v+g+l},{x:y+m-l,y:v+g},{x:y+m,y:v+g},{x:y+m,y:v+g-l},{x:y+m+l,y:v+g-l},{x:y+m+l,y:v-l},{x:y+l,y:v-l},{x:y+l,y:v},{x:y,y:v},{x:y,y:v+l}],C=[{x:y,y:v+l},{x:y+m-l,y:v+l},{x:y+m-l,y:v+g},{x:y+m,y:v+g},{x:y+m,y:v},{x:y,y:v}];t.look!=="handDrawn"&&(T.roughness=0,T.fillStyle="solid");let S=hr(w),k=b.path(S,T),R=hr(C),M=b.path(R,T);t.look!=="handDrawn"&&(k=mP(k),M=mP(M));let P=u.insert("g",":first-child");return P.insert(()=>k),P.insert(()=>M),P.attr("class","basic label-container outer-path"),x&&t.look!=="handDrawn"&&P.selectAll("path").attr("style",x),n&&t.look!=="handDrawn"&&P.selectAll("path").attr("style",n),d.attr("transform",`translate(${-(h.width/2)-l-(h.x-(h.left??0))}, ${-(h.height/2)+l-(h.y-(h.top??0))})`),ht(t,P),t.intersect=function(N){return it.polygon(t,w,N)},u}var ale=F(()=>{"use strict";Ht();Xt();Kt();Zt();s(ile,"multiRect")});async function sle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,label:o}=await yt(e,t,vt(t)),l=t.padding??0,u=t.look==="neo"?16:l,h=t.look==="neo"?12:l,d=!0;(t.width||t.height)&&(d=!1,t.width=(t?.width??0)-u*2,t.height=(t?.height??0)-h*3);let f=Math.max(a.width,t?.width??0)+u*2,p=Math.max(a.height,t?.height??0)+h*3,m=t.look==="neo"?p/4:p/8,g=p+(d?m/2:-m/2),y=-f/2,v=-g/2,x=10,{cssStyles:b}=t,T=vl(y-x,v+g+x,y+f-x,v+g+x,m,.8),w=T?.[T.length-1],C=[{x:y-x,y:v+x},{x:y-x,y:v+g+x},...T,{x:y+f-x,y:w.y-x},{x:y+f,y:w.y-x},{x:y+f,y:w.y-2*x},{x:y+f+x,y:w.y-2*x},{x:y+f+x,y:v-x},{x:y+x,y:v-x},{x:y+x,y:v},{x:y,y:v},{x:y,y:v+x}],S=[{x:y,y:v+x},{x:y+f-x,y:v+x},{x:y+f-x,y:w.y-x},{x:y+f,y:w.y-x},{x:y+f,y:v},{x:y,y:v}],k=ot.svg(i),R=ft(t,{});t.look!=="handDrawn"&&(R.roughness=0,R.fillStyle="solid");let M=hr(C),P=k.path(M,R),N=hr(S),_=k.path(N,R),A=i.insert(()=>P,":first-child");return A.insert(()=>_),A.attr("class","basic label-container outer-path"),b&&t.look!=="handDrawn"&&A.selectAll("path").attr("style",b),n&&t.look!=="handDrawn"&&A.selectAll("path").attr("style",n),A.attr("transform",`translate(0,${-m/2})`),o.attr("transform",`translate(${-(a.width/2)-x-(a.x-(a.left??0))}, ${-(a.height/2)+x-m/2-(a.y-(a.top??0))})`),ht(t,A),t.intersect=function(I){return it.polygon(t,C,I)},i}var ole=F(()=>{"use strict";Ht();Zt();Kt();Xt();s(sle,"multiWaveEdgedRectangle")});async function lle(e,t,{config:{themeVariables:r}}){let{labelStyles:n,nodeStyles:i}=st(t);t.labelStyle=n,t.useHtmlLabels||ai(Lt())||(t.centerLabel=!0);let{shapeSvg:o,bbox:l,label:u}=await yt(e,t,vt(t)),h=Math.max(l.width+(t.padding??0)*2,t?.width??0),d=Math.max(l.height+(t.padding??0)*2,t?.height??0),f=-h/2,p=-d/2,{cssStyles:m}=t,g=ot.svg(o),y=ft(t,{fill:r.noteBkgColor,stroke:r.noteBorderColor});t.look!=="handDrawn"&&(y.roughness=0,y.fillStyle="solid");let v=g.rectangle(f,p,h,d,y),x=o.insert(()=>v,":first-child");return x.attr("class","basic label-container outer-path"),u.attr("class","label noteLabel"),m&&t.look!=="handDrawn"&&x.selectAll("path").attr("style",m),i&&t.look!=="handDrawn"&&x.selectAll("path").attr("style",i),u.attr("transform",`translate(${-l.width/2-(l.x-(l.left??0))}, ${-(l.height/2)-(l.y-(l.top??0))})`),ht(t,x),t.intersect=function(b){return it.rect(t,b)},o}var cle=F(()=>{"use strict";Kt();Zt();Xt();Ht();br();br();s(lle,"note")});async function ule(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let{shapeSvg:i,bbox:a}=await yt(e,t,vt(t)),o=a.width+(t.padding??0),l=a.height+(t.padding??0),u=o+l,h=.5,d=[{x:u/2,y:0},{x:u,y:-u/2},{x:u/2,y:-u},{x:0,y:-u/2}],f,{cssStyles:p}=t;if(t.look==="handDrawn"){let m=ot.svg(i),g=ft(t,{}),y=FKe(0,0,u),v=m.path(y,g);f=i.insert(()=>v,":first-child").attr("transform",`translate(${-u/2+h}, ${u/2})`),p&&f.attr("style",p)}else f=Na(i,u,u,d),f.attr("transform",`translate(${-u/2+h}, ${u/2})`);return n&&f.attr("style",n),ht(t,f),t.calcIntersect=function(m,g){let y=m.width,v=[{x:y/2,y:0},{x:y,y:-y/2},{x:y/2,y:-y},{x:0,y:-y/2}],x=it.polygon(m,v,g);return{x:x.x-.5,y:x.y-.5}},t.intersect=function(m){return this.calcIntersect(t,m)},i}var FKe,hle=F(()=>{"use strict";Ht();Zt();Xt();Kt();iu();FKe=s((e,t,r)=>[`M${e+r/2},${t}`,`L${e+r},${t-r/2}`,`L${e+r/2},${t-r}`,`L${e},${t-r/2}`,"Z"].join(" "),"createDecisionBoxPathD");s(ule,"question")});async function dle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?21:i??0,o=t.look==="neo"?12:i??0,{shapeSvg:l,bbox:u,label:h}=await yt(e,t,vt(t)),d=u.width+(t.look==="neo"?a*2:a),f=Math.max(u.height+(t.look==="neo"?o*2:o),t.height??0),p=f/4,g=-Math.max(d,(t.width??0)-p)/2,y=-f/2,v=y/2,x=[{x:g+v,y},{x:g,y:0},{x:g+v,y:-y},{x:-g,y:-y},{x:-g,y}],{cssStyles:b}=t,T=ot.svg(l),w=ft(t,{});t.look!=="handDrawn"&&(w.roughness=0,w.fillStyle="solid");let C=hr(x),S=T.path(C,w),k=l.insert(()=>S,":first-child");return k.attr("class","basic label-container outer-path"),b&&t.look!=="handDrawn"&&k.selectAll("path").attr("style",b),n&&t.look!=="handDrawn"&&k.selectAll("path").attr("style",n),k.attr("transform",`translate(${-v/2},0)`),h.attr("transform",`translate(${-v/2-u.width/2-(u.x-(u.left??0))}, ${-(u.height/2)-(u.y-(u.top??0))})`),ht(t,k),t.intersect=function(R){return it.polygon(t,x,R)},l}var fle=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(dle,"rect_left_inv_arrow")});var GKe,sc,E4=F(()=>{"use strict";br();Jt();qo();GKe=s(async(e,t,r,n=!1,i=!1)=>{let a=t||"";typeof a=="object"&&(a=a[0]);let o=Pe(),l=ai(o);return await li(e,a,{style:r,isTitle:n,useHtmlLabels:l,markdown:!1,isNode:i,width:Number.POSITIVE_INFINITY},o)},"createLabel"),sc=GKe});async function ple(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i;t.cssClasses?i="node "+t.cssClasses:i="node default";let a=e.insert("g").attr("class",i).attr("id",t.domId||t.id),o=a.insert("g"),l=a.insert("g").attr("class","label").attr("style",n),u=t.description,h=t.label,d=await sc(l,h,t.labelStyle,!0,!0),f={width:0,height:0};if(ai(Pe())){let R=d.children[0],M=ut(d);f=R.getBoundingClientRect(),M.attr("width",f.width),M.attr("height",f.height)}se.info("Text 2",u);let p=u||[],m=d.getBBox(),g=await sc(l,Array.isArray(p)?p.join("
    "):p,t.labelStyle,!0,!0),y=g.children[0],v=ut(g);f=y.getBoundingClientRect(),v.attr("width",f.width),v.attr("height",f.height);let x=(t.padding||0)/2;ut(g).attr("transform","translate( "+(f.width>m.width?0:(m.width-f.width)/2)+", "+(m.height+x+5)+")"),ut(d).attr("transform","translate( "+(f.width(se.debug("Rough node insert CXC",P),N),":first-child"),S=a.insert(()=>(se.debug("Rough node insert CXC",P),P),":first-child")}else S=o.insert("rect",":first-child"),k=o.insert("line"),S.attr("class","outer title-state").attr("style",n).attr("x",-f.width/2-x).attr("y",-f.height/2-x).attr("width",f.width+(t.padding||0)).attr("height",f.height+(t.padding||0)),k.attr("class","divider").attr("x1",-f.width/2-x).attr("x2",f.width/2+x).attr("y1",-f.height/2-x+m.height+x).attr("y2",-f.height/2-x+m.height+x);return ht(t,S),t.intersect=function(R){return it.rect(t,R)},a}var mle=F(()=>{"use strict";Wr();Ht();E4();Zt();Xt();Kt();Jt();ic();St();br();s(ple,"rectWithTitle")});async function gle(e,t,{config:{themeVariables:r}}){let n=r?.radius??5,i={rx:n,ry:n,classes:"",labelPaddingX:(t?.padding??0)*1,labelPaddingY:(t?.padding??0)*1};return ac(e,t,i)}var yle=F(()=>{"use strict";km();s(gle,"roundedRect")});async function vle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.look==="neo"?16:t.padding??0,a=t.look==="neo"?12:t.padding??0,{shapeSvg:o,bbox:l,label:u}=await yt(e,t,vt(t)),h=(t?.width??l.width)+i*2+(t.look==="neo"?Em:Em*2),d=(t?.height??l.height)+a*2,f=h-Em,p=d,m=Em-h/2,g=-d/2,{cssStyles:y}=t,v=ot.svg(o),x=ft(t,{});t.look!=="handDrawn"&&(x.roughness=0,x.fillStyle="solid");let b=[{x:m,y:g},{x:m+f,y:g},{x:m+f,y:g+p},{x:m-Em,y:g+p},{x:m-Em,y:g},{x:m,y:g},{x:m,y:g+p}],T=v.polygon(b.map(C=>[C.x,C.y]),x),w=o.insert(()=>T,":first-child");return w.attr("class","basic label-container outer-path").attr("style",un(y)),n&&t.look!=="handDrawn"&&w.selectAll("path").attr("style",n),y&&t.look!=="handDrawn"&&w.selectAll("path").attr("style",n),u.attr("transform",`translate(${Em/2-l.width/2-(l.x-(l.left??0))}, ${-(l.height/2)-(l.y-(l.top??0))})`),ht(t,w),t.intersect=function(C){return it.rect(t,C)},o}var Em,xle=F(()=>{"use strict";Ht();Zt();Xt();Kt();er();Em=8;s(vle,"shadedProcess")});async function ble(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?12:i;(t.width||t.height)&&(t.width=Math.max((t?.width??0)-a*2,10),t.height=Math.max((t?.height??0)/1.5-o*2,10));let{shapeSvg:l,bbox:u,label:h}=await yt(e,t,vt(t)),d=(t?.width?t?.width:u.width)+a*2,f=((t?.height?t?.height:u.height)+o*2)*1.5,p=d,m=f/1.5,g=-p/2,y=-m/2,{cssStyles:v}=t,x=ot.svg(l),b=ft(t,{});t.look!=="handDrawn"&&(b.roughness=0,b.fillStyle="solid");let T=[{x:g,y},{x:g,y:y+m},{x:g+p,y:y+m},{x:g+p,y:y-m/2}],w=hr(T),C=x.path(w,b),S=l.insert(()=>C,":first-child");return S.attr("class","basic label-container outer-path"),v&&t.look!=="handDrawn"&&S.selectChildren("path").attr("style",v),n&&t.look!=="handDrawn"&&S.selectChildren("path").attr("style",n),S.attr("transform",`translate(0, ${m/4})`),h.attr("transform",`translate(${-p/2+(t.padding??0)-(u.x-(u.left??0))}, ${-m/4+(t.padding??0)-(u.y-(u.top??0))})`),ht(t,S),t.intersect=function(k){return it.polygon(t,T,k)},l}var Tle=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(ble,"slopedRect")});async function Cle(e,t){let r=t.padding??0,n=t.look==="neo"?16:r*2,i=t.look==="neo"?12:r,a={rx:0,ry:0,classes:"",labelPaddingX:t.labelPaddingX??n,labelPaddingY:i};return ac(e,t,a)}var Sle=F(()=>{"use strict";km();s(Cle,"squareRect")});async function wle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?20:i,o=t.look==="neo"?12:i,{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=u.height+(t.look==="neo"?o*2:o),d=u.width+h/4+(t.look==="neo"?a*2:a),f=h/2,{cssStyles:p}=t,m=ot.svg(l),g=ft(t,{});t.look!=="handDrawn"&&(g.roughness=0,g.fillStyle="solid");let y=[{x:-d/2+f,y:-h/2},{x:d/2-f,y:-h/2},...Ho(-d/2+f,0,f,50,90,270),{x:d/2-f,y:h/2},...Ho(d/2-f,0,f,50,270,450)],v=hr(y),x=m.path(v,g),b=l.insert(()=>x,":first-child");return b.attr("class","basic label-container outer-path"),p&&t.look!=="handDrawn"&&b.selectChildren("path").attr("style",p),n&&t.look!=="handDrawn"&&b.selectChildren("path").attr("style",n),ht(t,b),t.intersect=function(T){return it.polygon(t,y,T)},l}var kle=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(wle,"stadium")});async function Ele(e,t){let r={rx:t.look==="neo"?3:5,ry:t.look==="neo"?3:5,classes:"flowchart-node"};return ac(e,t,r)}var Ale=F(()=>{"use strict";km();s(Ele,"state")});function Rle(e,t,{config:{themeVariables:r}}){let{labelStyles:n,nodeStyles:i}=st(t);t.labelStyle=n;let{cssStyles:a}=t,{lineColor:o,stateBorder:l,nodeBorder:u,nodeShadow:h}=r;(t.width||t.height)&&((t.width??0)<14&&(t.width=14),(t.height??0)<14&&(t.height=14)),t.width||(t.width=14),t.height||(t.height=14);let d=e.insert("g").attr("class","node default").attr("id",t.domId??t.id),f=ot.svg(d),p=ft(t,{});t.look!=="handDrawn"&&(p.roughness=0,p.fillStyle="solid");let m=f.circle(0,0,t.width,{...p,stroke:o,strokeWidth:2}),g=l??u,y=(t.width??0)*5/14,v=f.circle(0,0,y,{...p,fill:g,stroke:g,strokeWidth:2,fillStyle:"solid"}),x=d.insert(()=>m,":first-child");if(x.insert(()=>v),t.look!=="handDrawn"&&x.attr("class","outer-path"),a&&x.selectAll("path").attr("style",a),i&&x.selectAll("path").attr("style",i),t.width<25&&h&&t.look!=="handDrawn"){let b=e.node()?.ownerSVGElement?.id??"",T=b?`${b}-drop-shadow-small`:"drop-shadow-small";x.attr("style",`filter:url(#${T})`)}return ht(t,x),t.intersect=function(b){return it.circle(t,(t.width??0)/2,b)},d}var _le=F(()=>{"use strict";Kt();Zt();Xt();Ht();s(Rle,"stateEnd")});function Lle(e,t,{config:{themeVariables:r}}){let{lineColor:n,nodeShadow:i}=r;(t.width||t.height)&&((t.width??0)<14&&(t.width=14),(t.height??0)<14&&(t.height=14)),t.width||(t.width=14),t.height||(t.height=14);let a=e.insert("g").attr("class","node default").attr("id",t.domId||t.id),o;if(t.look==="handDrawn"){let u=ot.svg(a).circle(0,0,t.width,hse(n));o=a.insert(()=>u),o.attr("class","state-start").attr("r",(t.width??7)/2).attr("width",t.width??14).attr("height",t.height??14)}else o=a.insert("circle",":first-child"),o.attr("class","state-start").attr("r",(t.width??7)/2).attr("width",t.width??14).attr("height",t.height??14);if(t.width<25&&i&&t.look!=="handDrawn"){let l=e.node()?.ownerSVGElement?.id??"",u=l?`${l}-drop-shadow-small`:"drop-shadow-small";o.attr("style",`filter:url(#${u})`)}return ht(t,o),t.intersect=function(l){return it.circle(t,(t.width??7)/2,l)},a}var Dle=F(()=>{"use strict";Kt();Zt();Xt();Ht();s(Lle,"stateStart")});async function Ile(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t?.padding??8,a=t.look==="neo"?28:i,o=t.look==="neo"?12:i,{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=Math.max(u.width+2*Oy+a,t.width??0),d=Math.max(u.height+o,t.height??0),f=h-2*Oy,p=d,m=-h/2,g=-d/2,y=[{x:0,y:0},{x:f,y:0},{x:f,y:-p},{x:0,y:-p},{x:0,y:0},{x:-8,y:0},{x:f+8,y:0},{x:f+8,y:-p},{x:-8,y:-p},{x:-8,y:0}];if(t.look==="handDrawn"){let v=ot.svg(l),x=ft(t,{}),b=v.rectangle(m,g,f+16,p,x),T=v.line(m+Oy,g,m+Oy,g+p,x),w=v.line(m+Oy+f,g,m+Oy+f,g+p,x);l.insert(()=>T,":first-child"),l.insert(()=>w,":first-child");let C=l.insert(()=>b,":first-child"),{cssStyles:S}=t;C.attr("class","basic label-container").attr("style",un(S)),ht(t,C)}else{let v=Na(l,f,p,y);n&&v.attr("style",n),ht(t,v)}return t.intersect=function(v){return it.polygon(t,y,v)},l}var Oy,Mle=F(()=>{"use strict";Ht();Zt();Xt();Kt();iu();er();Oy=8;s(Ile,"subroutine")});async function Nle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?12:i;(t.width||t.height)&&(t.height=Math.max((t?.height??0)-o*2,10),t.width=Math.max((t?.width??0)-a*2-FP*(t.height+o*2),10));let{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=(t?.height?t?.height:u.height)+o*2,d=FP*h,f=FP*h,m=(t?.width?t?.width:u.width)+a*2+d-d,g=h,y=-m/2,v=-g/2,{cssStyles:x}=t,b=ot.svg(l),T=ft(t,{}),w=[{x:y-d/2,y:v},{x:y+m+d/2,y:v},{x:y+m+d/2,y:v+g},{x:y-d/2,y:v+g}],C=[{x:y+m-d/2,y:v+g},{x:y+m+d/2,y:v+g},{x:y+m+d/2,y:v+g-f}];t.look!=="handDrawn"&&(T.roughness=0,T.fillStyle="solid");let S=hr(w),k=b.path(S,T),R=hr(C),M=b.path(R,{...T,fillStyle:"solid"}),P=l.insert(()=>M,":first-child");return P.insert(()=>k,":first-child"),P.attr("class","basic label-container outer-path"),x&&t.look!=="handDrawn"&&P.selectAll("path").attr("style",x),n&&t.look!=="handDrawn"&&P.selectAll("path").attr("style",n),ht(t,P),t.intersect=function(N){return it.polygon(t,w,N)},l}var FP,Ple=F(()=>{"use strict";Ht();Xt();Kt();Zt();FP=.2;s(Nle,"taggedRect")});async function Ole(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,label:o}=await yt(e,t,vt(t)),l=Math.max(a.width+(t.padding??0)*2,t?.width??0),u=Math.max(a.height+(t.padding??0)*2,t?.height??0),h=u/8,d=.2*l,f=.2*u,p=u+h,{cssStyles:m}=t,g=ot.svg(i),y=ft(t,{});t.look!=="handDrawn"&&(y.roughness=0,y.fillStyle="solid");let v=[{x:-l/2-l/2*.1,y:p/2},...vl(-l/2-l/2*.1,p/2,l/2+l/2*.1,p/2,h,.8),{x:l/2+l/2*.1,y:-p/2},{x:-l/2-l/2*.1,y:-p/2}],x=-l/2+l/2*.1,b=-p/2-f*.4,T=[{x:x+l-d,y:(b+u)*1.3},{x:x+l,y:b+u-f},{x:x+l,y:(b+u)*.9},...vl(x+l,(b+u)*1.25,x+l-d,(b+u)*1.3,-u*.02,.5)],w=hr(v),C=g.path(w,y),S=hr(T),k=g.path(S,{...y,fillStyle:"solid"}),R=i.insert(()=>k,":first-child");return R.insert(()=>C,":first-child"),R.attr("class","basic label-container outer-path"),m&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",m),n&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",n),R.attr("transform",`translate(0,${-h/2})`),o.attr("transform",`translate(${-l/2+(t.padding??0)-(a.x-(a.left??0))},${-u/2+(t.padding??0)-h/2-(a.y-(a.top??0))})`),ht(t,R),t.intersect=function(M){return it.polygon(t,v,M)},i}var Ble=F(()=>{"use strict";Ht();Zt();Kt();Xt();s(Ole,"taggedWaveEdgedRectangle")});async function $le(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let{shapeSvg:i,bbox:a}=await yt(e,t,vt(t)),o=Math.max(a.width+(t.padding??0),t?.width||0),l=Math.max(a.height+(t.padding??0),t?.height||0),u=-o/2,h=-l/2,d=i.insert("rect",":first-child");return d.attr("class","text").attr("style",n).attr("rx",0).attr("ry",0).attr("x",u).attr("y",h).attr("width",o).attr("height",l),ht(t,d),t.intersect=function(f){return it.rect(t,f)},i}var Fle=F(()=>{"use strict";Ht();Zt();Xt();s($le,"text")});async function Vle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?12:i/2,o=t.height??0;if(t.height&&(t.height=t.height-a,t.heightw,":first-child"),y=l.insert(()=>T,":first-child"),y.attr("class","basic label-container"),g&&y.attr("style",g)}else{let v=zKe(0,0,m,d,p,f);y=l.insert("path",":first-child").attr("d",v).attr("class","basic label-container").attr("style",un(g)).attr("style",n),y.attr("class","basic label-container outer-path"),g&&y.selectAll("path").attr("style",g),n&&y.selectAll("path").attr("style",n)}return y.attr("label-offset-x",p),y.attr("transform",`translate(${-m/2}, ${d/2} )`),h.attr("transform",`translate(${-(u.width/2)-p-(u.x-(u.left??0))}, ${-(u.height/2)-(u.y-(u.top??0))})`),ht(t,y),t.intersect=function(v){let x=it.rect(t,v),b=x.y-(t.y??0);if(f!=0&&(Math.abs(b)<(t.height??0)/2||Math.abs(b)==(t.height??0)/2&&Math.abs(x.x-(t.x??0))>(t.width??0)/2-p)){let T=p*p*(1-b*b/(f*f));T!=0&&(T=Math.sqrt(Math.abs(T))),T=p-T,v.x-(t.x??0)>0&&(T=-T),x.x+=T}return x},l}var zKe,VKe,WKe,Gle,zle,Wle=F(()=>{"use strict";Ht();Xt();Kt();Zt();er();zKe=s((e,t,r,n,i,a)=>`M${e},${t} + a${i},${a} 0,0,1 0,${-n} + l${r},0 + a${i},${a} 0,0,1 0,${n} + M${r},${-n} + a${i},${a} 0,0,0 0,${n} + l${-r},0`,"createCylinderPathD"),VKe=s((e,t,r,n,i,a)=>[`M${e},${t}`,`M${e+r},${t}`,`a${i},${a} 0,0,0 0,${-n}`,`l${-r},0`,`a${i},${a} 0,0,0 0,${n}`,`l${r},0`].join(" "),"createOuterCylinderPathD"),WKe=s((e,t,r,n,i,a)=>[`M${e+r/2},${-n/2}`,`a${i},${a} 0,0,0 0,${n}`].join(" "),"createInnerCylinderPathD"),Gle=5,zle=10;s(Vle,"tiltedCylinder")});async function qle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=(t.look==="neo",i),o=t.look==="neo"?i*2:i,{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=Math.max(u.height+a,t.height??0),d=Math.max(u.width+o,(t.width??0)-h),f=[{x:-3*h/6,y:0},{x:d+3*h/6,y:0},{x:d,y:-h},{x:0,y:-h}],p,{cssStyles:m}=t;if(t.look==="handDrawn"){let g=ot.svg(l),y=ft(t,{}),v=hr(f),x=g.path(v,y);p=l.insert(()=>x,":first-child").attr("transform",`translate(${-d/2}, ${h/2})`),m&&p.attr("style",m)}else p=Na(l,d,h,f);return n&&p.attr("style",n),t.width=d,t.height=h,ht(t,p),t.intersect=function(g){return it.polygon(t,f,g)},l}var Hle=F(()=>{"use strict";Ht();Zt();Xt();Kt();iu();s(qle,"trapezoid")});async function Ule(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?12:i,l=15,u=5;(t.width||t.height)&&(t.height=(t.height??0)-o*2,t.heightb,":first-child");return T.attr("class","basic label-container outer-path"),m&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",m),n&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",n),ht(t,T),t.intersect=function(w){return it.polygon(t,v,w)},h}var Yle=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(Ule,"trapezoidalPentagon")});async function Kle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?i*2:i;(t.width||t.height)&&(t.width=((t?.width??0)-a)/2,t.widthb,":first-child").attr("transform",`translate(${-f/2}, ${f/2})`).attr("class","outer-path");return g&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",g),n&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",n),t.width=d,t.height=f,ht(t,T),u.attr("transform",`translate(${-l.width/2-(l.x-(l.left??0))}, ${f/2-(l.height+(t.padding??0)/(h?2:1)-(l.y-(l.top??0)))})`),t.intersect=function(w){return se.info("Triangle intersect",t,m,w),it.polygon(t,m,w)},o}var jle,Xle,Zle=F(()=>{"use strict";St();Ht();Zt();Xt();Kt();Ht();Yr();Jt();jle=10,Xle=10;s(Kle,"triangle")});async function Qle(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?12:i,l=!0;(t.width||t.height)&&(l=!1,t.width=(t?.width??0)-a*2,t.width<10&&(t.width=10),t.height=(t?.height??0)-o*2,t.height<10&&(t.height=10));let{shapeSvg:u,bbox:h,label:d}=await yt(e,t,vt(t)),f=(t?.width?t?.width:h.width)+(a??0)*2,p=(t?.height?t?.height:h.height)+(o??0)*2,m=t.look==="neo"?p/4:p/8,g=p+(l?m:-m),{cssStyles:y}=t,x=14-f,b=x>0?x/2:0,T=ot.svg(u),w=ft(t,{});t.look!=="handDrawn"&&(w.roughness=0,w.fillStyle="solid");let C=[{x:-f/2-b,y:g/2},...vl(-f/2-b,g/2,f/2+b,g/2,m,.8),{x:f/2+b,y:-g/2},{x:-f/2-b,y:-g/2}],S=hr(C),k=T.path(S,w),R=u.insert(()=>k,":first-child");return R.attr("class","basic label-container outer-path"),y&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",y),n&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",n),R.attr("transform",`translate(0,${-m/2})`),d.attr("transform",`translate(${-f/2+(t.padding??0)-(h.x-(h.left??0))},${-p/2+(t.padding??0)-m-(h.y-(h.top??0))})`),ht(t,R),t.intersect=function(M){return it.polygon(t,C,M)},u}var Jle=F(()=>{"use strict";Ht();Zt();Kt();Xt();s(Qle,"waveEdgedRectangle")});async function ece(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,o=t.look==="neo"?20:i;if(t.width||t.height){t.width=t?.width??0,t.width<20&&(t.width=20),t.height=t?.height??0,t.height<10&&(t.height=10);let w=Math.min(t.height*.2,t.height/4);t.height=Math.ceil(t.height-o-w*(20/9)),t.width=t.width-a*2}let{shapeSvg:l,bbox:u}=await yt(e,t,vt(t)),h=(t?.width?t?.width:u.width)+a*2,d=(t?.height?t?.height:u.height)+o,f=d/8,p=d+f*2,{cssStyles:m}=t,g=ot.svg(l),y=ft(t,{});t.look!=="handDrawn"&&(y.roughness=0,y.fillStyle="solid");let v=[{x:-h/2,y:p/2},...vl(-h/2,p/2,h/2,p/2,f,1),{x:h/2,y:-p/2},...vl(h/2,-p/2,-h/2,-p/2,f,-1)],x=hr(v),b=g.path(x,y),T=l.insert(()=>b,":first-child");return T.attr("class","basic label-container"),m&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",m),n&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",n),ht(t,T),t.intersect=function(w){return it.polygon(t,v,w)},l}var tce=F(()=>{"use strict";Ht();Zt();Xt();Kt();s(ece,"waveRectangle")});async function rce(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t.look==="neo"?16:t.padding??0,a=t.look==="neo"?12:t.padding??0;(t.width||t.height)&&(t.width=Math.max((t?.width??0)-i*2-Ni,10),t.height=Math.max((t?.height??0)-a*2-Ni,10));let{shapeSvg:o,bbox:l,label:u}=await yt(e,t,vt(t)),h=(t?.width?t?.width:l.width)+i*2+Ni,d=(t?.height?t?.height:l.height)+a*2+Ni,f=h-Ni,p=d-Ni,m=-f/2,g=-p/2,{cssStyles:y}=t,v=ot.svg(o),x=ft(t,{}),b=[{x:m-Ni,y:g-Ni},{x:m-Ni,y:g+p},{x:m+f,y:g+p},{x:m+f,y:g-Ni}],T=`M${m-Ni},${g-Ni} L${m+f},${g-Ni} L${m+f},${g+p} L${m-Ni},${g+p} L${m-Ni},${g-Ni} + M${m-Ni},${g} L${m+f},${g} + M${m},${g-Ni} L${m},${g+p}`;t.look!=="handDrawn"&&(x.roughness=0,x.fillStyle="solid");let w=v.path(T,x),C=o.insert(()=>w,":first-child");return C.attr("transform",`translate(${Ni/2}, ${Ni/2})`),C.attr("class","basic label-container outer-path"),y&&t.look!=="handDrawn"&&C.selectAll("path").attr("style",y),n&&t.look!=="handDrawn"&&C.selectAll("path").attr("style",n),u.attr("transform",`translate(${-(l.width/2)+Ni/2-(l.x-(l.left??0))}, ${-(l.height/2)+Ni/2-(l.y-(l.top??0))})`),ht(t,C),t.intersect=function(S){return it.polygon(t,b,S)},o}var Ni,nce=F(()=>{"use strict";Ht();Xt();Kt();Zt();Ni=10;s(rce,"windowPane")});async function GP(e,t){let r=t;r.alias&&(t.label=r.alias);let{theme:n,themeVariables:i}=Lt(),{rowEven:a,rowOdd:o,nodeBorder:l,borderColorArray:u}=i;if(t.look==="handDrawn"){let{themeVariables:ne}=Lt(),{background:Z}=ne,pe={...t,id:t.id+"-background",domId:(t.domId||t.id)+"-background",look:"default",cssStyles:["stroke: none",`fill: ${Z}`]};await GP(e,pe)}let h=Lt();t.useHtmlLabels=h.htmlLabels;let d=h.er?.diagramPadding??10,f=h.er?.entityPadding??6,{cssStyles:p}=t,{labelStyles:m,nodeStyles:g}=st(t);if(r.attributes.length===0&&t.label){let ne={rx:0,ry:0,labelPaddingX:d,labelPaddingY:d*1.5,classes:""};Qa(t.label,h)+ne.labelPaddingX*20){let ne=x.width+d*2-(C+S+k+R);C+=ne/N,S+=ne/N,k>0&&(k+=ne/N),R>0&&(R+=ne/N)}let A=C+S+k+R,I=ot.svg(v),D=ft(t,{});t.look!=="handDrawn"&&(D.roughness=0,D.fillStyle="solid");let L=0;w.length>0&&(L=w.reduce((ne,Z)=>ne+(Z?.rowHeight??0),0));let B=Math.max(_.width+d*2,t?.width||0,A),O=Math.max((L??0)+x.height,t?.height||0),$=-B/2,W=-O/2;if(v.selectAll("g:not(:first-child)").each((ne,Z,pe)=>{let te=ut(pe[Z]),fe=te.attr("transform"),ce=0,de=0;if(fe){let we=RegExp(/translate\(([^,]+),([^)]+)\)/).exec(fe);we&&(ce=parseFloat(we[1]),de=parseFloat(we[2]),te.attr("class").includes("attribute-name")?ce+=C:te.attr("class").includes("attribute-keys")?ce+=C+S:te.attr("class").includes("attribute-comment")&&(ce+=C+S+k))}te.attr("transform",`translate(${$+d/2+ce}, ${de+W+x.height+f/2})`)}),v.select(".name").attr("transform","translate("+-x.width/2+", "+(W+f/2)+")"),n!=null&&ice.has(n)){let ne=r.colorIndex??0;v.attr("data-color-id",`color-${ne%u.length}`)}let V=I.rectangle($,W,B,O,D),z=v.insert(()=>V,":first-child").attr("class","outer-path").attr("style",p.join(""));T.push(0);for(let[ne,Z]of w.entries()){let te=(ne+1)%2===0&&Z.yOffset!==0,fe=I.rectangle($,x.height+W+Z?.yOffset,B,Z?.rowHeight,{...D,fill:te?a:o,stroke:l});v.insert(()=>fe,"g.label").attr("style",p.join("")).attr("class",`row-rect-${te?"even":"odd"}`)}let H=1e-4,K=_2($,x.height+W,B+$,x.height+W,H),J=I.polygon(K.map(ne=>[ne.x,ne.y]),D);if(v.insert(()=>J).attr("class","divider"),K=_2(C+$,x.height+W,C+$,O+W,H),J=I.polygon(K.map(ne=>[ne.x,ne.y]),D),v.insert(()=>J).attr("class","divider"),M){let ne=C+S+$;K=_2(ne,x.height+W,ne,O+W,H),J=I.polygon(K.map(Z=>[Z.x,Z.y]),D),v.insert(()=>J).attr("class","divider")}if(P){let ne=C+S+k+$;K=_2(ne,x.height+W,ne,O+W,H),J=I.polygon(K.map(Z=>[Z.x,Z.y]),D),v.insert(()=>J).attr("class","divider")}for(let ne of T){let Z=x.height+W+ne;K=_2($,Z,B+$,Z,H),J=I.polygon(K.map(pe=>[pe.x,pe.y]),D),v.insert(()=>J).attr("class","divider")}if(ht(t,z),g&&t.look!=="handDrawn")if(n!=null&&qKe.has(n))v.selectAll("path").attr("style",g);else{let Z=g.split(";")?.filter(pe=>pe.includes("stroke"))?.map(pe=>`${pe}`).join("; ");v.selectAll("path").attr("style",Z??""),v.selectAll(".row-rect-even path").attr("style",g)}return t.intersect=function(ne){return it.rect(t,ne)},v}async function R2(e,t,r,n=0,i=0,a=[],o=""){let l=e.insert("g").attr("class",`label ${a.join(" ")}`).attr("transform",`translate(${n}, ${i})`).attr("style",o);t!==Vc(t)&&(t=Vc(t),t=t.replaceAll("<","<").replaceAll(">",">"));let u=l.node().appendChild(await li(l,t,{width:Qa(t,r)+100,style:o,useHtmlLabels:r.htmlLabels},r));if(t.includes("<")||t.includes(">")){let d=u.children[0];for(d.textContent=d.textContent.replaceAll("<","<").replaceAll(">",">");d.childNodes[0];)d=d.childNodes[0],d.textContent=d.textContent.replaceAll("<","<").replaceAll(">",">")}let h=u.getBBox();if(Aa(r.htmlLabels)){let d=u.children[0];d.style.textAlign="start";let f=ut(u);h=d.getBoundingClientRect(),f.attr("width",h.width),f.attr("height",h.height)}return h}function _2(e,t,r,n,i){return e===r?[{x:e-i/2,y:t},{x:e+i/2,y:t},{x:r+i/2,y:n},{x:r-i/2,y:n}]:[{x:e,y:t-i/2},{x:e,y:t+i/2},{x:r,y:n+i/2},{x:r,y:n-i/2}]}var ice,qKe,ace=F(()=>{"use strict";Ht();Zt();Xt();Kt();km();br();qo();Yr();Wr();er();ice=new Set(["redux-color","redux-dark-color"]),qKe=new Set(["redux","redux-dark","redux-color","redux-dark-color"]);s(GP,"erBox");s(R2,"addText");s(_2,"lineToPolygon")});async function sce(e,t,r,n,i=r.class.padding??12){let a=n?0:3,o=e.insert("g").attr("class",vt(t)).attr("id",t.domId||t.id),l=null,u=null,h=null,d=null,f=0,p=0,m=0;if(l=o.insert("g").attr("class","annotation-group text"),t.annotations.length>0){let b=t.annotations[0];await A4(l,{text:`\xAB${b}\xBB`},0),f=l.node().getBBox().height}u=o.insert("g").attr("class","label-group text"),await A4(u,t,0,["font-weight: bolder"]);let g=u.node().getBBox();p=g.height,h=o.insert("g").attr("class","members-group text");let y=0;for(let b of t.members){let T=await A4(h,b,y,[b.parseClassifier()]);y+=T+a}m=h.node().getBBox().height,m<=0&&(m=i/2),d=o.insert("g").attr("class","methods-group text");let v=0;for(let b of t.methods){let T=await A4(d,b,v,[b.parseClassifier()]);v+=T+a}let x=o.node().getBBox();if(l!==null){let b=l.node().getBBox();l.attr("transform",`translate(${-b.width/2})`)}return u.attr("transform",`translate(${-g.width/2}, ${f})`),x=o.node().getBBox(),h.attr("transform",`translate(0, ${f+p+i*2})`),x=o.node().getBBox(),d.attr("transform",`translate(0, ${f+p+(m?m+i*4:i*2)})`),x=o.node().getBBox(),{shapeSvg:o,bbox:x}}async function A4(e,t,r,n=[]){let i=e.insert("g").attr("class","label").attr("style",n.join("; ")),a=Lt(),o="useHtmlLabels"in t?t.useHtmlLabels:Aa(a.htmlLabels)??!0,l="";"text"in t?l=t.text:l=t.label,!o&&l.startsWith("\\")&&(l=l.substring(1)),si(l)&&(o=!0);let u=await li(i,ab(lo(l)),{width:Qa(l,a)+50,classes:"markdown-node-label",useHtmlLabels:o},a),h,d=1;if(o){let f=u.children[0],p=ut(u);d=f.innerHTML.split("
    ").length,f.innerHTML.includes("")&&(d+=f.innerHTML.split("").length-1),await h4(f),h=f.getBoundingClientRect(),p.attr("width",h.width),p.attr("height",h.height)}else{n.includes("font-weight: bolder")&&ut(u).selectAll("tspan").attr("font-weight",""),d=u.children.length;let f=u.children[0];(u.textContent===""||u.textContent.includes(">"))&&(f.textContent=l[0]+l.substring(1).replaceAll(">",">").replaceAll("<","<").trim(),l[1]===" "&&(f.textContent=f.textContent[0]+" "+f.textContent.substring(1))),f.textContent==="undefined"&&(f.textContent=""),h=u.getBBox()}return i.attr("transform","translate(0,"+(-h.height/(2*d)+r)+")"),h.height}var oce=F(()=>{"use strict";Wr();br();Ht();er();Jt();qo();pP();Yr();s(sce,"textHelper");s(A4,"addText")});async function lce(e,t){let r=Pe(),{themeVariables:n}=r,{useGradient:i}=n,a=r.class.padding??12,o=a,l=t.useHtmlLabels??Aa(r.htmlLabels)??!0,u=t;u.annotations=u.annotations??[],u.members=u.members??[],u.methods=u.methods??[];let{shapeSvg:h,bbox:d}=await sce(e,t,r,l,o),{labelStyles:f,nodeStyles:p}=st(t);t.labelStyle=f,t.cssStyles=u.styles||"";let m=u.styles?.join(";")||p||"";t.cssStyles||(t.cssStyles=m.replaceAll("!important","").split(";"));let g=u.members.length===0&&u.methods.length===0&&!r.class?.hideEmptyMembersBox,y=ot.svg(h),v=ft(t,{});t.look!=="handDrawn"&&(v.roughness=0,v.fillStyle="solid");let x=Math.max(t.width??0,d.width),b=Math.max(t.height??0,d.height),T=(t.height??0)>d.height;u.members.length===0&&u.methods.length===0?b+=o:u.members.length>0&&u.methods.length===0&&(b+=o*2);let w=-x/2,C=-b/2,S=g?a*2:u.members.length===0&&u.methods.length===0?-a:0;T&&(S=a*2);let k=y.rectangle(w-a,C-a-(g?a:u.members.length===0&&u.methods.length===0?-a/2:0),x+2*a,b+2*a+S,v),R=h.insert(()=>k,":first-child");R.attr("class","basic label-container outer-path");let M=R.node().getBBox(),P=h.select(".annotation-group").node().getBBox().height-(g?a/2:0)||0,N=h.select(".label-group").node().getBBox().height-(g?a/2:0)||0,_=h.select(".members-group").node().getBBox().height-(g?a/2:0)||0,A=(P+N+C+a-(C-a-(g?a:u.members.length===0&&u.methods.length===0?-a/2:0)))/2;if(h.selectAll(".text").each((I,D,L)=>{let B=ut(L[D]),O=B.attr("transform"),$=0;if(O){let H=RegExp(/translate\(([^,]+),([^)]+)\)/).exec(O);H&&($=parseFloat(H[2]))}let W=$+C+a-(g?a:u.members.length===0&&u.methods.length===0?-a/2:0);if(B.attr("class").includes("methods-group")){let z=Math.max(_,o/2);T?W=Math.max(A,P+N+z+C+o*2+a)+o*2:W=P+N+z+C+o*4+a}u.members.length===0&&u.methods.length===0&&r.class?.hideEmptyMembersBox&&(u.annotations.length>0?W=$-o:W=$),l||(W-=4);let V=w;(B.attr("class").includes("label-group")||B.attr("class").includes("annotation-group"))&&(V=-B.node()?.getBBox().width/2||0,h.selectAll("text").each(function(z,H,K){window.getComputedStyle(K[H]).textAnchor==="middle"&&(V=0)})),B.attr("transform",`translate(${V}, ${W})`)}),u.members.length>0||u.methods.length>0||g){let I=P+N+C+a,D=y.line(M.x,I,M.x+M.width,I+.001,v);h.insert(()=>D).attr("class",`divider${t.look==="neo"&&!i?" neo-line":""}`).attr("style",m)}if(g||u.members.length>0||u.methods.length>0){let I=P+N+_+C+o*2+a,D=y.line(M.x,T?Math.max(A,I):I,M.x+M.width,(T?Math.max(A,I):I)+.001,v);h.insert(()=>D).attr("class",`divider${t.look==="neo"&&!i?" neo-line":""}`).attr("style",m)}if(u.look!=="handDrawn"&&h.selectAll("path").attr("style",m),R.select(":nth-child(2)").attr("style",m),h.selectAll(".divider").select("path").attr("style",m),t.labelStyle?h.selectAll("span").attr("style",t.labelStyle):h.selectAll("span").attr("style",m),!l){let I=RegExp(/color\s*:\s*([^;]*)/),D=I.exec(m);if(D){let L=D[0].replace("color","fill");h.selectAll("tspan").attr("style",L)}else if(f){let L=I.exec(f);if(L){let B=L[0].replace("color","fill");h.selectAll("tspan").attr("style",B)}}}return ht(t,R),t.intersect=function(I){return it.rect(t,I)},h}var cce=F(()=>{"use strict";Ht();Jt();Wr();Kt();Xt();Zt();oce();Yr();s(lce,"classBox")});async function uce(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let i=t,a=t,o=20,l=20,u="verifyMethod"in t,h=vt(t),d=Pe(),{themeVariables:f}=d,{borderColorArray:p,requirementEdgeLabelBackground:m}=f,g=d.layout==="elk"?"start":"center",y=e.insert("g").attr("class",h).attr("id",t.domId??t.id),v;u?v=await wh(y,`<<${i.type}>>`,0,t.labelStyle):v=await wh(y,"<<Element>>",0,t.labelStyle);let x=v,b=await wh(y,i.name,x,t.labelStyle+"; font-weight: bold;");if(x+=b+l,u){let N=await wh(y,`${i.requirementId?`ID: ${i.requirementId}`:""}`,x,t.labelStyle,g);x+=N;let _=await wh(y,`${i.text?`Text: ${i.text}`:""}`,x,t.labelStyle,g);x+=_;let A=await wh(y,`${i.risk?`Risk: ${i.risk}`:""}`,x,t.labelStyle,g);x+=A,await wh(y,`${i.verifyMethod?`Verification: ${i.verifyMethod}`:""}`,x,t.labelStyle,g)}else{let N=await wh(y,`${a.type?`Type: ${a.type}`:""}`,x,t.labelStyle,g);x+=N,await wh(y,`${a.docRef?`Doc Ref: ${a.docRef}`:""}`,x,t.labelStyle,g)}let T=(y.node()?.getBBox().width??200)+o,w=(y.node()?.getBBox().height??200)+o,C=-T/2,S=-w/2,k=ot.svg(y),R=ft(t,{});t.look!=="handDrawn"&&(R.roughness=0,R.fillStyle="solid");let M=k.rectangle(C,S,T,w,R),P=y.insert(()=>M,":first-child");if(P.attr("class","basic label-container outer-path").attr("style",n),p?.length){let N=t.colorIndex??0;y.attr("data-color-id",`color-${N%p.length}`)}if(y.selectAll(".label").each((N,_,A)=>{let I=ut(A[_]),D=I.attr("transform"),L=0,B=0;if(D){let V=RegExp(/translate\(([^,]+),([^)]+)\)/).exec(D);V&&(L=parseFloat(V[1]),B=parseFloat(V[2]))}let O=B-w/2,$=C+o/2;(_===0||_===1)&&($=L),I.attr("transform",`translate(${$}, ${O+o})`)}),x>v+b+l){let N=S+v+b+l,_;if(t.look==="neo"){let D=[[C,N],[C+T,N],[C+T,N+.001],[C,N+.001]];_=k.polygon(D,R)}else _=k.line(C,N,C+T,N,R);y.insert(()=>_).attr("class","divider")}return ht(t,P),t.intersect=function(N){return it.rect(t,N)},n&&t.look!=="handDrawn"&&(m||p?.length)&&y.selectAll("path").attr("style",n),y}async function wh(e,t,r,n="",i="center"){if(t==="")return 0;let a=e.insert("g").attr("class","label").attr("style",n),o=Pe(),l=o.htmlLabels??!0,u=await li(a,ab(lo(t)),{width:Qa(t,o)+50,classes:"markdown-node-label",useHtmlLabels:l,style:n},o),h;if(l){let d=u.children[0],f=ut(u);i==="start"&&ut(d).style("text-align","left"),h=d.getBoundingClientRect(),f.attr("width",h.width),f.attr("height",h.height)}else{let d=u.children[0];for(let f of d.children)n&&f.setAttribute("style",n);if(i==="start"){d.setAttribute("text-anchor","start");for(let f of d.children)f.setAttribute("text-anchor","start")}h=u.getBBox(),h.height+=6}return a.attr("transform",`translate(${-h.width/2},${-h.height/2+r})`),h.height}var hce=F(()=>{"use strict";Ht();Zt();Xt();Kt();er();Jt();qo();Wr();s(uce,"requirementBox");s(wh,"addText")});async function dce(e,t,{config:r}){let{labelStyles:n,nodeStyles:i}=st(t);t.labelStyle=n||"";let a=10,o=t.width;t.width=(t.width??200)-10;let{shapeSvg:l,bbox:u,label:h}=await yt(e,t,vt(t)),d=t.padding||10,f="",p;"ticket"in t&&t.ticket&&r?.kanban?.ticketBaseUrl&&(f=r?.kanban?.ticketBaseUrl.replace("#TICKET#",t.ticket),p=l.insert("svg:a",":first-child").attr("class","kanban-ticket-link").attr("xlink:href",f).attr("target","_blank"));let m={useHtmlLabels:t.useHtmlLabels,labelStyle:t.labelStyle||"",width:t.width,img:t.img,padding:t.padding||8,centerLabel:!1},g,y;p?{label:g,bbox:y}=await d4(p,"ticket"in t&&t.ticket||"",m):{label:g,bbox:y}=await d4(l,"ticket"in t&&t.ticket||"",m);let{label:v,bbox:x}=await d4(l,"assigned"in t&&t.assigned||"",m);t.width=o;let b=10,T=t?.width||0,w=Math.max(y.height,x.height)/2,C=Math.max(u.height+b*2,t?.height||0)+w,S=-T/2,k=-C/2;h.attr("transform","translate("+(d-T/2)+", "+(-w-u.height/2)+")"),g.attr("transform","translate("+(d-T/2)+", "+(-w+u.height/2)+")"),v.attr("transform","translate("+(d+T/2-x.width-2*a)+", "+(-w+u.height/2)+")");let R,{rx:M,ry:P}=t,{cssStyles:N}=t;if(t.look==="handDrawn"){let _=ot.svg(l),A=ft(t,{}),I=M||P?_.path(Mi(S,k,T,C,M||0),A):_.rectangle(S,k,T,C,A);R=l.insert(()=>I,":first-child"),R.attr("class","basic label-container").attr("style",N||null)}else{R=l.insert("rect",":first-child"),R.attr("class","basic label-container __APA__").attr("style",i).attr("rx",M??5).attr("ry",P??5).attr("x",S).attr("y",k).attr("width",T).attr("height",C);let _="priority"in t&&t.priority;if(_){let A=l.append("line"),I=S+2,D=k+Math.floor((M??0)/2),L=k+C-Math.floor((M??0)/2);A.attr("x1",I).attr("y1",D).attr("x2",I).attr("y2",L).attr("stroke-width","4").attr("stroke",HKe(_))}}return ht(t,R),t.height=C,t.intersect=function(_){return it.rect(t,_)},l}var HKe,fce=F(()=>{"use strict";Ht();Zt();ic();Xt();Kt();HKe=s(e=>{switch(e){case"Very High":return"red";case"High":return"orange";case"Medium":return null;case"Low":return"blue";case"Very Low":return"lightblue"}},"colorFromPriority");s(dce,"kanbanItem")});async function pce(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,halfPadding:o,label:l}=await yt(e,t,vt(t)),u=a.width+10*o,h=a.height+8*o,d=.15*u,{cssStyles:f}=t,p=a.width+20,m=a.height+20,g=Math.max(u,p),y=Math.max(h,m);l.attr("transform",`translate(${-a.width/2}, ${-a.height/2})`);let v,x=`M0 0 + a${d},${d} 1 0,0 ${g*.25},${-1*y*.1} + a${d},${d} 1 0,0 ${g*.25},0 + a${d},${d} 1 0,0 ${g*.25},0 + a${d},${d} 1 0,0 ${g*.25},${y*.1} + + a${d},${d} 1 0,0 ${g*.15},${y*.33} + a${d*.8},${d*.8} 1 0,0 0,${y*.34} + a${d},${d} 1 0,0 ${-1*g*.15},${y*.33} + + a${d},${d} 1 0,0 ${-1*g*.25},${y*.15} + a${d},${d} 1 0,0 ${-1*g*.25},0 + a${d},${d} 1 0,0 ${-1*g*.25},0 + a${d},${d} 1 0,0 ${-1*g*.25},${-1*y*.15} + + a${d},${d} 1 0,0 ${-1*g*.1},${-1*y*.33} + a${d*.8},${d*.8} 1 0,0 0,${-1*y*.34} + a${d},${d} 1 0,0 ${g*.1},${-1*y*.33} + H0 V0 Z`;if(t.look==="handDrawn"){let b=ot.svg(i),T=ft(t,{}),w=b.path(x,T);v=i.insert(()=>w,":first-child"),v.attr("class","basic label-container").attr("style",un(f))}else v=i.insert("path",":first-child").attr("class","basic label-container").attr("style",n).attr("d",x);return v.attr("transform",`translate(${-g/2}, ${-y/2})`),ht(t,v),t.calcIntersect=function(b,T){return it.rect(b,T)},t.intersect=function(b){return se.info("Bang intersect",t,b),it.rect(t,b)},i}var mce=F(()=>{"use strict";St();Ht();Zt();Xt();Kt();er();s(pce,"bang")});async function gce(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,halfPadding:o,label:l}=await yt(e,t,vt(t)),u=a.width+2*o,h=a.height+2*o,d=.15*u,f=.25*u,p=.35*u,m=.2*u,{cssStyles:g}=t,y,v=`M0 0 + a${d},${d} 0 0,1 ${u*.25},${-1*u*.1} + a${p},${p} 1 0,1 ${u*.4},${-1*u*.1} + a${f},${f} 1 0,1 ${u*.35},${u*.2} + + a${d},${d} 1 0,1 ${u*.15},${h*.35} + a${m},${m} 1 0,1 ${-1*u*.15},${h*.65} + + a${f},${d} 1 0,1 ${-1*u*.25},${u*.15} + a${p},${p} 1 0,1 ${-1*u*.5},0 + a${d},${d} 1 0,1 ${-1*u*.25},${-1*u*.15} + + a${d},${d} 1 0,1 ${-1*u*.1},${-1*h*.35} + a${m},${m} 1 0,1 ${u*.1},${-1*h*.65} + H0 V0 Z`;if(t.look==="handDrawn"){let x=ot.svg(i),b=ft(t,{}),T=x.path(v,b);y=i.insert(()=>T,":first-child"),y.attr("class","basic label-container").attr("style",un(g))}else y=i.insert("path",":first-child").attr("class","basic label-container").attr("style",n).attr("d",v);return l.attr("transform",`translate(${-a.width/2}, ${-a.height/2})`),y.attr("transform",`translate(${-u/2}, ${-h/2})`),ht(t,y),t.calcIntersect=function(x,b){return it.rect(x,b)},t.intersect=function(x){return se.info("Cloud intersect",t,x),it.rect(t,x)},i}var yce=F(()=>{"use strict";Kt();St();er();Zt();Xt();Ht();s(gce,"cloud")});async function vce(e,t){let{labelStyles:r,nodeStyles:n}=st(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,halfPadding:o,label:l}=await yt(e,t,vt(t)),u=a.width+8*o,h=a.height+2*o,d=5,f=t.look==="neo"?` + M${-u/2} ${h/2-d} + v${-h+2*d} + q0,-${d} ${d},-${d} + h${u-2*d} + q${d},0 ${d},${d} + v${h-d} + H${-u/2} + Z + `:` + M${-u/2} ${h/2-d} + v${-h+2*d} + q0,-${d} ${d},-${d} + h${u-2*d} + q${d},0 ${d},${d} + v${h-2*d} + q0,${d} ${-d},${d} + h${-(u-2*d)} + q${-d},0 ${-d},${-d} + Z + `;if(!t.domId)throw new Error(`defaultMindmapNode: node "${t.id}" is missing a domId \u2014 was render.ts domId prefixing skipped?`);let p=i.append("path").attr("id",t.domId).attr("class","node-bkg node-"+t.type).attr("style",n).attr("d",f);return i.append("line").attr("class","node-line-").attr("x1",-u/2).attr("y1",h/2).attr("x2",u/2).attr("y2",h/2),l.attr("transform",`translate(${-a.width/2}, ${-a.height/2})`),i.append(()=>l.node()),ht(t,p),t.calcIntersect=function(m,g){return it.rect(m,g)},t.intersect=function(m){return it.rect(t,m)},i}var xce=F(()=>{"use strict";Zt();Xt();Ht();s(vce,"defaultMindmapNode")});async function bce(e,t){let r={padding:t.padding??0};return k4(e,t,r)}var Tce=F(()=>{"use strict";$P();s(bce,"mindmapCircle")});function Cce(e){return e in L2}var UKe,YKe,L2,R4=F(()=>{"use strict";Ase();Lse();Ise();Nse();Bse();Fse();zse();Wse();$P();Hse();Yse();Xse();Zse();Jse();toe();noe();aoe();coe();hoe();foe();moe();yoe();Toe();Soe();koe();Aoe();_oe();Doe();Moe();Poe();Boe();Foe();zoe();Woe();Hoe();Yoe();Xoe();Zoe();tle();nle();ale();ole();cle();hle();fle();mle();yle();xle();Tle();Sle();kle();Ale();_le();Dle();Mle();Ple();Ble();Fle();Wle();Hle();Yle();Zle();Jle();tce();nce();ace();cce();hce();fce();mce();yce();xce();Tce();UKe=[{semanticName:"Process",name:"Rectangle",shortName:"rect",description:"Standard process shape",aliases:["proc","process","rectangle"],internalAliases:["squareRect"],handler:Cle},{semanticName:"Event",name:"Rounded Rectangle",shortName:"rounded",description:"Represents an event",aliases:["event"],internalAliases:["roundedRect"],handler:gle},{semanticName:"Terminal Point",name:"Stadium",shortName:"stadium",description:"Terminal point",aliases:["terminal","pill"],handler:wle},{semanticName:"Subprocess",name:"Framed Rectangle",shortName:"fr-rect",description:"Subprocess",aliases:["subprocess","subproc","framed-rectangle","subroutine"],handler:Ile},{semanticName:"Database",name:"Cylinder",shortName:"cyl",description:"Database storage",aliases:["db","database","cylinder"],handler:loe},{semanticName:"Data Store",name:"Data Store",shortName:"datastore",description:"Data flow diagram data store",aliases:["data-store"],handler:uoe},{semanticName:"Folder",name:"Folder",shortName:"folder",description:"Folder or directory",aliases:["directory"],handler:Coe},{semanticName:"Bucket",name:"Bucket",shortName:"bucket",description:"Object storage bucket",handler:Dse},{semanticName:"Console",name:"Console (terminal window)",shortName:"console",description:"Terminal or console window",handler:Use},{semanticName:"Browser",name:"Browser",shortName:"browser",description:"Browser window",handler:$se},{semanticName:"Person",name:"Person",shortName:"person",description:"Person (circular head above a rounded body)",handler:ioe},{semanticName:"Start",name:"Circle",shortName:"circle",description:"Starting point",aliases:["circ"],handler:k4},{semanticName:"Bang",name:"Bang",shortName:"bang",description:"Bang",aliases:["bang"],handler:pce},{semanticName:"Cloud",name:"Cloud",shortName:"cloud",description:"cloud",aliases:["cloud"],handler:gce},{semanticName:"Decision",name:"Diamond",shortName:"diam",description:"Decision-making step",aliases:["decision","diamond","question"],handler:ule},{semanticName:"Prepare Conditional",name:"Hexagon",shortName:"hex",description:"Preparation or condition step",aliases:["hexagon","prepare"],handler:Roe},{semanticName:"Data Input/Output",name:"Lean Right",shortName:"lean-r",description:"Represents input or output",aliases:["lean-right","in-out"],internalAliases:["lean_right"],handler:joe},{semanticName:"Data Input/Output",name:"Lean Left",shortName:"lean-l",description:"Represents output or input",aliases:["lean-left","out-in"],internalAliases:["lean_left"],handler:Uoe},{semanticName:"Priority Action",name:"Trapezoid Base Bottom",shortName:"trap-b",description:"Priority action",aliases:["priority","trapezoid-bottom","trapezoid"],handler:qle},{semanticName:"Manual Operation",name:"Trapezoid Base Top",shortName:"trap-t",description:"Represents a manual task",aliases:["manual","trapezoid-top","inv-trapezoid"],internalAliases:["inv_trapezoid"],handler:Voe},{semanticName:"Stop",name:"Double Circle",shortName:"dbl-circ",description:"Represents a stop point",aliases:["double-circle"],internalAliases:["doublecircle"],handler:poe},{semanticName:"Text Block",name:"Text Block",shortName:"text",description:"Text block",handler:$le},{semanticName:"Card",name:"Notched Rectangle",shortName:"notch-rect",description:"Represents a card",aliases:["card","notched-rectangle"],handler:Gse},{semanticName:"Lined/Shaded Process",name:"Lined Rectangle",shortName:"lin-rect",description:"Lined process shape",aliases:["lined-rectangle","lined-process","lin-proc","shaded-process"],handler:vle},{semanticName:"Start",name:"Small Circle",shortName:"sm-circ",description:"Small starting point",aliases:["start","small-circle"],internalAliases:["stateStart"],handler:Lle},{semanticName:"Stop",name:"Framed Circle",shortName:"fr-circ",description:"Stop point",aliases:["stop","framed-circle"],internalAliases:["stateEnd"],handler:Rle},{semanticName:"Fork/Join",name:"Filled Rectangle",shortName:"fork",description:"Fork or join in process flow",aliases:["join"],internalAliases:["forkJoin"],handler:woe},{semanticName:"Collate",name:"Hourglass",shortName:"hourglass",description:"Represents a collate operation",aliases:["hourglass","collate"],handler:Loe},{semanticName:"Comment",name:"Curly Brace",shortName:"brace",description:"Adds a comment",aliases:["comment","brace-l"],handler:Kse},{semanticName:"Comment Right",name:"Curly Brace",shortName:"brace-r",description:"Adds a comment",handler:Qse},{semanticName:"Comment with braces on both sides",name:"Curly Braces",shortName:"braces",description:"Adds a comment",handler:eoe},{semanticName:"Com Link",name:"Lightning Bolt",shortName:"bolt",description:"Communication link",aliases:["com-link","lightning-bolt"],handler:Koe},{semanticName:"Document",name:"Document",shortName:"doc",description:"Represents a document",aliases:["doc","document"],handler:Qle},{semanticName:"Delay",name:"Half-Rounded Rectangle",shortName:"delay",description:"Represents a delay",aliases:["half-rounded-rectangle"],handler:Eoe},{semanticName:"Direct Access Storage",name:"Horizontal Cylinder",shortName:"h-cyl",description:"Direct access storage",aliases:["das","horizontal-cylinder"],handler:Vle},{semanticName:"Disk Storage",name:"Lined Cylinder",shortName:"lin-cyl",description:"Disk storage",aliases:["disk","lined-cylinder"],handler:ele},{semanticName:"Display",name:"Curved Trapezoid",shortName:"curv-trap",description:"Represents a display",aliases:["curved-trapezoid","display"],handler:roe},{semanticName:"Divided Process",name:"Divided Rectangle",shortName:"div-rect",description:"Divided process shape",aliases:["div-proc","divided-rectangle","divided-process"],handler:doe},{semanticName:"Extract",name:"Triangle",shortName:"tri",description:"Extraction process",aliases:["extract","triangle"],handler:Kle},{semanticName:"Internal Storage",name:"Window Pane",shortName:"win-pane",description:"Internal storage",aliases:["internal-storage","window-pane"],handler:rce},{semanticName:"Junction",name:"Filled Circle",shortName:"f-circ",description:"Junction point",aliases:["junction","filled-circle"],handler:goe},{semanticName:"Loop Limit",name:"Trapezoidal Pentagon",shortName:"notch-pent",description:"Loop limit step",aliases:["loop-limit","notched-pentagon"],handler:Ule},{semanticName:"Manual File",name:"Flipped Triangle",shortName:"flip-tri",description:"Manual file operation",aliases:["manual-file","flipped-triangle"],handler:boe},{semanticName:"Manual Input",name:"Sloped Rectangle",shortName:"sl-rect",description:"Manual input step",aliases:["manual-input","sloped-rectangle"],handler:ble},{semanticName:"Multi-Document",name:"Stacked Document",shortName:"docs",description:"Multiple documents",aliases:["documents","st-doc","stacked-document"],handler:sle},{semanticName:"Multi-Process",name:"Stacked Rectangle",shortName:"st-rect",description:"Multiple processes",aliases:["procs","processes","stacked-rectangle"],handler:ile},{semanticName:"Stored Data",name:"Bow Tie Rectangle",shortName:"bow-rect",description:"Stored data",aliases:["stored-data","bow-tie-rectangle"],handler:_se},{semanticName:"Summary",name:"Crossed Circle",shortName:"cross-circ",description:"Summary",aliases:["summary","crossed-circle"],handler:jse},{semanticName:"Tagged Document",name:"Tagged Document",shortName:"tag-doc",description:"Tagged document",aliases:["tag-doc","tagged-document"],handler:Ole},{semanticName:"Tagged Process",name:"Tagged Rectangle",shortName:"tag-rect",description:"Tagged process",aliases:["tagged-rectangle","tag-proc","tagged-process"],handler:Nle},{semanticName:"Paper Tape",name:"Flag",shortName:"flag",description:"Paper tape",aliases:["paper-tape"],handler:ece},{semanticName:"Odd",name:"Odd",shortName:"odd",description:"Odd shape",internalAliases:["rect_left_inv_arrow"],handler:dle},{semanticName:"Lined Document",name:"Lined Document",shortName:"lin-doc",description:"Lined document",aliases:["lined-document"],handler:rle}],YKe=s(()=>{let t=[...Object.entries({state:Ele,choice:Vse,note:lle,composite:qse,rectWithTitle:ple,labelRect:qoe,block_arrow:Ose,collapsedGroup:Mse,iconSquare:$oe,iconCircle:Noe,icon:Ioe,iconRounded:Ooe,imageSquare:Goe,anchor:Ese,kanbanItem:dce,mindmapCircle:bce,defaultMindmapNode:vce,classBox:lce,erBox:GP,requirementBox:uce}),...UKe.flatMap(r=>[r.shortName,..."aliases"in r?r.aliases:[],..."internalAliases"in r?r.internalAliases:[]].map(i=>[i,r.handler]))];return Object.fromEntries(t)},"generateShapeMap"),L2=YKe();s(Cce,"isValidShape")});var jKe,XKe,KKe,zP,ZKe,QKe,JKe,eZe,tZe,VP,rZe,Sce,nZe,WP,qP=F(()=>{"use strict";jKe=new Set(["system_queue","external_system_queue","container_queue","external_container_queue","component_queue","external_component_queue"]),XKe=new Set(["system_db","external_system_db","container_db","external_container_db","component_db","external_component_db"]),KKe={person:"person",box:"rounded",rounded:"rounded",cylinder:"cylinder",database:"cylinder",db:"cylinder",queue:"h-cyl",pipe:"h-cyl",component:"fr-rect"},zP=s(e=>e?KKe[e.toLowerCase()]:void 0,"keywordShape"),ZKe=s(e=>{let t=zP(e.shape)??zP(e.sprite);if(t)return t;if(e.tags)for(let n of e.tags.split(",")){let i=zP(n.trim());if(i)return i}let r=e.typeC4Shape.text;return r==="person"||r==="external_person"?"person":XKe.has(r)?"cylinder":jKe.has(r)?"h-cyl":"rounded"},"resolveNodeShape"),QKe={person:"Person",system:"Software System",container:"Container",component:"Component"},JKe=s(e=>{let t=e.replace(/^external_/,"").replace(/_(db|queue)$/,"");return QKe[t]??t.replace(/_/g," ")},"stereotypeLabel"),eZe=s(e=>e.startsWith("external_"),"isExternal"),tZe=s(e=>{let t=JKe(e.typeC4Shape.text);return e.techn?.text?`[${t}: ${e.techn.text}]`:`[${t}]`},"stereotypeText"),VP=["person","system","system_db","system_queue","container","container_db","container_queue","component","component_db","component_queue"].flatMap(e=>[e,`external_${e}`]),rZe=new Set(VP),Sce=s(e=>rZe.has(e),"isC4ElementType"),nZe=s((e,t)=>{let r=e.typeC4Shape.text,n=e.bgColor??(Sce(r)&&t[`${r}_bg_color`]),i=e.borderColor??(Sce(r)&&t[`${r}_border_color`]),a=[];return n&&a.push(`fill:${n}`),i&&a.push(`stroke:${i}`),a.push(`color:${e.fontColor??"#FFFFFF"}`),a},"elementCssStyles"),WP=s((e,t,r,n,i)=>{let a=e.typeC4Shape.text,o=["c4-shape",`c4-${a}`];eZe(a)&&o.push("c4-external");let l=ZKe(e),u=nZe(e,t);return(l==="rounded"||l==="fr-rect")&&u.push("rx:12px","ry:12px"),{id:e.alias,label:e.label.text,stereotype:tZe(e),description:e.descr?.text?[e.descr.text]:void 0,labelType:"string",isGroup:!1,shape:l,cssClasses:o.join(" "),cssStyles:u,padding:r,look:n,useHtmlLabels:!1,width:i}},"buildC4Node")});function Am(e,t,r,n,i){let a=t[e];if(!a.width)if(r)a.text=Tm(a.text,i,n),a.textLines=a.text.split(Tt.lineBreakRegex).length,a.width=i,a.height=n4(a.text,n);else{let o=a.text.split(Tt.lineBreakRegex);a.textLines=o.length;let l=0;a.height=0,a.width=0;for(let u of o)a.width=Math.max(Qa(u,n),a.width),l=n4(u,n),a.height=a.height+l}return a}async function Rce(e,t,r,n,i){let a=i.db,o=new L4(i);o.data.widthLimit=r.data.widthLimit/Math.min(HP,n.length);for(let[l,u]of n.entries()){let h=0;u.image={width:0,height:0,Y:0},u.sprite&&(u.image.width=48,u.image.height=48,u.image.Y=h,h=u.image.Y+u.image.height);let d=u.wrap&&Cr.wrap,f=_4(Cr);f.fontSize=f.fontSize+2,f.fontWeight="bold";let p=Am("label",u,d,f,o.data.widthLimit);if(p.Y=h+8,h=p.Y+p.height,u.type&&u.type.text!==""){u.type.text="["+u.type.text+"]";let v=_4(Cr),x=Am("type",u,d,v,o.data.widthLimit);x.Y=h+5,h=x.Y+x.height}if(u.descr&&u.descr.text!==""){let v=_4(Cr);v.fontSize=v.fontSize-2;let x=Am("descr",u,d,v,o.data.widthLimit);x.Y=h+20,h=x.Y+x.height}if(l==0||l%HP===0){let v=r.data.startx+Cr.diagramMarginX,x=r.data.stopy+Cr.diagramMarginY+h;o.setData(v,v,x,x)}else{let v=o.data.stopx!==o.data.startx?o.data.stopx+Cr.diagramMarginX:o.data.startx,x=o.data.starty;o.setData(v,v,x,x)}o.name=u.alias;let m=a.getC4ShapeArray(u.alias),g=a.getC4ShapeKeys(u.alias);g.length>0&&await Ace(o,e,m,g),t=u.alias;let y=a.getBoundaries(t);y.length>0&&await Rce(e,t,o,y,i),u.alias!=="global"&&Ece(e,u,o),r.data.stopy=Math.max(o.data.stopy+Cr.c4ShapeMargin,r.data.stopy),r.data.stopx=Math.max(o.data.stopx+Cr.c4ShapeMargin,r.data.stopx),D2=Math.max(D2,r.data.stopx),I2=Math.max(I2,r.data.stopy)}}var D2,I2,kce,HP,Cr,L4,UP,_4,iZe,Ece,Ace,YP,wce,aZe,sZe,oZe,jP,_ce=F(()=>{"use strict";Bie();St();K7();Yr();oI();Jt();iI();$0();er();vae();Vn();R4();qP();D2=0,I2=0,kce=4,HP=2;rb.yy=cb;Cr={},L4=class{static{s(this,"Bounds")}constructor(t){this.name="",this.data={},this.data.startx=void 0,this.data.stopx=void 0,this.data.starty=void 0,this.data.stopy=void 0,this.data.widthLimit=void 0,this.nextData={},this.nextData.startx=void 0,this.nextData.stopx=void 0,this.nextData.starty=void 0,this.nextData.stopy=void 0,this.nextData.cnt=0,UP(t.db.getConfig())}setData(t,r,n,i){this.nextData.startx=this.data.startx=t,this.nextData.stopx=this.data.stopx=r,this.nextData.starty=this.data.starty=n,this.nextData.stopy=this.data.stopy=i}updateVal(t,r,n,i){t[r]===void 0?t[r]=n:t[r]=i(n,t[r])}insert(t){this.nextData.cnt=this.nextData.cnt+1;let r=this.nextData.stopx,n=this.data.widthLimit,i=this.nextData.startx===this.nextData.stopx?r+t.margin:r+t.margin*2,a=i+t.width,o=this.nextData.starty+t.margin*2,l=o+t.height;(i>=n||a>=n||this.nextData.cnt>kce)&&(i=this.nextData.startx+t.margin+Cr.nextLinePaddingX,o=this.nextData.stopy+t.margin*2,this.nextData.stopx=a=i+t.width,this.nextData.starty=this.nextData.stopy,this.nextData.stopy=l=o+t.height,this.nextData.cnt=1),t.x=i,t.y=o,this.updateVal(this.data,"startx",i,Math.min),this.updateVal(this.data,"starty",o,Math.min),this.updateVal(this.data,"stopx",a,Math.max),this.updateVal(this.data,"stopy",l,Math.max),this.updateVal(this.nextData,"startx",i,Math.min),this.updateVal(this.nextData,"starty",o,Math.min),this.updateVal(this.nextData,"stopx",a,Math.max),this.updateVal(this.nextData,"stopy",l,Math.max)}init(t){this.name="",this.data={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0,widthLimit:void 0},this.nextData={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0,cnt:0},UP(t.db.getConfig())}bumpLastMargin(t){this.data.stopx+=t,this.data.stopy+=t}},UP=s(function(e){ii(Cr,e),e?.fontFamily&&(Cr.personFontFamily=Cr.systemFontFamily=Cr.messageFontFamily=e.fontFamily),e?.fontSize&&(Cr.personFontSize=Cr.systemFontSize=Cr.messageFontSize=e.fontSize),e?.fontWeight&&(Cr.personFontWeight=Cr.systemFontWeight=Cr.messageFontWeight=e.fontWeight)},"setConf"),_4=s(e=>({fontFamily:e.boundaryFontFamily,fontSize:e.boundaryFontSize,fontWeight:e.boundaryFontWeight}),"boundaryFont"),iZe=s(e=>({fontFamily:e.messageFontFamily,fontSize:e.messageFontSize,fontWeight:e.messageFontWeight}),"messageFont");s(Am,"calcC4ShapeTextWH");Ece=s(function(e,t,r){let n=r.data.startx,i=r.data.starty;t.x=n,t.y=i,t.width=r.data.stopx-n,t.height=r.data.stopy-i,t.label.y=Cr.c4ShapeMargin-35;let a=t.wrap&&Cr.wrap,o=_4(Cr);o.fontSize=o.fontSize+2,o.fontWeight="bold";let l=Qa(t.label.text,o);Am("label",t,a,o,l),Qc.drawBoundary(e,t,Cr)},"drawBoundary"),Ace=s(async function(e,t,r,n){let i=Pe(),a=i.look??"classic",o={config:i},l=t.attr("id")??"",u=n.map(d=>r[Number(d)]),h=s(d=>{let f=d.shape?L2[d.shape]:void 0;if(!f)throw new Error(`C4: no shape handler for "${d.shape}"`);return f},"shapeHandlerFor");await Promise.all(u.map(async d=>{let f=WP(d,Cr,Cr.c4ShapePadding,a,Cr.width);f.domId=`${l}-${f.id}`;let p=await h(f)(t,f,o);d.width=f.width??Cr.width,d.height=f.height??Cr.height,d.margin=Cr.c4ShapeMargin,p.remove()}));for(let d of u)e.insert(d);await Promise.all(u.map(async d=>{let f=WP(d,Cr,Cr.c4ShapePadding,a,Cr.width);f.domId=`${l}-${f.id}`,f.x=d.x+d.width/2,f.y=d.y+d.height/2;let p=t.append("g").attr("transform",`translate(${d.x+d.width/2}, ${d.y+d.height/2})`);await h(f)(p,f,o),d.intersect=f.intersect})),e.bumpLastMargin(Cr.c4ShapeMargin)},"drawC4ShapeArray"),YP=class{static{s(this,"Point")}constructor(t,r){this.x=t,this.y=r}},wce=s(function(e,t){if(!e.intersect)throw new Error(`C4 shape "${e.alias}" has no intersect function. Please report this to https://github.com/mermaid-js/mermaid/issues`);let{x:r,y:n}=e.intersect(t);return new YP(r,n)},"getIntersectPoint"),aZe=s(function(e,t){let r={x:0,y:0};r.x=t.x+t.width/2,r.y=t.y+t.height/2;let n=wce(e,r);r.x=e.x+e.width/2,r.y=e.y+e.height/2;let i=wce(t,r);return{startPoint:n,endPoint:i}},"getIntersectPoints"),sZe=s(function(e,t,r,n,i){let a=n.db.getC4Type(),o=0;for(let l of t){o=o+1;let u=l.wrap&&Cr.wrap,h=iZe(Cr);a==="C4Dynamic"&&(l.label.text=o+": "+l.label.text);let d=Qa(l.label.text,h);Am("label",l,u,h,d),l.techn&&l.techn.text!==""&&(d=Qa(l.techn.text,h),Am("techn",l,u,h,d)),l.descr&&l.descr.text!==""&&(d=Qa(l.descr.text,h),Am("descr",l,u,h,d));let f=r(l.from),p=r(l.to);if(!f||!p)throw new Error(`C4 rel "${l.from}" -> "${l.to}" references an unknown shape`);let m=aZe(f,p);if(!m.startPoint||!m.endPoint)throw new Error(`Could not calculate intersection points for rel "${l.from}" -> "${l.to}"`);l.startPoint=m.startPoint,l.endPoint=m.endPoint}Qc.drawRels(e,t,Cr,i)},"drawRels");s(Rce,"drawInsideBoundary");oZe=s(async function(e,t,r,n){Cr=mk("c4");let i=Pe().securityLevel,{root:a}=yae(t,i),o=n.db;o.setWrap(Cr.wrap),kce=o.getC4ShapeInRow(),HP=o.getC4BoundaryInRow(),se.debug(`C:${JSON.stringify(Cr,null,2)}`);let l=a.select(`[id="${t}"]`);Qc.insertComputerIcon(l,t),Qc.insertDatabaseIcon(l,t),Qc.insertClockIcon(l,t);let u=new L4(n);u.setData(Cr.diagramMarginX,Cr.diagramMarginX,Cr.diagramMarginY,Cr.diagramMarginY),u.data.widthLimit=screen.availWidth,D2=Cr.diagramMarginX,I2=Cr.diagramMarginY;let h=o.getTitle(),d=o.getBoundaries("");await Rce(l,"",u,d,n),Qc.insertArrowHead(l,t),Qc.insertArrowEnd(l,t),Qc.insertArrowCrossHead(l,t),Qc.insertArrowFilledHead(l,t),sZe(l,o.getRels(),o.getC4Shape,n,t),u.data.stopx=D2,u.data.stopy=I2;let f=u.data,p=f.startx,m=f.starty,y=I2-m+2*Cr.diagramMarginY,v=D2-p,x=v+2*Cr.diagramMarginX;h&&l.append("text").text(h).attr("x",v/2-4*Cr.diagramMarginX).attr("y",m+Cr.diagramMarginY),jr(l,y,x,Cr.useMaxWidth);let b=h?60:0;l.attr("viewBox",p-Cr.diagramMarginX+" -"+(Cr.diagramMarginY+b)+" "+x+" "+(y+b)),se.debug("models:",f)},"draw"),jP={drawPersonOrSystemArray:Ace,drawBoundary:Ece,setConf:UP,draw:oZe}});var lZe,cZe,Lce,Dce=F(()=>{"use strict";Jt();qP();lZe=s(()=>{let e=Pe().c4??{},t=new CSSStyleSheet;for(let r of VP){let n=t.cssRules[t.insertRule(`.c4-shape.c4-${r} .label {}`,t.cssRules.length)],i=e[`${r}FontFamily`],a=e[`${r}FontSize`],o=e[`${r}FontWeight`];i&&n.style.setProperty("font-family",i),a&&n.style.setProperty("font-size",typeof a=="number"?`${a}px`:a),o&&n.style.setProperty("font-weight",String(o))}return[...t.cssRules].filter(r=>r.style.length>0).map(r=>` ${r.cssText}`).join(` +`)},"elementFontStyles"),cZe=s(e=>`.person { + stroke: ${e.personBorder}; + fill: ${e.personBkg}; + } +${lZe()} + + /* The element font colour is set inline per element (default white); the + label text takes it via currentColor. */ + .c4-shape .label, + .c4-shape .label text { + color: inherit; + fill: currentColor; + } + /* Structurizr typography: bold name, smaller stereotype/type and description lines. */ + .c4-shape .label .c4-name { + font-weight: bold; + } + .c4-shape .label .c4-type { + font-size: 0.75em; + } + .c4-shape .label .c4-descr { + font-size: 0.82em; + } + .c4-shape .basic, + .c4-shape rect, + .c4-shape path, + .c4-shape circle, + .c4-shape ellipse, + .c4-shape line { + stroke-width: 2px; + } +`,"getStyles"),Lce=cZe});var Ice={};sr(Ice,{diagram:()=>uZe});var uZe,Mce=F(()=>{"use strict";K7();oI();_ce();Dce();uZe={parser:yJ,db:cb,renderer:jP,styles:Lce,init:s(({c4:e,wrap:t})=>{jP.setConf(e),cb.setWrap(t)},"init")}});function pZe(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function M2(){if(Pce)return rf;Pce=1;function e(o){return typeof o>"u"||o===null}s(e,"isNothing");function t(o){return typeof o=="object"&&o!==null}s(t,"isObject");function r(o){return Array.isArray(o)?o:e(o)?[]:[o]}s(r,"toArray");function n(o,l){if(l){let u=Object.keys(l);for(let h=0,d=u.length;hf&&(h=" ... ",a=l-f+h.length),o-l>f&&(d=" ...",o=l+f-d.length),{str:h+i.slice(a,o).replace(/\t/g,"\u2192")+d,pos:l-a+h.length}}s(t,"getLine");function r(i,a){return e.repeat(" ",a-i.length)+i}s(r,"padStart");function n(i,a){if(a=Object.create(a||null),!i.buffer)return null;a.maxLength||(a.maxLength=79),typeof a.indent!="number"&&(a.indent=1),typeof a.linesBefore!="number"&&(a.linesBefore=3),typeof a.linesAfter!="number"&&(a.linesAfter=2);let o=/\r?\n|\r|\0/g,l=[0],u=[],h,d=-1;for(;h=o.exec(i.buffer);)u.push(h.index),l.push(h.index+h[0].length),i.position<=h.index&&d<0&&(d=l.length-2);d<0&&(d=l.length-1);let f="",p=Math.min(i.line+a.linesAfter,u.length).toString().length,m=a.maxLength-(a.indent+p+3);for(let y=1;y<=a.linesBefore&&!(d-y<0);y++){let v=t(i.buffer,l[d-y],u[d-y],i.position-(l[d]-l[d-y]),m);f=e.repeat(" ",a.indent)+r((i.line-y+1).toString(),p)+" | "+v.str+` +`+f}let g=t(i.buffer,l[d],u[d],i.position,m);f+=e.repeat(" ",a.indent)+r((i.line+1).toString(),p)+" | "+g.str+` +`,f+=e.repeat("-",a.indent+p+3+g.pos)+`^ +`;for(let y=1;y<=a.linesAfter&&!(d+y>=u.length);y++){let v=t(i.buffer,l[d+y],u[d+y],i.position-(l[d]-l[d+y]),m);f+=e.repeat(" ",a.indent)+r((i.line+y+1).toString(),p)+" | "+v.str+` +`}return f.replace(/\n$/,"")}return s(n,"makeSnippet"),KP=n,KP}function Fs(){if($ce)return ZP;$ce=1;let e=N2(),t=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],r=["scalar","sequence","mapping"];function n(a){let o={};return a!==null&&Object.keys(a).forEach(function(l){a[l].forEach(function(u){o[String(u)]=l})}),o}s(n,"compileStyleAliases");function i(a,o){if(o=o||{},Object.keys(o).forEach(function(l){if(t.indexOf(l)===-1)throw new e('Unknown option "'+l+'" is met in definition of "'+a+'" YAML type.')}),this.options=o,this.tag=a,this.kind=o.kind||null,this.resolve=o.resolve||function(){return!0},this.construct=o.construct||function(l){return l},this.instanceOf=o.instanceOf||null,this.predicate=o.predicate||null,this.represent=o.represent||null,this.representName=o.representName||null,this.defaultStyle=o.defaultStyle||null,this.multi=o.multi||!1,this.styleAliases=n(o.styleAliases||null),r.indexOf(this.kind)===-1)throw new e('Unknown kind "'+this.kind+'" is specified for "'+a+'" YAML type.')}return s(i,"Type2"),ZP=i,ZP}function sue(){if(Fce)return QP;Fce=1;let e=N2(),t=Fs();function r(a,o){let l=[];return a[o].forEach(function(u){let h=l.length;l.forEach(function(d,f){d.tag===u.tag&&d.kind===u.kind&&d.multi===u.multi&&(h=f)}),l[h]=u}),l}s(r,"compileList");function n(){let a={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function o(l){l.multi?(a.multi[l.kind].push(l),a.multi.fallback.push(l)):a[l.kind][l.tag]=a.fallback[l.tag]=l}s(o,"collectType");for(let l=0,u=arguments.length;l=48&&h<=57||h>=65&&h<=70||h>=97&&h<=102}s(r,"isHexCode");function n(h){return h>=48&&h<=55}s(n,"isOctCode");function i(h){return h>=48&&h<=57}s(i,"isDecCode");function a(h){if(h===null)return!1;let d=h.length,f=0,p=!1;if(!d)return!1;let m=h[f];if((m==="-"||m==="+")&&(m=h[++f]),m==="0"){if(f+1===d)return!0;if(m=h[++f],m==="b"){for(f++;f=0?"0b"+h.toString(2):"-0b"+h.toString(2).slice(1)},"binary"),octal:s(function(h){return h>=0?"0o"+h.toString(8):"-0o"+h.toString(8).slice(1)},"octal"),decimal:s(function(h){return h.toString(10)},"decimal"),hexadecimal:s(function(h){return h>=0?"0x"+h.toString(16).toUpperCase():"-0x"+h.toString(16).toUpperCase().slice(1)},"hexadecimal")},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),aO}function pue(){if(Yce)return sO;Yce=1;let e=M2(),t=Fs(),r=new RegExp("^(?:[-+]?(?:[0-9]+)(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),n=new RegExp("^(?:[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");function i(h){return h===null||!r.test(h)?!1:isFinite(parseFloat(h,10))?!0:n.test(h)}s(i,"resolveYamlFloat");function a(h){let d=h.toLowerCase(),f=d[0]==="-"?-1:1;return"+-".indexOf(d[0])>=0&&(d=d.slice(1)),d===".inf"?f===1?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:d===".nan"?NaN:f*parseFloat(d,10)}s(a,"constructYamlFloat");let o=/^[-+]?[0-9]+e/;function l(h,d){if(isNaN(h))switch(d){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===h)switch(d){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===h)switch(d){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(e.isNegativeZero(h))return"-0.0";let f=h.toString(10);return o.test(f)?f.replace("e",".e"):f}s(l,"representYamlFloat");function u(h){return Object.prototype.toString.call(h)==="[object Number]"&&(h%1!==0||e.isNegativeZero(h))}return s(u,"isFloat"),sO=new t("tag:yaml.org,2002:float",{kind:"scalar",resolve:i,construct:a,predicate:u,represent:l,defaultStyle:"lowercase"}),sO}function mue(){return jce||(jce=1,oO=uue().extend({implicit:[hue(),due(),fue(),pue()]})),oO}function gue(){return Xce||(Xce=1,lO=mue()),lO}function yue(){if(Kce)return cO;Kce=1;let e=Fs(),t=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),r=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");function n(o){return o===null?!1:t.exec(o)!==null||r.exec(o)!==null}s(n,"resolveYamlTimestamp");function i(o){let l=0,u=null,h=t.exec(o);if(h===null&&(h=r.exec(o)),h===null)throw new Error("Date resolve error");let d=+h[1],f=+h[2]-1,p=+h[3];if(!h[4])return new Date(Date.UTC(d,f,p));let m=+h[4],g=+h[5],y=+h[6];if(h[7]){for(l=h[7].slice(0,3);l.length<3;)l+="0";l=+l}if(h[9]){let x=+h[10],b=+(h[11]||0);u=(x*60+b)*6e4,h[9]==="-"&&(u=-u)}let v=new Date(Date.UTC(d,f,p,m,g,y,l));return u&&v.setTime(v.getTime()-u),v}s(i,"constructYamlTimestamp");function a(o){return o.toISOString()}return s(a,"representYamlTimestamp"),cO=new e("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:n,construct:i,instanceOf:Date,represent:a}),cO}function vue(){if(Zce)return uO;Zce=1;let e=Fs();function t(r){return r==="<<"||r===null}return s(t,"resolveYamlMerge"),uO=new e("tag:yaml.org,2002:merge",{kind:"scalar",resolve:t}),uO}function xue(){if(Qce)return hO;Qce=1;let e=Fs(),t=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= +\r`;function r(o){if(o===null)return!1;let l=0,u=o.length,h=t;for(let d=0;d64)){if(f<0)return!1;l+=6}}return l%8===0}s(r,"resolveYamlBinary");function n(o){let l=o.replace(/[\r\n=]/g,""),u=l.length,h=t,d=0,f=[];for(let m=0;m>16&255),f.push(d>>8&255),f.push(d&255)),d=d<<6|h.indexOf(l.charAt(m));let p=u%4*6;return p===0?(f.push(d>>16&255),f.push(d>>8&255),f.push(d&255)):p===18?(f.push(d>>10&255),f.push(d>>2&255)):p===12&&f.push(d>>4&255),new Uint8Array(f)}s(n,"constructYamlBinary");function i(o){let l="",u=0,h=o.length,d=t;for(let p=0;p>18&63],l+=d[u>>12&63],l+=d[u>>6&63],l+=d[u&63]),u=(u<<8)+o[p];let f=h%3;return f===0?(l+=d[u>>18&63],l+=d[u>>12&63],l+=d[u>>6&63],l+=d[u&63]):f===2?(l+=d[u>>10&63],l+=d[u>>4&63],l+=d[u<<2&63],l+=d[64]):f===1&&(l+=d[u>>2&63],l+=d[u<<4&63],l+=d[64],l+=d[64]),l}s(i,"representYamlBinary");function a(o){return Object.prototype.toString.call(o)==="[object Uint8Array]"}return s(a,"isBinary"),hO=new e("tag:yaml.org,2002:binary",{kind:"scalar",resolve:r,construct:n,predicate:a,represent:i}),hO}function bue(){if(Jce)return dO;Jce=1;let e=Fs(),t=Object.prototype.hasOwnProperty,r=Object.prototype.toString;function n(a){if(a===null)return!0;let o=[],l=a;for(let u=0,h=l.length;u=48&&G<=57)return G-48;let X=G|32;return X>=97&&X<=102?X-97+10:-1}s(S,"fromHexCode");function k(G){return G===120?2:G===117?4:G===85?8:0}s(k,"escapedHexLen");function R(G){return G>=48&&G<=57?G-48:-1}s(R,"fromDecimalCode");function M(G){switch(G){case 48:return"\0";case 97:return"\x07";case 98:return"\b";case 116:return" ";case 9:return" ";case 110:return` +`;case 118:return"\v";case 102:return"\f";case 114:return"\r";case 101:return"\x1B";case 32:return" ";case 34:return'"';case 47:return"/";case 92:return"\\";case 78:return"\x85";case 95:return"\xA0";case 76:return"\u2028";case 80:return"\u2029";default:return""}}s(M,"simpleEscapeSequence");function P(G){return G<=65535?String.fromCharCode(G):String.fromCharCode((G-65536>>10)+55296,(G-65536&1023)+56320)}s(P,"charFromCodepoint");function N(G,X,ee){X==="__proto__"?Object.defineProperty(G,X,{configurable:!0,enumerable:!0,writable:!0,value:ee}):G[X]=ee}s(N,"setProperty");let _=new Array(256),A=new Array(256);for(let G=0;G<256;G++)_[G]=M(G)?1:0,A[G]=M(G);function I(G,X){this.input=G,this.filename=X.filename||null,this.schema=X.schema||n,this.onWarning=X.onWarning||null,this.legacy=X.legacy||!1,this.json=X.json||!1,this.listener=X.listener||null,this.maxDepth=typeof X.maxDepth=="number"?X.maxDepth:100,this.maxTotalMergeKeys=typeof X.maxTotalMergeKeys=="number"?X.maxTotalMergeKeys:1e4,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=G.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.depth=0,this.totalMergeKeys=0,this.firstTabInLine=-1,this.documents=[],this.anchorMapTransactions=[]}s(I,"State");function D(G,X){let ee={name:G.filename,buffer:G.input.slice(0,-1),position:G.position,line:G.line,column:G.position-G.lineStart};return ee.snippet=r(ee),new t(X,ee)}s(D,"generateError");function L(G,X){throw D(G,X)}s(L,"throwError");function B(G,X){G.onWarning&&G.onWarning.call(null,D(G,X))}s(B,"throwWarning");function O(G,X,ee){let q=G.anchorMapTransactions;if(q.length!==0){let ie=q[q.length-1];i.call(ie,X)||(ie[X]={existed:i.call(G.anchorMap,X),value:G.anchorMap[X]})}G.anchorMap[X]=ee}s(O,"storeAnchor");function $(G){G.anchorMapTransactions.push(Object.create(null))}s($,"beginAnchorTransaction");function W(G){let X=G.anchorMapTransactions.pop(),ee=G.anchorMapTransactions;if(ee.length===0)return;let q=ee[ee.length-1],ie=Object.keys(X);for(let j=0,U=ie.length;j=0;q-=1){let ie=X[ee[q]];ie.existed?G.anchorMap[ee[q]]=ie.value:delete G.anchorMap[ee[q]]}}s(V,"rollbackAnchorTransaction");function z(G){return{position:G.position,line:G.line,lineStart:G.lineStart,lineIndent:G.lineIndent,firstTabInLine:G.firstTabInLine,tag:G.tag,anchor:G.anchor,kind:G.kind,result:G.result}}s(z,"snapshotState");function H(G,X){G.position=X.position,G.line=X.line,G.lineStart=X.lineStart,G.lineIndent=X.lineIndent,G.firstTabInLine=X.firstTabInLine,G.tag=X.tag,G.anchor=X.anchor,G.kind=X.kind,G.result=X.result}s(H,"restoreState");let K={YAML:s(function(X,ee,q){X.version!==null&&L(X,"duplication of %YAML directive"),q.length!==1&&L(X,"YAML directive accepts exactly one argument");let ie=/^([0-9]+)\.([0-9]+)$/.exec(q[0]);ie===null&&L(X,"ill-formed argument of the YAML directive");let j=parseInt(ie[1],10),U=parseInt(ie[2],10);j!==1&&L(X,"unacceptable YAML version of the document"),X.version=q[0],X.checkLineBreaks=U<2,U!==1&&U!==2&&B(X,"unsupported YAML version of the document")},"handleYamlDirective"),TAG:s(function(X,ee,q){let ie;q.length!==2&&L(X,"TAG directive accepts exactly two arguments");let j=q[0];ie=q[1],y.test(j)||L(X,"ill-formed tag handle (first argument) of the TAG directive"),i.call(X.tagMap,j)&&L(X,'there is a previously declared suffix for "'+j+'" tag handle'),v.test(ie)||L(X,"ill-formed tag prefix (second argument) of the TAG directive");try{ie=decodeURIComponent(ie)}catch{L(X,"tag prefix is malformed: "+ie)}X.tagMap[j]=ie},"handleTagDirective")};function J(G,X,ee,q){if(X=32&&Y<=1114111||L(G,"expected valid JSON character")}else p.test(ie)&&L(G,"the stream contains non-printable characters");G.result+=ie}}s(J,"captureSegment");function ne(G,X,ee,q){e.isObject(ee)||L(G,"cannot merge mappings; the provided source object is unacceptable");let ie=Object.keys(ee);for(let j=0,U=ie.length;jG.maxTotalMergeKeys&&L(G,"merge keys exceeded maxTotalMergeKeys ("+G.maxTotalMergeKeys+")"),i.call(X,Y)||(N(X,Y,ee[Y]),q[Y]=!0)}}s(ne,"mergeMappings");function Z(G,X,ee,q,ie,j,U,Y,Ae){if(Array.isArray(ie)){ie=Array.prototype.slice.call(ie);for(let me=0,Se=ie.length;me1&&(G.result+=e.repeat(` +`,X-1))}s(ce,"writeFoldedLines");function de(G,X,ee){let q,ie,j,U,Y,Ae,me=G.kind,Se=G.result,Ce=G.input.charCodeAt(G.position);if(w(Ce)||C(Ce)||Ce===35||Ce===38||Ce===42||Ce===33||Ce===124||Ce===62||Ce===39||Ce===34||Ce===37||Ce===64||Ce===96)return!1;if(Ce===63||Ce===45){let Me=G.input.charCodeAt(G.position+1);if(w(Me)||ee&&C(Me))return!1}for(G.kind="scalar",G.result="",q=ie=G.position,j=!1;Ce!==0;){if(Ce===58){let Me=G.input.charCodeAt(G.position+1);if(w(Me)||ee&&C(Me))break}else if(Ce===35){let Me=G.input.charCodeAt(G.position-1);if(w(Me))break}else{if(G.position===G.lineStart&&fe(G)||ee&&C(Ce))break;if(b(Ce))if(U=G.line,Y=G.lineStart,Ae=G.lineIndent,te(G,!1,-1),G.lineIndent>=X){j=!0,Ce=G.input.charCodeAt(G.position);continue}else{G.position=ie,G.line=U,G.lineStart=Y,G.lineIndent=Ae;break}}j&&(J(G,q,ie,!1),ce(G,G.line-U),q=ie=G.position,j=!1),T(Ce)||(ie=G.position+1),Ce=G.input.charCodeAt(++G.position)}return J(G,q,ie,!1),G.result?!0:(G.kind=me,G.result=Se,!1)}s(de,"readPlainScalar");function Ee(G,X){let ee,q,ie=G.input.charCodeAt(G.position);if(ie!==39)return!1;for(G.kind="scalar",G.result="",G.position++,ee=q=G.position;(ie=G.input.charCodeAt(G.position))!==0;)if(ie===39)if(J(G,ee,G.position,!0),ie=G.input.charCodeAt(++G.position),ie===39)ee=G.position,G.position++,q=G.position;else return!0;else b(ie)?(J(G,ee,q,!0),ce(G,te(G,!1,X)),ee=q=G.position):G.position===G.lineStart&&fe(G)?L(G,"unexpected end of the document within a single quoted scalar"):(G.position++,T(ie)||(q=G.position));L(G,"unexpected end of the stream within a single quoted scalar")}s(Ee,"readSingleQuotedScalar");function we(G,X){let ee,q,ie,j=G.input.charCodeAt(G.position);if(j!==34)return!1;for(G.kind="scalar",G.result="",G.position++,ee=q=G.position;(j=G.input.charCodeAt(G.position))!==0;){if(j===34)return J(G,ee,G.position,!0),G.position++,!0;if(j===92){if(J(G,ee,G.position,!0),j=G.input.charCodeAt(++G.position),b(j))te(G,!1,X);else if(j<256&&_[j])G.result+=A[j],G.position++;else if((ie=k(j))>0){let U=ie,Y=0;for(;U>0;U--)j=G.input.charCodeAt(++G.position),(ie=S(j))>=0?Y=(Y<<4)+ie:L(G,"expected hexadecimal character");G.result+=P(Y),G.position++}else L(G,"unknown escape sequence");ee=q=G.position}else b(j)?(J(G,ee,q,!0),ce(G,te(G,!1,X)),ee=q=G.position):G.position===G.lineStart&&fe(G)?L(G,"unexpected end of the document within a double quoted scalar"):(G.position++,T(j)||(q=G.position))}L(G,"unexpected end of the stream within a double quoted scalar")}s(we,"readDoubleQuotedScalar");function Fe(G,X){let ee=!0,q,ie,j,U=G.tag,Y,Ae=G.anchor,me,Se,Ce,Me,_e=Object.create(null),ze,Le,qe,ve=G.input.charCodeAt(G.position);if(ve===91)me=93,Me=!1,Y=[];else if(ve===123)me=125,Me=!0,Y={};else return!1;for(G.anchor!==null&&O(G,G.anchor,Y),ve=G.input.charCodeAt(++G.position);ve!==0;){if(te(G,!0,X),ve=G.input.charCodeAt(G.position),ve===me)return G.position++,G.tag=U,G.anchor=Ae,G.kind=Me?"mapping":"sequence",G.result=Y,!0;if(ee?ve===44&&L(G,"expected the node content, but found ','"):L(G,"missed comma between flow collection entries"),Le=ze=qe=null,Se=Ce=!1,ve===63){let lt=G.input.charCodeAt(G.position+1);w(lt)&&(Se=Ce=!0,G.position++,te(G,!0,X))}q=G.line,ie=G.lineStart,j=G.position,ue(G,X,a,!1,!0),Le=G.tag,ze=G.result,te(G,!0,X),ve=G.input.charCodeAt(G.position),(Ce||G.line===q)&&ve===58&&(Se=!0,ve=G.input.charCodeAt(++G.position),te(G,!0,X),ue(G,X,a,!1,!0),qe=G.result),Me?Z(G,Y,_e,Le,ze,qe,q,ie,j):Se?Y.push(Z(G,null,_e,Le,ze,qe,q,ie,j)):Y.push(ze),te(G,!0,X),ve=G.input.charCodeAt(G.position),ve===44?(ee=!0,ve=G.input.charCodeAt(++G.position)):ee=!1}L(G,"unexpected end of the stream within a flow collection")}s(Fe,"readFlowCollection");function Ye(G,X){let ee,q=h,ie=!1,j=!1,U=X,Y=0,Ae=!1,me,Se=G.input.charCodeAt(G.position);if(Se===124)ee=!1;else if(Se===62)ee=!0;else return!1;for(G.kind="scalar",G.result="";Se!==0;)if(Se=G.input.charCodeAt(++G.position),Se===43||Se===45)h===q?q=Se===43?f:d:L(G,"repeat of a chomping mode identifier");else if((me=R(Se))>=0)me===0?L(G,"bad explicit indentation width of a block scalar; it cannot be less than one"):j?L(G,"repeat of an indentation width identifier"):(U=X+me-1,j=!0);else break;if(T(Se)){do Se=G.input.charCodeAt(++G.position);while(T(Se));if(Se===35)do Se=G.input.charCodeAt(++G.position);while(!b(Se)&&Se!==0)}for(;Se!==0;){for(pe(G),G.lineIndent=0,Se=G.input.charCodeAt(G.position);(!j||G.lineIndentU&&(U=G.lineIndent),b(Se)){Y++;continue}if(!j&&U===0&&L(G,"missing indentation for block scalar"),G.lineIndentX)&&U!==0)L(G,"bad indentation of a sequence entry");else if(G.lineIndentX)&&(ze&&(ie=G.line,j=G.lineStart,U=G.position),ue(G,X,u,!0,q)&&(ze?Me=G.result:_e=G.result),ze||(Z(G,me,Se,Ce,Me,_e,ie,j,U),Ce=Me=_e=null),te(G,!0,-1),qe=G.input.charCodeAt(G.position)),(G.line===lt||G.lineIndent>X)&&qe!==0)L(G,"bad indentation of a mapping entry");else if(G.lineIndent=G.maxDepth&&L(G,"nesting exceeded maxDepth ("+G.maxDepth+")"),G.depth+=1,G.listener!==null&&G.listener("open",G),G.tag=null,G.anchor=null,G.kind=null,G.result=null;let ze=j=U=u===ee||l===ee;if(q&&te(G,!0,-1)&&(Ae=!0,G.lineIndent>X?Y=1:G.lineIndent===X?Y=0:G.lineIndentX?Y=1:G.lineIndent===X?Y=0:G.lineIndent tag; it should be "scalar", not "'+G.kind+'"');for(let Le=0,qe=G.implicitTypes.length;Le"),G.result!==null&&Ce.kind!==G.kind&&L(G,"unacceptable node kind for !<"+G.tag+'> tag; it should be "'+Ce.kind+'", not "'+G.kind+'"'),Ce.resolve(G.result,G.tag)?(G.result=Ce.construct(G.result,G.tag),G.anchor!==null&&O(G,G.anchor,G.result)):L(G,"cannot resolve a node with !<"+G.tag+"> explicit tag")}return G.listener!==null&&G.listener("close",G),G.depth-=1,G.tag!==null||G.anchor!==null||me}s(ue,"composeNode");function le(G){let X=G.position,ee=!1,q;for(G.version=null,G.checkLineBreaks=G.legacy,G.tagMap=Object.create(null),G.anchorMap=Object.create(null);(q=G.input.charCodeAt(G.position))!==0&&(te(G,!0,-1),q=G.input.charCodeAt(G.position),!(G.lineIndent>0||q!==37));){ee=!0,q=G.input.charCodeAt(++G.position);let ie=G.position;for(;q!==0&&!w(q);)q=G.input.charCodeAt(++G.position);let j=G.input.slice(ie,G.position),U=[];for(j.length<1&&L(G,"directive name must not be less than one character in length");q!==0;){for(;T(q);)q=G.input.charCodeAt(++G.position);if(q===35){do q=G.input.charCodeAt(++G.position);while(q!==0&&!b(q));break}if(b(q))break;for(ie=G.position;q!==0&&!w(q);)q=G.input.charCodeAt(++G.position);U.push(G.input.slice(ie,G.position))}q!==0&&pe(G),i.call(K,j)?K[j](G,j,U):B(G,'unknown document directive "'+j+'"')}if(te(G,!0,-1),G.lineIndent===0&&G.input.charCodeAt(G.position)===45&&G.input.charCodeAt(G.position+1)===45&&G.input.charCodeAt(G.position+2)===45?(G.position+=3,te(G,!0,-1)):ee&&L(G,"directives end mark is expected"),ue(G,G.lineIndent-1,u,!1,!0),te(G,!0,-1),G.checkLineBreaks&&m.test(G.input.slice(X,G.position))&&B(G,"non-ASCII line breaks are interpreted as content"),G.documents.push(G.result),G.position===G.lineStart&&fe(G)){G.input.charCodeAt(G.position)===46&&(G.position+=3,te(G,!0,-1));return}G.position"u"&&(ee=X,X=null);let q=he(G,ee);if(typeof X!="function")return q;for(let ie=0,j=q.length;ie=32&&U<=126||U>=161&&U<=55295&&U!==8232&&U!==8233||U>=57344&&U<=65533&&U!==a||U>=65536&&U<=1114111}s(ne,"isPrintable");function Z(U){return ne(U)&&U!==a&&U!==u&&U!==l}s(Z,"isNsCharOrWhitespace");function pe(U,Y,Ae){let me=Z(U),Se=me&&!J(U);return(Ae?me:me&&U!==x&&U!==R&&U!==M&&U!==N&&U!==A)&&U!==p&&!(Y===T&&!Se)||Z(Y)&&!J(Y)&&U===p||Y===T&&Se}s(pe,"isPlainSafe");function te(U){return ne(U)&&U!==a&&!J(U)&&U!==b&&U!==S&&U!==T&&U!==x&&U!==R&&U!==M&&U!==N&&U!==A&&U!==p&&U!==g&&U!==v&&U!==d&&U!==_&&U!==w&&U!==C&&U!==y&&U!==f&&U!==m&&U!==k&&U!==P}s(te,"isPlainSafeFirst");function fe(U){return!J(U)&&U!==T}s(fe,"isPlainSafeLast");function ce(U,Y){let Ae=U.charCodeAt(Y),me;return Ae>=55296&&Ae<=56319&&Y+1=56320&&me<=57343)?(Ae-55296)*1024+me-56320+65536:Ae}s(ce,"codePointAt");function de(U){return/^\n* /.test(U)}s(de,"needIndentIndicator");let Ee=1,we=2,Fe=3,Ye=4,He=5;function ye(U,Y,Ae,me,Se,Ce,Me,_e){let ze,Le=0,qe=null,ve=!1,lt=!1,Oe=me!==-1,mt=-1,At=te(ce(U,0))&&fe(ce(U,U.length-1));if(Y||Me)for(ze=0;ze=65536?ze+=2:ze++){if(Le=ce(U,ze),!ne(Le))return He;At=At&&pe(Le,qe,_e),qe=Le}else{for(ze=0;ze=65536?ze+=2:ze++){if(Le=ce(U,ze),Le===l)ve=!0,Oe&&(lt=lt||ze-mt-1>me&&U[mt+1]!==" ",mt=ze);else if(!ne(Le))return He;At=At&&pe(Le,qe,_e),qe=Le}lt=lt||Oe&&ze-mt-1>me&&U[mt+1]!==" "}return!ve&&!lt?At&&!Me&&!Se(U)?Ee:Ce===W?He:we:Ae>9&&de(U)?He:Me?Ce===W?He:we:lt?Ye:Fe}s(ye,"chooseScalarStyle");function Be(U,Y,Ae,me,Se){U.dump=(function(){if(Y.length===0)return U.quotingType===W?'""':"''";if(!U.noCompatMode&&(D.indexOf(Y)!==-1||L.test(Y)))return U.quotingType===W?'"'+Y+'"':"'"+Y+"'";let Ce=U.indent*Math.max(1,Ae),Me=U.lineWidth===-1?-1:Math.max(Math.min(U.lineWidth,40),U.lineWidth-Ce),_e=me||U.flowLevel>-1&&Ae>=U.flowLevel;function ze(Le){return K(U,Le)}switch(s(ze,"testAmbiguity"),ye(Y,_e,U.indent,Me,ze,U.quotingType,U.forceQuotes&&!me,Se)){case Ee:return Y;case we:return"'"+Y.replace(/'/g,"''")+"'";case Fe:return"|"+Ie(Y,U.indent)+Ne(z(Y,Ce));case Ye:return">"+Ie(Y,U.indent)+Ne(z(re(Y,Me),Ce));case He:return'"'+le(Y)+'"';default:throw new t("impossible error: invalid scalar style")}})()}s(Be,"writeScalar");function Ie(U,Y){let Ae=de(U)?String(Y):"",me=U[U.length-1]===` +`,Ce=me&&(U[U.length-2]===` +`||U===` +`)?"+":me?"":"-";return Ae+Ce+` +`}s(Ie,"blockHeader");function Ne(U){return U[U.length-1]===` +`?U.slice(0,-1):U}s(Ne,"dropEndingNewline");function re(U,Y){let Ae=/(\n+)([^\n]*)/g,me=(function(){let _e=U.indexOf(` +`);return _e=_e!==-1?_e:U.length,Ae.lastIndex=_e,ue(U.slice(0,_e),Y)})(),Se=U[0]===` +`||U[0]===" ",Ce,Me;for(;Me=Ae.exec(U);){let _e=Me[1],ze=Me[2];Ce=ze[0]===" ",me+=_e+(!Se&&!Ce&&ze!==""?` +`:"")+ue(ze,Y),Se=Ce}return me}s(re,"foldString");function ue(U,Y){if(U===""||U[0]===" ")return U;let Ae=/ [^ ]/g,me,Se=0,Ce,Me=0,_e=0,ze="";for(;me=Ae.exec(U);)_e=me.index,_e-Se>Y&&(Ce=Me>Se?Me:_e,ze+=` +`+U.slice(Se,Ce),Se=Ce+1),Me=_e;return ze+=` +`,U.length-Se>Y&&Me>Se?ze+=U.slice(Se,Me)+` +`+U.slice(Me+1):ze+=U.slice(Se),ze.slice(1)}s(ue,"foldLine");function le(U){let Y="",Ae=0;for(let me=0;me=65536?me+=2:me++){Ae=ce(U,me);let Se=I[Ae];!Se&&ne(Ae)?(Y+=U[me],Ae>=65536&&(Y+=U[me+1])):Y+=Se||O(Ae)}return Y}s(le,"escapeString");function he(U,Y,Ae){let me="",Se=U.tag;for(let Ce=0,Me=Ae.length;Ce"u"&&ee(U,Y,null,!1,!1))&&(me!==""&&(me+=","+(U.condenseFlow?"":" ")),me+=U.dump)}U.tag=Se,U.dump="["+me+"]"}s(he,"writeFlowSequence");function Te(U,Y,Ae,me){let Se="",Ce=U.tag;for(let Me=0,_e=Ae.length;Me<_e;Me+=1){let ze=Ae[Me];U.replacer&&(ze=U.replacer.call(Ae,String(Me),ze)),(ee(U,Y+1,ze,!0,!0,!1,!0)||typeof ze>"u"&&ee(U,Y+1,null,!0,!0,!1,!0))&&((!me||Se!=="")&&(Se+=H(U,Y)),U.dump&&l===U.dump.charCodeAt(0)?Se+="-":Se+="- ",Se+=U.dump)}U.tag=Ce,U.dump=Se||"[]"}s(Te,"writeBlockSequence");function oe(U,Y,Ae){let me="",Se=U.tag,Ce=Object.keys(Ae);for(let Me=0,_e=Ce.length;Me<_e;Me+=1){let ze="";me!==""&&(ze+=", "),U.condenseFlow&&(ze+='"');let Le=Ce[Me],qe=Ae[Le];U.replacer&&(qe=U.replacer.call(Ae,Le,qe)),ee(U,Y,Le,!1,!1)&&(U.dump.length>1024&&(ze+="? "),ze+=U.dump+(U.condenseFlow?'"':"")+":"+(U.condenseFlow?"":" "),ee(U,Y,qe,!1,!1)&&(ze+=U.dump,me+=ze))}U.tag=Se,U.dump="{"+me+"}"}s(oe,"writeFlowMapping");function G(U,Y,Ae,me){let Se="",Ce=U.tag,Me=Object.keys(Ae);if(U.sortKeys===!0)Me.sort();else if(typeof U.sortKeys=="function")Me.sort(U.sortKeys);else if(U.sortKeys)throw new t("sortKeys must be a boolean or a function");for(let _e=0,ze=Me.length;_e1024;lt&&(U.dump&&l===U.dump.charCodeAt(0)?Le+="?":Le+="? "),Le+=U.dump,lt&&(Le+=H(U,Y)),ee(U,Y+1,ve,!0,lt)&&(U.dump&&l===U.dump.charCodeAt(0)?Le+=":":Le+=": ",Le+=U.dump,Se+=Le)}U.tag=Ce,U.dump=Se||"{}"}s(G,"writeBlockMapping");function X(U,Y,Ae){let me=Ae?U.explicitTypes:U.implicitTypes;for(let Se=0,Ce=me.length;Se tag resolver accepts not "'+_e+'" style');U.dump=ze}return!0}}return!1}s(X,"detectType");function ee(U,Y,Ae,me,Se,Ce,Me){U.tag=null,U.dump=Ae,X(U,Ae,!1)||X(U,Ae,!0);let _e=n.call(U.dump),ze=me;me&&(me=U.flowLevel<0||U.flowLevel>Y);let Le=_e==="[object Object]"||_e==="[object Array]",qe,ve;if(Le&&(qe=U.duplicates.indexOf(Ae),ve=qe!==-1),(U.tag!==null&&U.tag!=="?"||ve||U.indent!==2&&Y>0)&&(Se=!1),ve&&U.usedDuplicates[qe])U.dump="*ref_"+qe;else{if(Le&&ve&&!U.usedDuplicates[qe]&&(U.usedDuplicates[qe]=!0),_e==="[object Object]")me&&Object.keys(U.dump).length!==0?(G(U,Y,U.dump,Se),ve&&(U.dump="&ref_"+qe+U.dump)):(oe(U,Y,U.dump),ve&&(U.dump="&ref_"+qe+" "+U.dump));else if(_e==="[object Array]")me&&U.dump.length!==0?(U.noArrayIndent&&!Me&&Y>0?Te(U,Y-1,U.dump,Se):Te(U,Y,U.dump,Se),ve&&(U.dump="&ref_"+qe+U.dump)):(he(U,Y,U.dump),ve&&(U.dump="&ref_"+qe+" "+U.dump));else if(_e==="[object String]")U.tag!=="?"&&Be(U,U.dump,Y,Ce,ze);else{if(_e==="[object Undefined]")return!1;if(U.skipInvalid)return!1;throw new t("unacceptable kind of an object to dump "+_e)}if(U.tag!==null&&U.tag!=="?"){let lt=encodeURI(U.tag[0]==="!"?U.tag.slice(1):U.tag).replace(/!/g,"%21");U.tag[0]==="!"?lt="!"+lt:lt.slice(0,18)==="tag:yaml.org,2002:"?lt="!!"+lt.slice(18):lt="!<"+lt+">",U.dump=lt+" "+U.dump}}return!0}s(ee,"writeNode");function q(U,Y){let Ae=[],me=[];ie(U,Ae,me);let Se=me.length;for(let Ce=0;Ce{"use strict";s(pZe,"getDefaultExportFromCjs");ys={},D4={},rf={};s(M2,"requireCommon");s(N2,"requireException");s(mZe,"requireSnippet");s(Fs,"requireType");s(sue,"requireSchema");s(oue,"requireStr");s(lue,"requireSeq");s(cue,"requireMap");s(uue,"requireFailsafe");s(hue,"require_null");s(due,"requireBool");s(fue,"requireInt");s(pue,"requireFloat");s(mue,"requireJson");s(gue,"requireCore");s(yue,"requireTimestamp");s(vue,"requireMerge");s(xue,"requireBinary");s(bue,"requireOmap");s(Tue,"requirePairs");s(Cue,"requireSet");s(yO,"require_default");s(gZe,"requireLoader");gO={};s(yZe,"requireDumper");s(vZe,"requireJsYaml");xZe=vZe(),bZe=pZe(xZe),{Type:_mr,Schema:Lmr,FAILSAFE_SCHEMA:Dmr,JSON_SCHEMA:nf,CORE_SCHEMA:Imr,DEFAULT_SCHEMA:Mmr,load:af,loadAll:Nmr,dump:Pmr,YAMLException:Omr,types:Bmr,safeLoad:$mr,safeLoadAll:Fmr,safeDump:Gmr}=bZe});var TZe,M4,Sue=F(()=>{"use strict";Wr();P2();Jt();St();R4();er();Yr();$n();Xd();q0();TZe="flowchart-",M4=class{constructor(){this.vertexCounter=0;this.config=Pe();this.diagramId="";this.vertices=new Map;this.edges=[];this.classes=new Map;this.subGraphs=[];this.subGraphLookup=new Map;this.tooltips=new Map;this.subCount=0;this.firstGraphFlag=!0;this.secCount=-1;this.posCrossRef=[];this.funs=[];this.setAccTitle=Lr;this.setAccDescription=Mr;this.setDiagramTitle=Gr;this.getAccTitle=Ir;this.getAccDescription=Nr;this.getDiagramTitle=Pr;this.funs.push(this.setupToolTips.bind(this)),this.addVertex=this.addVertex.bind(this),this.firstGraph=this.firstGraph.bind(this),this.setDirection=this.setDirection.bind(this),this.addSubGraph=this.addSubGraph.bind(this),this.addLink=this.addLink.bind(this),this.setLink=this.setLink.bind(this),this.updateLink=this.updateLink.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.destructLink=this.destructLink.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setTooltip=this.setTooltip.bind(this),this.updateLinkInterpolate=this.updateLinkInterpolate.bind(this),this.setClickFun=this.setClickFun.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.lex={firstGraph:this.firstGraph.bind(this)},this.clear(),this.setGen("gen-2")}static{s(this,"FlowDB")}sanitizeText(t){return Tt.sanitizeText(t,this.config)}sanitizeNodeLabelType(t){switch(t){case"markdown":case"string":case"text":return t;default:return"markdown"}}setDiagramId(t){this.diagramId=t}lookUpDomId(t){for(let r of this.vertices.values())if(r.id===t)return this.diagramId?`${this.diagramId}-${r.domId}`:r.domId;return this.diagramId?`${this.diagramId}-${t}`:t}addVertex(t,r,n,i,a,o,l={},u){if(!t||t.trim().length===0)return;let h;if(u!==void 0){let g;u.includes(` +`)?g=u+` +`:g=`{ +`+u+` +}`,h=af(g,{schema:nf})}let d=this.subGraphLookup.get(t);if(d&&h){d.metadata={...d.metadata,...h};return}let f=this.edges.find(g=>g.id===t);if(f){let g=h;g?.animate!==void 0&&(f.animate=g.animate),g?.animation!==void 0&&(f.animation=g.animation),g?.curve!==void 0&&(f.interpolate=g.curve);return}let p,m=this.vertices.get(t);if(m===void 0&&(r===void 0&&n===void 0&&i!==void 0&&i!==null&&se.warn(`Style applied to unknown node "${t}". This may indicate a typo. The node will be created automatically.`),m={id:t,labelType:"text",domId:TZe+t+"-"+this.vertexCounter,styles:[],classes:[]},this.vertices.set(t,m)),this.vertexCounter++,r!==void 0?(this.config=Pe(),p=this.sanitizeText(r.text.trim()),m.labelType=r.type,p.startsWith('"')&&p.endsWith('"')&&(p=p.substring(1,p.length-1)),m.text=p):m.text===void 0&&(m.text=t),n!==void 0&&(m.type=n),i?.forEach(g=>{m.styles.push(g)}),a?.forEach(g=>{m.classes.push(g)}),o!==void 0&&(m.dir=o),m.props===void 0?m.props=l:l!==void 0&&Object.assign(m.props,l),h!==void 0){if(h.shape){if(h.shape!==h.shape.toLowerCase()||h.shape.includes("_"))throw new Error(`No such shape: ${h.shape}. Shape names should be lowercase.`);if(!Cce(h.shape))throw new Error(`No such shape: ${h.shape}.`);m.type=h?.shape}h?.label&&(m.text=h?.label,m.labelType=this.sanitizeNodeLabelType(h?.labelType)),h?.icon&&(m.icon=h?.icon,!h.label?.trim()&&m.text===t&&(m.text="")),h?.form&&(m.form=h?.form),h?.pos&&(m.pos=h?.pos),h?.img&&(m.img=h?.img,!h.label?.trim()&&m.text===t&&(m.text="")),h?.constraint&&(m.constraint=h.constraint),h.w&&(m.assetWidth=Number(h.w)),h.h&&(m.assetHeight=Number(h.h))}}addSingleLink(t,r,n,i){let l={start:t,end:r,type:void 0,text:"",labelType:"text",classes:[],isUserDefinedId:!1,interpolate:this.edges.defaultInterpolate};se.info("abc78 Got edge...",l);let u=n.text;if(u!==void 0&&(l.text=this.sanitizeText(u.text.trim()),l.text.startsWith('"')&&l.text.endsWith('"')&&(l.text=l.text.substring(1,l.text.length-1)),l.labelType=this.sanitizeNodeLabelType(u.type)),n!==void 0&&(l.type=n.type,l.stroke=n.stroke,l.length=n.length>10?10:n.length),i&&!this.edges.some(h=>h.id===i))l.id=i,l.isUserDefinedId=!0;else{let h=this.edges.filter(d=>d.start===l.start&&d.end===l.end);h.length===0?l.id=Jc(l.start,l.end,{counter:0,prefix:"L"}):l.id=Jc(l.start,l.end,{counter:h.length+1,prefix:"L"})}if(this.edges.length<(this.config.maxEdges??500))se.info("Pushing edge..."),this.edges.push(l);else throw new Error(`Edge limit exceeded. ${this.edges.length} edges found, but the limit is ${this.config.maxEdges}. + +Initialize mermaid with maxEdges set to a higher number to allow more edges. +You cannot set this config via configuration inside the diagram as it is a secure config. +You have to call mermaid.initialize.`)}isLinkData(t){return t!==null&&typeof t=="object"&&"id"in t&&typeof t.id=="string"}addLink(t,r,n){let i=this.isLinkData(n)?n.id.replace("@",""):void 0;se.info("addLink",t,r,i);for(let a of t)for(let o of r){let l=a===t[t.length-1],u=o===r[0];l&&u?this.addSingleLink(a,o,n,i):this.addSingleLink(a,o,n,void 0)}}updateLinkInterpolate(t,r){t.forEach(n=>{n==="default"?this.edges.defaultInterpolate=r:this.edges[n].interpolate=r})}updateLink(t,r){t.forEach(n=>{if(typeof n=="number"&&n>=this.edges.length)throw new Error(`The index ${n} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${this.edges.length-1}. (Help: Ensure that the index is within the range of existing edges.)`);n==="default"?this.edges.defaultStyle=r:(this.edges[n].style=r,(this.edges[n]?.style?.length??0)>0&&!this.edges[n]?.style?.some(i=>i?.startsWith("fill"))&&this.edges[n]?.style?.push("fill:none"))})}addClass(t,r){let n=r.join().replace(/\\,/g,"\xA7\xA7\xA7").replace(/,/g,";").replace(/§§§/g,",").split(";");t.split(",").forEach(i=>{let a=this.classes.get(i);a===void 0&&(a={id:i,styles:[],textStyles:[]},this.classes.set(i,a)),n?.forEach(o=>{if(/color/.exec(o)){let l=o.replace("fill","bgFill");a.textStyles.push(l)}a.styles.push(o)})})}setDirection(t){this.direction=t.trim(),/.*/.exec(this.direction)&&(this.direction="LR"),/.*v/.exec(this.direction)&&(this.direction="TB"),this.direction==="TD"&&(this.direction="TB")}setClass(t,r){for(let n of t.split(",")){let i=this.vertices.get(n);i&&i.classes.push(r);let a=this.edges.find(l=>l.id===n);a&&a.classes.push(r);let o=this.subGraphLookup.get(n);o&&o.classes.push(r)}}setTooltip(t,r){if(r!==void 0){r=this.sanitizeText(r);for(let n of t.split(","))this.tooltips.set(this.version==="gen-1"?this.lookUpDomId(n):n,r)}}setClickFun(t,r,n){if(Pe().securityLevel!=="loose"||r===void 0)return;let i=[];if(typeof n=="string"){i=n.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);for(let o=0;o{let o=this.lookUpDomId(t),l=document.querySelector(`[id="${o}"]`);l!==null&&l.addEventListener("click",()=>{or.runFunc(r,...i)},!1)}))}setLink(t,r,n){t.split(",").forEach(i=>{let a=this.vertices.get(i);a!==void 0&&(a.link=or.formatUrl(r,this.config),a.linkTarget=n)}),this.setClass(t,"clickable")}getTooltip(t){return this.tooltips.get(t)}setClickEvent(t,r,n){t.split(",").forEach(i=>{this.setClickFun(i,r,n)}),this.setClass(t,"clickable")}bindFunctions(t){this.funs.forEach(r=>{r(t)})}getDirection(){return this.direction?.trim()}getVertices(){return this.vertices}getEdges(){return this.edges}getClasses(){return this.classes}setupToolTips(t){let r=_y();ut(t).select("svg").selectAll("g.node").on("mouseover",a=>{let o=ut(a.currentTarget),l=o.attr("title");if(l===null)return;let u=a.currentTarget?.getBoundingClientRect();r.transition().duration(200).style("opacity",".9"),r.text(o.attr("title")).style("left",window.scrollX+u.left+(u.right-u.left)/2+"px").style("top",window.scrollY+u.bottom+"px"),r.html(ao.sanitize(l)),o.classed("hover",!0)}).on("mouseout",a=>{r.transition().duration(500).style("opacity",0),ut(a.currentTarget).classed("hover",!1)})}clear(t="gen-2"){this.vertices=new Map,this.classes=new Map,this.edges=[],this.funs=[this.setupToolTips.bind(this)],this.diagramId="",this.subGraphs=[],this.subGraphLookup=new Map,this.subCount=0,this.tooltips=new Map,this.firstGraphFlag=!0,this.version=t,this.config=Pe(),Tr()}setGen(t){this.version=t||"gen-2"}defaultStyle(){return"fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;"}addSubGraph(t,r,n){let i=t.text.trim(),a=n.text;t===n&&/\s/.exec(n.text)&&(i=void 0);let l=s(p=>{let m={boolean:{},number:{},string:{}},g=[],y;return{nodeList:p.filter(function(x){let b=typeof x;return x.stmt&&x.stmt==="dir"?(y=x.value,!1):x.trim()===""?!1:b in m?m[b].hasOwnProperty(x)?!1:m[b][x]=!0:g.includes(x)?!1:g.push(x)}),dir:y}},"uniq")(r.flat()),u=l.nodeList,h=l.dir,d=Pe().flowchart??{};if(h=h??(d.inheritDir?this.getDirection()??Pe().direction??void 0:void 0),this.version==="gen-1")for(let p=0;p2e3)return{result:!1,count:0};if(this.posCrossRef[this.secCount]=r,this.subGraphs[r].id===t)return{result:!0,count:0};let i=0,a=1;for(;i=0){let l=this.indexNodes2(t,o);if(l.result)return{result:!0,count:a+l.count};a=a+l.count}i=i+1}return{result:!1,count:a}}getDepthFirstPos(t){return this.posCrossRef[t]}indexNodes(){this.secCount=-1,this.subGraphs.length>0&&this.indexNodes2("none",this.subGraphs.length-1)}getSubGraphs(){return this.subGraphs}firstGraph(){return this.firstGraphFlag?(this.firstGraphFlag=!1,!0):!1}destructStartLink(t){let r=t.trim(),n="arrow_open";switch(r[0]){case"<":n="arrow_point",r=r.slice(1);break;case"x":n="arrow_cross",r=r.slice(1);break;case"o":n="arrow_circle",r=r.slice(1);break}let i="normal";return r.includes("=")&&(i="thick"),r.includes(".")&&(i="dotted"),{type:n,stroke:i}}countChar(t,r){let n=r.length,i=0;for(let a=0;a":i="arrow_point",r.startsWith("<")&&(i="double_"+i,n=n.slice(1));break;case"o":i="arrow_circle",r.startsWith("o")&&(i="double_"+i,n=n.slice(1));break}let a="normal",o=n.length-1;n.startsWith("=")&&(a="thick"),n.startsWith("~")&&(a="invisible");let l=this.countChar(".",n);return l&&(a="dotted",o=l),{type:i,stroke:a,length:o}}destructLink(t,r){let n=this.destructEndLink(t),i;if(r){if(i=this.destructStartLink(r),i.stroke!==n.stroke)return{type:"INVALID",stroke:"INVALID"};if(i.type==="arrow_open")i.type=n.type;else{if(i.type!==n.type)return{type:"INVALID",stroke:"INVALID"};i.type="double_"+i.type}return i.type==="double_arrow"&&(i.type="double_arrow_point"),i.length=n.length,i}return n}exists(t,r){for(let n of t)if(n.nodes.includes(r))return!0;return!1}makeUniq(t,r){let n=[];return t.nodes.forEach((i,a)=>{this.exists(r,i)||n.push(t.nodes[a])}),{nodes:n}}getTypeFromVertex(t){if(t.img)return"imageSquare";if(t.icon)return t.form==="circle"?"iconCircle":t.form==="square"?"iconSquare":t.form==="rounded"?"iconRounded":"icon";switch(t.type){case"square":case void 0:return"squareRect";case"round":return"roundedRect";case"ellipse":return"ellipse";default:return t.type}}findNode(t,r){return t.find(n=>n.id===r)}destructEdgeType(t){let r="none",n="arrow_point";switch(t){case"arrow_point":case"arrow_circle":case"arrow_cross":n=t;break;case"double_arrow_point":case"double_arrow_circle":case"double_arrow_cross":r=t.replace("double_",""),n=r;break}return{arrowTypeStart:r,arrowTypeEnd:n}}addNodeFromVertex(t,r,n,i,a,o){let l=n.get(t.id),u=i.get(t.id)??!1,h=this.findNode(r,t.id);if(h)h.cssStyles=t.styles,h.cssCompiledStyles=this.getCompiledStyles(t.classes),h.cssClasses=t.classes.join(" ");else{let d={id:t.id,label:t.text,labelType:t.labelType,labelStyle:"",parentId:l,padding:a.flowchart?.padding||8,cssStyles:t.styles,cssCompiledStyles:this.getCompiledStyles(["default","node",...t.classes]),cssClasses:"default "+t.classes.join(" "),dir:t.dir,domId:t.domId,look:o,link:t.link,linkTarget:t.linkTarget,tooltip:this.getTooltip(t.id),icon:t.icon,pos:t.pos,img:t.img,assetWidth:t.assetWidth,assetHeight:t.assetHeight,constraint:t.constraint};u?r.push({...d,isGroup:!0,shape:"rect"}):r.push({...d,isGroup:!1,shape:this.getTypeFromVertex(t)})}}getCompiledStyles(t){let r=[];for(let n of t){let i=this.classes.get(n);i?.styles&&(r=[...r,...i.styles??[]].map(a=>a.trim())),i?.textStyles&&(r=[...r,...i.textStyles??[]].map(a=>a.trim()))}return r}getData(){let t=Pe(),r=[],n=[],i=this.getSubGraphs(),a=new Map,o=new Map,l=new Map;for(let g of i)for(let y of g.nodes)this.subGraphLookup.has(y)&&l.set(y,g.id);let u=s(g=>this.subGraphLookup.get(g)?.metadata?.view==="collapsed","isCollapsed"),h=s(g=>{let y,v=new Set,x=g;for(;x!==void 0&&!v.has(x);)v.add(x),u(x)&&(y=x),x=l.get(x);return y},"outermostCollapsed"),d=new Set,f=new Map;for(let g of i){let y=h(g.id);if(y!==void 0){g.id!==y&&(d.add(g.id),f.set(g.id,y));for(let v of g.nodes)v!==y&&(d.add(v),f.set(v,y))}}for(let g=i.length-1;g>=0;g--){let y=i[g];if(!d.has(y.id)){y.nodes.length>0&&o.set(y.id,!0);for(let v of y.nodes)a.set(v,y.id)}}for(let g=i.length-1;g>=0;g--){let y=i[g];d.has(y.id)||(y.metadata?.view==="collapsed"?r.push({id:y.id,label:y.title,labelStyle:"",labelType:y.labelType,parentId:a.get(y.id),padding:8,cssCompiledStyles:this.getCompiledStyles(y.classes),cssClasses:y.classes.join(" "),shape:"collapsedGroup",dir:y.dir,isGroup:!1,look:t.look}):r.push({id:y.id,label:y.title,labelStyle:"",labelType:y.labelType,parentId:a.get(y.id),padding:8,cssCompiledStyles:this.getCompiledStyles(y.classes),cssClasses:y.classes.join(" "),shape:"rect",dir:y.dir,isGroup:!0,look:t.look}))}this.getVertices().forEach(g=>{d.has(g.id)||this.addNodeFromVertex(g,r,a,o,t,t.look||"classic")});let m=this.getEdges();return m.forEach((g,y)=>{let{arrowTypeStart:v,arrowTypeEnd:x}=this.destructEdgeType(g.type),b=[...m.defaultStyle??[]],T=f.get(g.start)??g.start,w=f.get(g.end)??g.end;if(T===w&&(f.has(g.start)||f.has(g.end)))return;g.style&&b.push(...g.style);let C={id:Jc(T,w,{counter:y,prefix:"L"},g.id),isUserDefinedId:g.isUserDefinedId,start:T,end:w,type:g.type??"normal",label:g.text,labelType:g.labelType,labelpos:"c",thickness:g.stroke,minlen:g.length,classes:g?.stroke==="invisible"?"":"edge-thickness-normal edge-pattern-solid flowchart-link",arrowTypeStart:g?.stroke==="invisible"||g?.type==="arrow_open"?"none":v,arrowTypeEnd:g?.stroke==="invisible"||g?.type==="arrow_open"?"none":x,arrowheadStyle:"fill: #333",cssCompiledStyles:this.getCompiledStyles(g.classes),labelStyle:b,style:b,pattern:g.stroke,look:t.look,animate:g.animate,animation:g.animation,curve:g.interpolate||this.edges.defaultInterpolate||t.flowchart?.curve};n.push(C)}),{nodes:r,edges:n,other:{},config:t}}defaultConfig(){return pk.flowchart}}});var xl,Rm=F(()=>{"use strict";Wr();xl=s((e,t)=>{let r;return t==="sandbox"&&(r=ut("#i"+e)),(t==="sandbox"?ut(r.nodes()[0].contentDocument.body):ut("body")).select(`[id="${e}"]`)},"getDiagramElement")});var kh,O2=F(()=>{"use strict";kh=s(({flowchart:e})=>{let t=e?.subGraphTitleMargin?.top??0,r=e?.subGraphTitleMargin?.bottom??0,n=t+r;return{subGraphTitleTopMargin:t,subGraphTitleBottomMargin:r,subGraphTitleTotalMargin:n}},"getSubGraphTitleMargins")});var wue,kue=F(()=>{"use strict";Jt();br();St();Wr();Kt();qo();p4();Xt();wue=s(async(e,t)=>{let r=Pe(),{themeVariables:n,handDrawnSeed:i}=r,{clusterBkg:a,clusterBorder:o}=n,l=o,{labelStyles:u,nodeStyles:h,borderStyles:d,backgroundStyles:f}=st(t),p=e.insert("g").attr("class","cluster swimlane "+(t.cssClasses||"")).attr("id",t.id).attr("data-id",t.id).attr("data-et","cluster").attr("data-look",t.look),m=Aa(r.flowchart.htmlLabels),g=t.direction==="LR",y=p.insert("g").attr("class","cluster-label swimlane-label"),v=await li(y,t.label,{style:t.labelStyle,useHtmlLabels:m,isNode:!0,width:t.width}),x=v.getBBox();if(m){let A=v.children[0],I=ut(v);x=A.getBoundingClientRect(),I.attr("width",x.width),I.attr("height",x.height)}let b=t.padding??0,T=t.width<=x.width+b?x.width+b:t.width;t.width<=x.width+b?t.diff=(T-t.width)/2-b:t.diff=-b;let w=t.height,C=t.y-w/2,S=t.y+w/2,k=t.x-T/2,R=t.swimlaneContentTop!==void 0?t.swimlaneContentTop:C+w/3,M=g?4:0,P=x.height+2*M,N,_;if(g){let A=Math.max(P,x.height+2*M),I=k+A,D=Math.max(0,T-A);if(t.look==="handDrawn"){let O=ot.svg(p),$=ft(t,{roughness:.7,fill:a,stroke:l,fillWeight:3,seed:i}),W=ft(t,{roughness:.7,fill:"none",stroke:l,seed:i}),V=O.rectangle(k,C,A,w,$);N=p.insert(()=>V,":first-child");let z=O.rectangle(I,C,D,w,W);_=p.insert(()=>z,":first-child"),N.select("path:nth-child(2)").attr("style",d.join(";")),N.select("path").attr("style",f.join(";").replace("fill","stroke"))}else N=p.insert("rect",":first-child"),_=p.insert("rect",":first-child"),N.attr("class","swimlane-title").attr("style",h).attr("x",k).attr("y",C).attr("width",A).attr("height",w).attr("fill",a).attr("stroke",l),_.attr("class","swimlane-body").attr("style",h).attr("x",I).attr("y",C).attr("width",D).attr("height",w).attr("fill","none").attr("stroke",l);let L=k+A/2,B=t.y;y.attr("transform",`translate(${L}, ${B}) rotate(-90) translate(${-x.width/2}, ${-x.height/2})`)}else{let A=Math.max(0,R-C),I=Math.min(P,A),D=C+I,L=Math.max(0,S-D),B=t.x-T/2;if(t.look==="handDrawn"){let W=ot.svg(p),V=ft(t,{roughness:.7,fill:a,stroke:l,fillWeight:3,seed:i}),z=ft(t,{roughness:.7,fill:"none",stroke:l,seed:i}),H=W.rectangle(B,C,T,I,V);N=p.insert(()=>H,":first-child");let K=W.rectangle(B,D,T,L,z);_=p.insert(()=>K,":first-child"),N.select("path:nth-child(2)").attr("style",d.join(";")),N.select("path").attr("style",f.join(";").replace("fill","stroke"))}else N=p.insert("rect",":first-child"),_=p.insert("rect",":first-child"),N.attr("class","swimlane-title").attr("style",h).attr("x",B).attr("y",C).attr("width",T).attr("height",I).attr("fill",a).attr("stroke",l),_.attr("class","swimlane-body").attr("style",h).attr("x",B).attr("y",D).attr("width",T).attr("height",L).attr("fill","none").attr("stroke",l);let O=t.x-x.width/2,$=C+(I-x.height)/2;y.attr("transform",`translate(${O}, ${$})`)}if(se.trace("Swimlane data ",t,JSON.stringify(t)),u){let A=y.select("span");A&&A.attr("style",u)}return t.offsetX=0,t.width=T,t.height=w,t.offsetY=x.height-b/2,t.intersect=function(A){return ru(t,A)},{cluster:p,labelBBox:x}},"swimlane")});var Eue,CZe,SZe,wZe,kZe,EZe,AZe,Aue,sf,Rue,B2=F(()=>{"use strict";Jt();br();St();O2();Wr();Kt();qo();p4();E4();ic();Xt();kue();Eue=s(async(e,t)=>{se.info("Creating subgraph rect for ",t.id,t);let r=Pe(),{themeVariables:n,handDrawnSeed:i}=r,{clusterBkg:a,clusterBorder:o}=n,{labelStyles:l,nodeStyles:u,borderStyles:h,backgroundStyles:d}=st(t),f=e.insert("g").attr("class","cluster "+t.cssClasses).attr("id",t.domId).attr("data-look",t.look),p=ai(r),m=f.insert("g").attr("class","cluster-label "),g;t.labelType==="markdown"?g=await li(m,t.label,{style:t.labelStyle,useHtmlLabels:p,isNode:!0,width:t.width}):g=await sc(m,t.label,t.labelStyle||"",!1,!0);let y=g.getBBox();if(ai(r)){let k=g.children[0],R=ut(g);y=k.getBoundingClientRect(),R.attr("width",y.width),R.attr("height",y.height)}let v=t.width<=y.width+t.padding?y.width+t.padding:t.width;t.width<=y.width+t.padding?t.diff=(v-t.width)/2-t.padding:t.diff=-t.padding;let x=t.height,b=t.x-v/2,T=t.y-x/2;se.trace("Data ",t,JSON.stringify(t));let w;if(t.look==="handDrawn"){let k=ot.svg(f),R=ft(t,{roughness:.7,fill:a,stroke:o,fillWeight:3,seed:i}),M=k.path(Mi(b,T,v,x,0),R);w=f.insert(()=>(se.debug("Rough node insert CXC",M),M),":first-child"),w.select("path:nth-child(2)").attr("style",h.join(";")),w.select("path").attr("style",d.join(";").replace("fill","stroke"))}else w=f.insert("rect",":first-child"),w.attr("style",u).attr("rx",t.rx).attr("ry",t.ry).attr("x",b).attr("y",T).attr("width",v).attr("height",x);let{subGraphTitleTopMargin:C}=kh(r);if(m.attr("transform",`translate(${t.x-y.width/2}, ${t.y-t.height/2+C})`),l){let k=m.select("span");k&&k.attr("style",l)}let S=w.node().getBBox();return t.offsetX=0,t.width=S.width,t.height=S.height,t.offsetY=y.height-t.padding/2,t.intersect=function(k){return ru(t,k)},{cluster:f,labelBBox:y}},"rect"),CZe=s((e,t)=>{let r=e.insert("g").attr("class","note-cluster").attr("id",t.domId),n=r.insert("rect",":first-child"),i=0*t.padding,a=i/2;n.attr("rx",t.rx).attr("ry",t.ry).attr("x",t.x-t.width/2-a).attr("y",t.y-t.height/2-a).attr("width",t.width+i).attr("height",t.height+i).attr("fill","none");let o=n.node().getBBox();return t.width=o.width,t.height=o.height,t.intersect=function(l){return ru(t,l)},{cluster:r,labelBBox:{width:0,height:0}}},"noteGroup"),SZe=s(async(e,t)=>{let r=Pe(),{themeVariables:n,handDrawnSeed:i}=r,{altBackground:a,compositeBackground:o,compositeTitleBackground:l,nodeBorder:u}=n,h=e.insert("g").attr("class",t.cssClasses).attr("id",t.domId).attr("data-id",t.id).attr("data-look",t.look),d=h.insert("g",":first-child"),f=h.insert("g").attr("class","cluster-label"),p=h.append("rect"),m=await sc(f,t.label,t.labelStyle,void 0,!0),g=m.getBBox();if(ai(r)){let M=m.children[0],P=ut(m);g=M.getBoundingClientRect(),P.attr("width",g.width),P.attr("height",g.height)}let y=0*t.padding,v=y/2,x=(t.width<=g.width+t.padding?g.width+t.padding:t.width)+y;t.width<=g.width+t.padding?t.diff=(x-t.width)/2-t.padding:t.diff=-t.padding;let b=t.height+y,T=t.height+y-g.height-6,w=t.x-x/2,C=t.y-b/2;t.width=x;let S=t.y-t.height/2-v+g.height+2,k;if(t.look==="handDrawn"){let M=t.cssClasses.includes("statediagram-cluster-alt"),P=ot.svg(h),N=t.rx||t.ry?P.path(Mi(w,C,x,b,10),{roughness:.7,fill:l,fillStyle:"solid",stroke:u,seed:i}):P.rectangle(w,C,x,b,{seed:i});k=h.insert(()=>N,":first-child");let _=P.rectangle(w,S,x,T,{fill:M?a:o,fillStyle:M?"hachure":"solid",stroke:u,seed:i});k=h.insert(()=>N,":first-child"),p=h.insert(()=>_)}else k=d.insert("rect",":first-child"),k.attr("class","outer").attr("x",w).attr("y",C).attr("width",x).attr("height",b).attr("data-look",t.look),p.attr("class","inner").attr("x",w).attr("y",S).attr("width",x).attr("height",T);f.attr("transform",`translate(${t.x-g.width/2}, ${C+1-(ai(r)?0:3)})`);let R=k.node().getBBox();return t.height=R.height,t.offsetX=0,t.offsetY=g.height-t.padding/2,t.labelBBox=g,t.intersect=function(M){return ru(t,M)},{cluster:h,labelBBox:g}},"roundedWithTitle"),wZe=s(async(e,t)=>{se.info("Creating subgraph rect for ",t.id,t);let r=Pe(),{themeVariables:n,handDrawnSeed:i}=r,{clusterBkg:a,clusterBorder:o}=n,{labelStyles:l,nodeStyles:u,borderStyles:h,backgroundStyles:d}=st(t),f=e.insert("g").attr("class","cluster "+t.cssClasses).attr("id",t.domId).attr("data-look",t.look),p=ai(r),m=f.insert("g").attr("class","cluster-label "),g=await li(m,t.label,{style:t.labelStyle,useHtmlLabels:p,isNode:!0,width:t.width}),y=g.getBBox();if(ai(r)){let k=g.children[0],R=ut(g);y=k.getBoundingClientRect(),R.attr("width",y.width),R.attr("height",y.height)}let v=t.width<=y.width+t.padding?y.width+t.padding:t.width;t.width<=y.width+t.padding?t.diff=(v-t.width)/2-t.padding:t.diff=-t.padding;let x=t.height,b=t.x-v/2,T=t.y-x/2;se.trace("Data ",t,JSON.stringify(t));let w;if(t.look==="handDrawn"){let k=ot.svg(f),R=ft(t,{roughness:.7,fill:a,stroke:o,fillWeight:4,seed:i}),M=k.path(Mi(b,T,v,x,t.rx),R);w=f.insert(()=>(se.debug("Rough node insert CXC",M),M),":first-child"),w.select("path:nth-child(2)").attr("style",h.join(";")),w.select("path").attr("style",d.join(";").replace("fill","stroke"))}else w=f.insert("rect",":first-child"),w.attr("style",u).attr("rx",t.rx).attr("ry",t.ry).attr("x",b).attr("y",T).attr("width",v).attr("height",x);let{subGraphTitleTopMargin:C}=kh(r);if(m.attr("transform",`translate(${t.x-y.width/2}, ${t.y-t.height/2+C})`),l){let k=m.select("span");k&&k.attr("style",l)}let S=w.node().getBBox();return t.offsetX=0,t.width=S.width,t.height=S.height,t.offsetY=y.height-t.padding/2,t.intersect=function(k){return ru(t,k)},{cluster:f,labelBBox:y}},"kanbanSection"),kZe=s((e,t)=>{let r=Pe(),{themeVariables:n,handDrawnSeed:i}=r,{nodeBorder:a}=n,o=e.insert("g").attr("class",t.cssClasses).attr("id",t.domId).attr("data-look",t.look),l=o.insert("g",":first-child"),u=0*t.padding,h=t.width+u;t.diff=-t.padding;let d=t.height+u,f=t.x-h/2,p=t.y-d/2;t.width=h;let m;if(t.look==="handDrawn"){let v=ot.svg(o).rectangle(f,p,h,d,{fill:"lightgrey",roughness:.5,strokeLineDash:[5],stroke:a,seed:i});m=o.insert(()=>v,":first-child")}else{m=l.insert("rect",":first-child");let y="outer";t.look,y="divider",m.attr("class",y).attr("x",f).attr("y",p).attr("width",h).attr("height",d).attr("data-look",t.look)}let g=m.node().getBBox();return t.height=g.height,t.offsetX=0,t.offsetY=0,t.intersect=function(y){return ru(t,y)},{cluster:o,labelBBox:{}}},"divider"),EZe=Eue,AZe={rect:Eue,squareRect:EZe,roundedWithTitle:SZe,noteGroup:CZe,divider:kZe,kanbanSection:wZe,swimlane:wue},Aue=new Map,sf=s(async(e,t)=>{let r=t.shape||"rect",n=await AZe[r](e,t);return Aue.set(t.id,n),n},"insertCluster"),Rue=s(()=>{Aue=new Map},"clear")});var By,_ue=F(()=>{"use strict";By=s((e,t)=>{if(t)return"translate("+-e.width/2+", "+-e.height/2+")";let r=e.x??0,n=e.y??0;return"translate("+-(r+e.width/2)+", "+-(n+e.height/2)+")"},"computeLabelTransform")});function N4(e,t){if(e===void 0||t===void 0)return{angle:0,deltaX:0,deltaY:0};e=gi(e),t=gi(t);let[r,n]=[e.x,e.y],[i,a]=[t.x,t.y],o=i-r,l=a-n;return{angle:Math.atan(l/o),deltaX:o,deltaY:l}}var ra,vO,gi,Lue,xO=F(()=>{"use strict";ra={aggregation:17.25,extension:17.25,composition:17.25,dependency:6,lollipop:13.5,arrow_point:4,arrow_barb:0,arrow_barb_neo:5.5},vO={arrow_point:4,arrow_cross:12.5,arrow_circle:12.5};s(N4,"calculateDeltaAndAngle");gi=s(e=>Array.isArray(e)?{x:e[0],y:e[1]}:e,"pointTransformer"),Lue=s(e=>({x:s(function(t,r,n){let i=0,a=gi(n[0]).x=0?1:-1)}else if(r===n.length-1&&Object.hasOwn(ra,e.arrowTypeEnd)){let{angle:m,deltaX:g}=N4(n[n.length-1],n[n.length-2]);i=ra[e.arrowTypeEnd]*Math.cos(m)*(g>=0?1:-1)}let o=Math.abs(gi(t).x-gi(n[n.length-1]).x),l=Math.abs(gi(t).y-gi(n[n.length-1]).y),u=Math.abs(gi(t).x-gi(n[0]).x),h=Math.abs(gi(t).y-gi(n[0]).y),d=ra[e.arrowTypeStart],f=ra[e.arrowTypeEnd],p=1;if(o0&&l0&&h=0?1:-1)}else if(r===n.length-1&&Object.hasOwn(ra,e.arrowTypeEnd)){let{angle:m,deltaY:g}=N4(n[n.length-1],n[n.length-2]);i=ra[e.arrowTypeEnd]*Math.abs(Math.sin(m))*(g>=0?1:-1)}let o=Math.abs(gi(t).y-gi(n[n.length-1]).y),l=Math.abs(gi(t).x-gi(n[n.length-1]).x),u=Math.abs(gi(t).y-gi(n[0]).y),h=Math.abs(gi(t).x-gi(n[0]).x),d=ra[e.arrowTypeStart],f=ra[e.arrowTypeEnd],p=1;if(o0&&l0&&h{"use strict";St();Iue=s((e,t,r,n,i,a=!1,o)=>{t.arrowTypeStart&&Due(e,"start",t.arrowTypeStart,r,n,i,a,o),t.arrowTypeEnd&&Due(e,"end",t.arrowTypeEnd,r,n,i,a,o)},"addEdgeMarkers"),RZe={arrow_cross:{type:"cross",fill:!1},arrow_point:{type:"point",fill:!0},arrow_barb:{type:"barb",fill:!0},arrow_barb_neo:{type:"barb",fill:!0},arrow_circle:{type:"circle",fill:!1},aggregation:{type:"aggregation",fill:!1},extension:{type:"extension",fill:!1},composition:{type:"composition",fill:!0},dependency:{type:"dependency",fill:!0},lollipop:{type:"lollipop",fill:!1},only_one:{type:"onlyOne",fill:!1},zero_or_one:{type:"zeroOrOne",fill:!1},one_or_more:{type:"oneOrMore",fill:!1},zero_or_more:{type:"zeroOrMore",fill:!1},requirement_arrow:{type:"requirement_arrow",fill:!1},requirement_contains:{type:"requirement_contains",fill:!1}},_Ze=["cross","point","circle","lollipop","aggregation","extension","composition","dependency","barb"],Due=s((e,t,r,n,i,a,o=!1,l)=>{if(!r||r==="none")return;let u=RZe[r],h=u&&_Ze.includes(u.type);if(!u){se.warn(`Unknown arrow type: ${r}`);return}let d=u.type,m=`${i}_${a}-${d}${t==="start"?"Start":"End"}${o&&h?"-margin":""}`;if(l&&l.trim()!==""){let g=l.replace(/[^\dA-Za-z]/g,"_"),y=`${m}_${g}`;if(!document.getElementById(y)){let v=document.getElementById(m);if(v){let x=v.cloneNode(!0);x.id=y,x.querySelectorAll("path, circle, line").forEach(T=>{T.setAttribute("stroke",l),u.fill&&T.setAttribute("fill",l)}),v.parentNode?.appendChild(x)}}e.attr(`marker-${t}`,`url(${n}#${y})`)}else e.attr(`marker-${t}`,`url(${n}#${m})`)},"addEdgeMarker")});function P4(e,t){ai(Pe())&&e&&(e.style.width=t.length*9+"px",e.style.height="12px")}function NZe(e){let t=[],r=[];for(let n=1;n5&&Math.abs(a.y-i.y)>5||i.y===a.y&&a.x===o.x&&Math.abs(a.x-i.x)>5&&Math.abs(a.y-o.y)>5)&&(t.push(a),r.push(n))}return{cornerPoints:t,cornerPointPositions:r}}function BZe(e,t){if(e.length<2)return"";let r="",n=e.length,i=1e-5;for(let a=0;a({...i}));if(e.length>=2&&ra[t.arrowTypeStart]){let i=ra[t.arrowTypeStart],a=e[0],o=e[1],{angle:l}=Oue(a,o),u=i*Math.cos(l),h=i*Math.sin(l);r[0].x=a.x+u,r[0].y=a.y+h}let n=e.length;if(n>=2&&ra[t.arrowTypeEnd]){let i=ra[t.arrowTypeEnd],a=e[n-1],o=e[n-2],{angle:l}=Oue(o,a),u=i*Math.cos(l),h=i*Math.sin(l);r[n-1].x=a.x-u,r[n-1].y=a.y-h}return r}var LZe,_m,Pi,Bue,O4,$2,oc,$y,DZe,IZe,MZe,Nue,Pue,PZe,OZe,of,Fy=F(()=>{"use strict";Jt();br();St();qo();u4();_ue();er();xO();O2();Wr();Kt();E4();Mue();Xt();LZe=s(e=>typeof e=="string"?e:Pe()?.flowchart?.curve,"resolveEdgeCurveType"),_m=new Map,Pi=new Map,Bue=s(()=>{_m.clear(),Pi.clear()},"clear"),O4=s(e=>!!(e.label||e.startLabelLeft||e.startLabelRight||e.endLabelLeft||e.endLabelRight),"hasEdgeLabel"),$2=s(e=>e?typeof e=="string"?e:e.reduce((t,r)=>t+";"+r,""):"","getLabelStyles"),oc=s(async(e,t)=>{let r=Pe(),n=ai(r),{labelStyles:i}=st(t);t.labelStyle=i;let a=e.insert("g").attr("class","edgeLabel"),o=a.insert("g").attr("class","label").attr("data-id",t.id),l=t.labelType==="markdown",h=await li(e,t.label,{style:$2(t.labelStyle),useHtmlLabels:n,addSvgBackground:!0,isNode:!1,markdown:l,width:l?void 0:void 0},r);o.node().appendChild(h),se.info("abc82",t,t.labelType);let d,f;if(n){let m=h.children[0],g=ut(h);d=await tu.measure(()=>m.getBoundingClientRect()),f=d,g.attr("width",d.width),g.attr("height",d.height)}else{let m=ut(h).select("text").node();await tu.measure(()=>{d=h.getBBox(),m&&typeof m.getBBox=="function"?f=m.getBBox():f=d})}o.attr("transform",By(f,n)),_m.set(t.id,a),t.width=d.width,t.height=d.height;let p;if(t.startLabelLeft){let m=e.insert("g").attr("class","edgeTerminals"),g=m.insert("g").attr("class","inner"),y=await sc(g,t.startLabelLeft,$2(t.labelStyle)||"",!1,!1);p=y;let v=y.getBBox();if(n){let x=y.children[0],b=ut(y);v=x.getBoundingClientRect(),b.attr("width",v.width),b.attr("height",v.height)}g.attr("transform",By(v,n)),Pi.get(t.id)||Pi.set(t.id,{}),Pi.get(t.id).startLeft=m,P4(p,t.startLabelLeft)}if(t.startLabelRight){let m=e.insert("g").attr("class","edgeTerminals"),g=m.insert("g").attr("class","inner"),y=await sc(g,t.startLabelRight,$2(t.labelStyle)||"",!1,!1);p=y;let v=y.getBBox();if(n){let x=y.children[0],b=ut(y);v=x.getBoundingClientRect(),b.attr("width",v.width),b.attr("height",v.height)}g.attr("transform",By(v,n)),Pi.get(t.id)||Pi.set(t.id,{}),Pi.get(t.id).startRight=m,P4(p,t.startLabelRight)}if(t.endLabelLeft){let m=e.insert("g").attr("class","edgeTerminals"),g=m.insert("g").attr("class","inner"),y=await sc(m,t.endLabelLeft,$2(t.labelStyle)||"",!1,!1);p=y;let v=y.getBBox();if(n){let x=y.children[0],b=ut(y);v=x.getBoundingClientRect(),b.attr("width",v.width),b.attr("height",v.height)}g.attr("transform",By(v,n)),Pi.get(t.id)||Pi.set(t.id,{}),Pi.get(t.id).endLeft=m,P4(p,t.endLabelLeft)}if(t.endLabelRight){let m=e.insert("g").attr("class","edgeTerminals"),g=m.insert("g").attr("class","inner"),y=await sc(m,t.endLabelRight,$2(t.labelStyle)||"",!1,!1);p=y;let v=y.getBBox();if(n){let x=y.children[0],b=ut(y);v=x.getBoundingClientRect(),b.attr("width",v.width),b.attr("height",v.height)}g.attr("transform",By(v,n)),Pi.get(t.id)||Pi.set(t.id,{}),Pi.get(t.id).endRight=m,P4(p,t.endLabelRight)}return h},"insertEdgeLabel");s(P4,"setTerminalWidth");$y=s((e,t)=>{se.debug("Moving label abc88 ",e.id,e.label,_m.get(e.id),t);let r=t.updatedPath?t.updatedPath:t.originalPath,n=Pe(),{subGraphTitleTotalMargin:i}=kh(n);if(e.label){let a=_m.get(e.id),o=e.x,l=e.y;if(r){let u=or.calcLabelPosition(r);se.debug("Moving label "+e.label+" from (",o,",",l,") to (",u.x,",",u.y,") abc88"),t.updatedPath&&(o=u.x,l=u.y)}a.attr("transform",`translate(${o}, ${l+i/2})`)}if(e.startLabelLeft){let a=Pi.get(e.id).startLeft,o=e.x,l=e.y;if(r){let u=or.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_left",r);o=u.x,l=u.y}a.attr("transform",`translate(${o}, ${l})`)}if(e.startLabelRight){let a=Pi.get(e.id).startRight,o=e.x,l=e.y;if(r){let u=or.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_right",r);o=u.x,l=u.y}a.attr("transform",`translate(${o}, ${l})`)}if(e.endLabelLeft){let a=Pi.get(e.id).endLeft,o=e.x,l=e.y;if(r){let u=or.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_left",r);o=u.x,l=u.y}a.attr("transform",`translate(${o}, ${l})`)}if(e.endLabelRight){let a=Pi.get(e.id).endRight,o=e.x,l=e.y;if(r){let u=or.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_right",r);o=u.x,l=u.y}a.attr("transform",`translate(${o}, ${l})`)}},"positionEdgeLabel"),DZe=s((e,t)=>{if(!e?.isLabelEdge||!e?.id?.endsWith("-to-label")||!Array.isArray(t)||t.length!==2)return t;let[r,n]=t,i=Math.abs(n.x-r.x),a=Math.abs(n.y-r.y);return i<.001||a<.001?t:a>=i?[r,{x:r.x,y:n.y},n]:[r,{x:n.x,y:r.y},n]},"orthogonalizeToLabelClippedPoints"),IZe=s((e,t)=>{let r=e.x,n=e.y,i=Math.abs(t.x-r),a=Math.abs(t.y-n),o=e.width/2,l=e.height/2;return i>=o||a>=l},"outsideNode"),MZe=s((e,t,r)=>{se.debug(`intersection calc abc89: + outsidePoint: ${JSON.stringify(t)} + insidePoint : ${JSON.stringify(r)} + node : x:${e.x} y:${e.y} w:${e.width} h:${e.height}`);let n=e.x,i=e.y,a=Math.abs(n-r.x),o=e.width/2,l=r.xMath.abs(n-t.x)*u){let f=r.y{se.warn("abc88 cutPathAtIntersect",e,t);let r=[],n=e[0],i=!1;return e.forEach(a=>{if(se.info("abc88 checking point",a,t),!IZe(t,a)&&!i){let o=MZe(t,n,a);se.debug("abc88 inside",a,n,o),se.debug("abc88 intersection",o,t);let l=!1;r.forEach(u=>{l=l||u.x===o.x&&u.y===o.y}),r.some(u=>u.x===o.x&&u.y===o.y)?se.warn("abc88 no intersect",o,r):r.push(o),i=!0}else se.warn("abc88 outside",a,n),n=a,i||r.push(a)}),se.debug("returning points",r),r},"cutPathAtIntersect");s(NZe,"extractCornerPoints");Pue=s(function(e,t,r){let n=t.x-e.x,i=t.y-e.y,a=Math.sqrt(n*n+i*i),o=r/a;return{x:t.x-o*n,y:t.y-o*i}},"findAdjacentPoint"),PZe=s(function(e){let{cornerPointPositions:t}=NZe(e),r=[];for(let n=0;n10&&Math.abs(a.y-i.y)>=10){se.debug("Corner point fixing",Math.abs(a.x-i.x),Math.abs(a.y-i.y));let m=5;o.x===l.x?p={x:h<0?l.x-m+f:l.x+m-f,y:d<0?l.y-f:l.y+f}:p={x:h<0?l.x-f:l.x+f,y:d<0?l.y-m+f:l.y+m-f}}else se.debug("Corner point skipping fixing",Math.abs(a.x-i.x),Math.abs(a.y-i.y));r.push(p,u)}else r.push(e[n]);return r},"fixCorners"),OZe=s((e,t,r)=>{let n=e-t-r,i=2,a=2,o=i+a,l=Math.floor(n/o),u=Number.isFinite(l)?Math.max(0,l):0,h=Array(u).fill(`${i} ${a}`).join(" ");return`0 ${t} ${h} ${r}`},"generateDashArray"),of=s(function(e,t,r,n,i,a,o,l=!1){if(!o)throw new Error(`insertEdge: missing diagramId for edge "${t.id}" \u2014 edge IDs require a diagram prefix for uniqueness`);let{handDrawnSeed:u,layout:h}=Pe(),d=t.points,f=!1,p=i;var m=a;let g=[];for(let O in t.cssCompiledStyles)S2(O)||g.push(t.cssCompiledStyles[O]);if(h==="swimlane"){if(m.intersect&&p.intersect&&Array.isArray(d)&&d.length>=2)if(d.length===2)d=[p.intersect(d[0]),m.intersect(d[1])];else{let O=d.slice(1,-1),$=O[0],W=O[O.length-1],V=.5,z=Math.abs(d[d.length-1].x-W.x)!Number.isNaN(O.y)),x=LZe(t.curve);x!=="rounded"&&(v=PZe(v));let b=Zc;switch(x){case"linear":b=Zc;break;case"basis":b=bh;break;case"cardinal":b=Kb;break;case"bumpX":b=Hb;break;case"bumpY":b=Ub;break;case"catmullRom":b=Jb;break;case"monotoneX":b=e2;break;case"monotoneY":b=t2;break;case"natural":b=Ay;break;case"step":b=Ry;break;case"stepAfter":b=n2;break;case"stepBefore":b=r2;break;case"rounded":b=Zc;break;default:b=bh}let{x:T,y:w}=Lue(t),C=xh().x(T).y(w).curve(b),S;switch(t.thickness){case"normal":S="edge-thickness-normal";break;case"thick":S="edge-thickness-thick";break;case"invisible":S="edge-thickness-invisible";break;default:S="edge-thickness-normal"}switch(t.pattern){case"solid":S+=" edge-pattern-solid";break;case"dotted":S+=" edge-pattern-dotted";break;case"dashed":S+=" edge-pattern-dashed";break;default:S+=" edge-pattern-solid"}let k,R=x==="rounded"?BZe($Ze(v,t),5):C(v),M=Array.isArray(t.style)?t.style:[t.style],P=M.find(O=>O?.startsWith("stroke:")),N="";t.animate&&(N="edge-animation-fast"),t.animation&&(N="edge-animation-"+t.animation);let _=!1;if(t.look==="handDrawn"){let O=ot.svg(e);Object.assign([],v);let $=O.path(R,{roughness:.3,seed:u});S+=" transition",k=ut($).select("path").attr("id",`${o}-${t.id}`).attr("class"," "+S+(t.classes?" "+t.classes:"")+(N?" "+N:"")).attr("style",M?M.reduce((V,z)=>V+";"+z,""):"");let W=k.attr("d");k.attr("d",W),e.node().appendChild(k.node())}else{let O=g.join(";"),$=M?M.reduce((J,ne)=>J+ne+";",""):"",W=(O?O+";"+$+";":$)+";"+(M?M.reduce((J,ne)=>J+";"+ne,""):"");k=e.append("path").attr("d",R).attr("id",`${o}-${t.id}`).attr("class"," "+S+(t.classes?" "+t.classes:"")+(N?" "+N:"")).attr("style",W),P=W.match(/stroke:([^;]+)/)?.[1],_=t.animate===!0||!!t.animation||O.includes("animation");let V=k.node(),z=typeof V.getTotalLength=="function"?V.getTotalLength():0,H=vO[t.arrowTypeStart]||0,K=vO[t.arrowTypeEnd]||0;if(t.look==="neo"&&!_){let ne=`stroke-dasharray: ${t.pattern==="dotted"||t.pattern==="dashed"?OZe(z,H,K):`0 ${H} ${z-H-K} ${K}`}; stroke-dashoffset: 0;`;k.attr("style",ne+k.attr("style"))}}k.attr("data-edge",!0),k.attr("data-et","edge"),k.attr("data-id",t.id),k.attr("data-points",y),k.attr("data-look",un(t.look)),t.showPoints&&v.forEach(O=>{e.append("circle").style("stroke","red").style("fill","red").attr("r",1).attr("cx",O.x).attr("cy",O.y)});let A="";(Pe().flowchart.arrowMarkerAbsolute||Pe().state.arrowMarkerAbsolute)&&(A=window.location.protocol+"//"+window.location.host+window.location.pathname+window.location.search,A=A.replace(/\(/g,"\\(").replace(/\)/g,"\\)")),se.info("arrowTypeStart",t.arrowTypeStart),se.info("arrowTypeEnd",t.arrowTypeEnd);let I=!_&&t?.look==="neo";Iue(k,t,A,o,n,I,P);let D=Math.floor(d.length/2),L=d[D];or.isLabelCoordinateInPath(L,k.attr("d"))||(f=!0);let B={};return f&&(B.updatedPath=d),B.originalPath=t.points,B},"insertEdge");s(BZe,"generateRoundedPath");s(Oue,"calculateDeltaAndAngle");s($Ze,"applyMarkerOffsetsToPoints")});var FZe,GZe,zZe,VZe,WZe,qZe,HZe,UZe,YZe,jZe,XZe,KZe,ZZe,QZe,JZe,eQe,tQe,rQe,nQe,iQe,aQe,sQe,oQe,lQe,Gy,B4=F(()=>{"use strict";St();br();FZe=s((e,t,r,n)=>{t.forEach(i=>{lQe[i](e,r,n)})},"insertMarkers"),GZe=s((e,t,r)=>{se.trace("Making markers for ",r),e.append("defs").append("marker").attr("id",r+"_"+t+"-extensionStart").attr("class","marker extension "+t).attr("refX",18).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M 1,7 L18,13 V 1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-extensionEnd").attr("class","marker extension "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M 1,1 V 13 L18,7 Z"),e.append("marker").attr("id",r+"_"+t+"-extensionStart-margin").attr("class","marker extension "+t).attr("refX",18).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").attr("viewBox","0 0 20 14").append("polygon").attr("points","10,7 18,13 18,1").style("stroke-width",2).style("stroke-dasharray","0"),e.append("defs").append("marker").attr("id",r+"_"+t+"-extensionEnd-margin").attr("class","marker extension "+t).attr("refX",9).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").attr("viewBox","0 0 20 14").append("polygon").attr("points","10,1 10,13 18,7").style("stroke-width",2).style("stroke-dasharray","0")},"extension"),zZe=s((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionStart").attr("class","marker composition "+t).attr("refX",18).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionEnd").attr("class","marker composition "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionStart-margin").attr("class","marker composition "+t).attr("refX",15).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",0).attr("viewBox","0 0 15 15").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionEnd-margin").attr("class","marker composition "+t).attr("refX",3.5).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",0).attr("d","M 18,7 L9,13 L1,7 L9,1 Z")},"composition"),VZe=s((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationStart").attr("class","marker aggregation "+t).attr("refX",18).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationEnd").attr("class","marker aggregation "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationStart-margin").attr("class","marker aggregation "+t).attr("refX",15).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",2).attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationEnd-margin").attr("class","marker aggregation "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",2).attr("d","M 18,7 L9,13 L1,7 L9,1 Z")},"aggregation"),WZe=s((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyStart").attr("class","marker dependency "+t).attr("refX",6).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M 5,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyEnd").attr("class","marker dependency "+t).attr("refX",13).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M 18,7 L9,13 L14,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyStart-margin").attr("class","marker dependency "+t).attr("refX",4).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",0).attr("d","M 5,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyEnd-margin").attr("class","marker dependency "+t).attr("refX",16).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",0).attr("d","M 18,7 L9,13 L14,7 L9,1 Z")},"dependency"),qZe=s((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopStart").attr("class","marker lollipop "+t).attr("refX",13).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("circle").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6),e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopEnd").attr("class","marker lollipop "+t).attr("refX",1).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("circle").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6),e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopStart-margin").attr("class","marker lollipop "+t).attr("refX",13).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("circle").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6).attr("stroke-width",2),e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopEnd-margin").attr("class","marker lollipop "+t).attr("refX",1).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("circle").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6).attr("stroke-width",2)},"lollipop"),HZe=s((e,t,r)=>{e.append("marker").attr("id",r+"_"+t+"-pointEnd").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",5).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",8).attr("markerHeight",8).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-pointStart").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",4.5).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",8).attr("markerHeight",8).attr("orient","auto").append("path").attr("d","M 0 5 L 10 10 L 10 0 z").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-pointEnd-margin").attr("class","marker "+t).attr("viewBox","0 0 11.5 14").attr("refX",11.5).attr("refY",7).attr("markerUnits","userSpaceOnUse").attr("markerWidth",10.5).attr("markerHeight",14).attr("orient","auto").append("path").attr("d","M 0 0 L 11.5 7 L 0 14 z").attr("class","arrowMarkerPath").style("stroke-width",0).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-pointStart-margin").attr("class","marker "+t).attr("viewBox","0 0 11.5 14").attr("refX",1).attr("refY",7).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11.5).attr("markerHeight",14).attr("orient","auto").append("polygon").attr("points","0,7 11.5,14 11.5,0").attr("class","arrowMarkerPath").style("stroke-width",0).style("stroke-dasharray","1,0")},"point"),UZe=s((e,t,r)=>{e.append("marker").attr("id",r+"_"+t+"-circleEnd").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",11).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-circleStart").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",-1).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-circleEnd-margin").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refY",5).attr("refX",12.25).attr("markerUnits","userSpaceOnUse").attr("markerWidth",14).attr("markerHeight",14).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",0).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-circleStart-margin").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",-2).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",14).attr("markerHeight",14).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",0).style("stroke-dasharray","1,0")},"circle"),YZe=s((e,t,r)=>{e.append("marker").attr("id",r+"_"+t+"-crossEnd").attr("class","marker cross "+t).attr("viewBox","0 0 11 11").attr("refX",12).attr("refY",5.2).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("path").attr("d","M 1,1 l 9,9 M 10,1 l -9,9").attr("class","arrowMarkerPath").style("stroke-width",2).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-crossStart").attr("class","marker cross "+t).attr("viewBox","0 0 11 11").attr("refX",-1).attr("refY",5.2).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("path").attr("d","M 1,1 l 9,9 M 10,1 l -9,9").attr("class","arrowMarkerPath").style("stroke-width",2).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-crossEnd-margin").attr("class","marker cross "+t).attr("viewBox","0 0 15 15").attr("refX",17.7).attr("refY",7.5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto").append("path").attr("d","M 1,1 L 14,14 M 1,14 L 14,1").attr("class","arrowMarkerPath").style("stroke-width",2.5),e.append("marker").attr("id",r+"_"+t+"-crossStart-margin").attr("class","marker cross "+t).attr("viewBox","0 0 15 15").attr("refX",-3.5).attr("refY",7.5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto").append("path").attr("d","M 1,1 L 14,14 M 1,14 L 14,1").attr("class","arrowMarkerPath").style("stroke-width",2.5).style("stroke-dasharray","1,0")},"cross"),jZe=s((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-barbEnd").attr("refX",19).attr("refY",7).attr("markerWidth",20).attr("markerHeight",14).attr("markerUnits","userSpaceOnUse").attr("orient","auto").append("path").attr("d","M 19,7 L9,13 L14,7 L9,1 Z")},"barb"),XZe=s((e,t,r)=>{let n=Lt(),{themeVariables:i}=n,{transitionColor:a}=i;e.append("defs").append("marker").attr("id",r+"_"+t+"-barbEnd").attr("refX",19).attr("refY",7).attr("markerWidth",20).attr("markerHeight",14).attr("markerUnits","strokeWidth").attr("orient","auto").append("path").attr("d","M 19,7 L11,14 L13,7 L11,0 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-barbEnd-margin").attr("refX",17).attr("refY",7).attr("markerWidth",20).attr("markerHeight",14).attr("markerUnits","userSpaceOnUse").attr("orient","auto").append("path").attr("d","M 19,7 L11,14 L13,7 L11,0 Z").attr("fill",`${a}`)},"barbNeo"),KZe=s((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-onlyOneStart").attr("class","marker onlyOne "+t).attr("refX",0).attr("refY",9).attr("markerWidth",18).attr("markerHeight",18).attr("orient","auto").append("path").attr("d","M9,0 L9,18 M15,0 L15,18"),e.append("defs").append("marker").attr("id",r+"_"+t+"-onlyOneEnd").attr("class","marker onlyOne "+t).attr("refX",18).attr("refY",9).attr("markerWidth",18).attr("markerHeight",18).attr("orient","auto").append("path").attr("d","M3,0 L3,18 M9,0 L9,18")},"only_one"),ZZe=s((e,t,r)=>{let n=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrOneStart").attr("class","marker zeroOrOne "+t).attr("refX",0).attr("refY",9).attr("markerWidth",30).attr("markerHeight",18).attr("orient","auto");n.append("circle").attr("fill","white").attr("cx",21).attr("cy",9).attr("r",6),n.append("path").attr("d","M9,0 L9,18");let i=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrOneEnd").attr("class","marker zeroOrOne "+t).attr("refX",30).attr("refY",9).attr("markerWidth",30).attr("markerHeight",18).attr("orient","auto");i.append("circle").attr("fill","white").attr("cx",9).attr("cy",9).attr("r",6),i.append("path").attr("d","M21,0 L21,18")},"zero_or_one"),QZe=s((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-oneOrMoreStart").attr("class","marker oneOrMore "+t).attr("refX",18).attr("refY",18).attr("markerWidth",45).attr("markerHeight",36).attr("orient","auto").append("path").attr("d","M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27"),e.append("defs").append("marker").attr("id",r+"_"+t+"-oneOrMoreEnd").attr("class","marker oneOrMore "+t).attr("refX",27).attr("refY",18).attr("markerWidth",45).attr("markerHeight",36).attr("orient","auto").append("path").attr("d","M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18")},"one_or_more"),JZe=s((e,t,r)=>{let n=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrMoreStart").attr("class","marker zeroOrMore "+t).attr("refX",18).attr("refY",18).attr("markerWidth",57).attr("markerHeight",36).attr("orient","auto");n.append("circle").attr("fill","white").attr("cx",48).attr("cy",18).attr("r",6),n.append("path").attr("d","M0,18 Q18,0 36,18 Q18,36 0,18");let i=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrMoreEnd").attr("class","marker zeroOrMore "+t).attr("refX",39).attr("refY",18).attr("markerWidth",57).attr("markerHeight",36).attr("orient","auto");i.append("circle").attr("fill","white").attr("cx",9).attr("cy",18).attr("r",6),i.append("path").attr("d","M21,18 Q39,0 57,18 Q39,36 21,18")},"zero_or_more"),eQe=s((e,t,r)=>{let n=Lt(),{themeVariables:i}=n,{strokeWidth:a}=i;e.append("defs").append("marker").attr("id",r+"_"+t+"-onlyOneStart").attr("class","marker onlyOne "+t).attr("refX",0).attr("refY",9).attr("markerWidth",18).attr("markerHeight",18).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M9,0 L9,18 M15,0 L15,18").attr("stroke-width",`${a}`),e.append("defs").append("marker").attr("id",r+"_"+t+"-onlyOneEnd").attr("class","marker onlyOne "+t).attr("refX",18).attr("refY",9).attr("markerWidth",18).attr("markerHeight",18).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M3,0 L3,18 M9,0 L9,18").attr("stroke-width",`${a}`)},"only_one_neo"),tQe=s((e,t,r)=>{let n=Lt(),{themeVariables:i}=n,{strokeWidth:a,mainBkg:o}=i,l=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrOneStart").attr("class","marker zeroOrOne "+t).attr("refX",0).attr("refY",9).attr("markerWidth",30).attr("markerHeight",18).attr("orient","auto").attr("markerUnits","userSpaceOnUse");l.append("circle").attr("fill",o??"white").attr("cx",21).attr("cy",9).attr("stroke-width",`${a}`).attr("r",6),l.append("path").attr("d","M9,0 L9,18").attr("stroke-width",`${a}`);let u=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrOneEnd").attr("class","marker zeroOrOne "+t).attr("refX",30).attr("refY",9).attr("markerWidth",30).attr("markerHeight",18).attr("markerUnits","userSpaceOnUse").attr("orient","auto");u.append("circle").attr("fill",o??"white").attr("cx",9).attr("cy",9).attr("stroke-width",`${a}`).attr("r",6),u.append("path").attr("d","M21,0 L21,18").attr("stroke-width",`${a}`)},"zero_or_one_neo"),rQe=s((e,t,r)=>{let n=Lt(),{themeVariables:i}=n,{strokeWidth:a}=i;e.append("defs").append("marker").attr("id",r+"_"+t+"-oneOrMoreStart").attr("class","marker oneOrMore "+t).attr("refX",18).attr("refY",18).attr("markerWidth",45).attr("markerHeight",36).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27").attr("stroke-width",`${a}`),e.append("defs").append("marker").attr("id",r+"_"+t+"-oneOrMoreEnd").attr("class","marker oneOrMore "+t).attr("refX",27).attr("refY",18).attr("markerWidth",45).attr("markerHeight",36).attr("markerUnits","userSpaceOnUse").attr("orient","auto").append("path").attr("d","M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18").attr("stroke-width",`${a}`)},"one_or_more_neo"),nQe=s((e,t,r)=>{let n=Lt(),{themeVariables:i}=n,{strokeWidth:a,mainBkg:o}=i,l=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrMoreStart").attr("class","marker zeroOrMore "+t).attr("refX",18).attr("refY",18).attr("markerWidth",57).attr("markerHeight",36).attr("markerUnits","userSpaceOnUse").attr("orient","auto");l.append("circle").attr("fill",o??"white").attr("cx",45.5).attr("cy",18).attr("r",6).attr("stroke-width",`${a}`),l.append("path").attr("d","M0,18 Q18,0 36,18 Q18,36 0,18").attr("stroke-width",`${a}`);let u=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrMoreEnd").attr("class","marker zeroOrMore "+t).attr("refX",39).attr("refY",18).attr("markerWidth",57).attr("markerHeight",36).attr("orient","auto").attr("markerUnits","userSpaceOnUse");u.append("circle").attr("fill",o??"white").attr("cx",11).attr("cy",18).attr("r",6).attr("stroke-width",`${a}`),u.append("path").attr("d","M21,18 Q39,0 57,18 Q39,36 21,18").attr("stroke-width",`${a}`)},"zero_or_more_neo"),iQe=s((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-requirement_arrowEnd").attr("refX",20).attr("refY",10).attr("markerWidth",20).attr("markerHeight",20).attr("orient","auto").append("path").attr("d",`M0,0 + L20,10 + M20,10 + L0,20`)},"requirement_arrow"),aQe=s((e,t,r)=>{let n=Lt(),{themeVariables:i}=n,{strokeWidth:a}=i;e.append("defs").append("marker").attr("id",r+"_"+t+"-requirement_arrowEnd").attr("refX",20).attr("refY",10).attr("markerWidth",20).attr("markerHeight",20).attr("orient","auto").attr("markerUnits","userSpaceOnUse").attr("stroke-width",`${a}`).attr("viewBox","0 0 25 20").append("path").attr("d",`M0,0 + L20,10 + M20,10 + L0,20`).attr("stroke-linejoin","miter")},"requirement_arrow_neo"),sQe=s((e,t,r)=>{let n=e.append("defs").append("marker").attr("id",r+"_"+t+"-requirement_containsStart").attr("refX",0).attr("refY",10).attr("markerWidth",20).attr("markerHeight",20).attr("orient","auto").append("g");n.append("circle").attr("cx",10).attr("cy",10).attr("r",9).attr("fill","none"),n.append("line").attr("x1",1).attr("x2",19).attr("y1",10).attr("y2",10),n.append("line").attr("y1",1).attr("y2",19).attr("x1",10).attr("x2",10)},"requirement_contains"),oQe=s((e,t,r)=>{let n=Lt(),{themeVariables:i}=n,{strokeWidth:a}=i,o=e.append("defs").append("marker").attr("id",r+"_"+t+"-requirement_containsStart").attr("refX",0).attr("refY",10).attr("markerWidth",20).attr("markerHeight",20).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("g");o.append("circle").attr("cx",10).attr("cy",10).attr("r",9).attr("fill","none"),o.append("line").attr("x1",1).attr("x2",19).attr("y1",10).attr("y2",10),o.append("line").attr("y1",1).attr("y2",19).attr("x1",10).attr("x2",10),o.selectAll("*").attr("stroke-width",`${a}`)},"requirement_contains_neo"),lQe={extension:GZe,composition:zZe,aggregation:VZe,dependency:WZe,lollipop:qZe,point:HZe,circle:UZe,cross:YZe,barb:jZe,barbNeo:XZe,only_one:KZe,zero_or_one:ZZe,one_or_more:QZe,zero_or_more:JZe,only_one_neo:eQe,zero_or_one_neo:tQe,one_or_more_neo:rQe,zero_or_more_neo:nQe,requirement_arrow:iQe,requirement_contains:sQe,requirement_arrow_neo:aQe,requirement_contains_neo:oQe},Gy=FZe});async function Eh(e,t,r){let n,i;t.shape==="rect"&&(t.rx&&t.ry?t.shape="roundedRect":t.shape="squareRect");let a=t.shape?L2[t.shape]:void 0;if(!a)throw new Error(`No such shape: ${t.shape}. Please check your syntax.`);if(t.link){let o;r.config.securityLevel==="sandbox"?o="_top":t.linkTarget&&(o=t.linkTarget||"_blank"),n=e.insert("svg:a").attr("xlink:href",t.link).attr("target",o??null),i=await a(n,t,r)}else i=await a(e,t,r),n=i;return n.attr("data-look",un(t.look)),t.tooltip&&i.attr("title",t.tooltip),$4.set(t.id,n),t.haveCallback&&n.attr("class",n.attr("class")+" clickable"),n}var $4,$ue,Fue,au,Lm=F(()=>{"use strict";St();R4();er();$4=new Map;s(Eh,"insertNode");$ue=s((e,t)=>{$4.set(t.id,e)},"setNodeElem"),Fue=s(()=>{$4.clear()},"clear"),au=s(e=>{let t=$4.get(e.id);se.trace("Transforming node",e.diff,e,"translate("+(e.x-e.width/2-5)+", "+e.width/2+")");let r=8,n=e.diff||0;return e.clusterNode?t.attr("transform","translate("+(e.x+n-e.width/2)+", "+(e.y-e.height/2-r)+")"):t.attr("transform","translate("+e.x+", "+e.y+")"),n},"positionNode")});var Gue,zue=F(()=>{"use strict";br();Yr();St();B2();Fy();B4();Lm();Ht();er();Gue={common:Tt,getConfig:Lt,insertCluster:sf,insertEdge:of,insertEdgeLabel:oc,insertMarkers:Gy,insertNode:Eh,interpolateToCurve:qN,labelHelper:yt,log:se,positionEdgeLabel:$y}});var cQe,F4,bO=F(()=>{"use strict";cQe=typeof global=="object"&&global&&global.Object===Object&&global,F4=cQe});var uQe,hQe,Oi,bl=F(()=>{"use strict";bO();uQe=typeof self=="object"&&self&&self.Object===Object&&self,hQe=F4||uQe||Function("return this")(),Oi=hQe});var dQe,Pa,Dm=F(()=>{"use strict";bl();dQe=Oi.Symbol,Pa=dQe});function mQe(e){var t=fQe.call(e,F2),r=e[F2];try{e[F2]=void 0;var n=!0}catch{}var i=pQe.call(e);return n&&(t?e[F2]=r:delete e[F2]),i}var Vue,fQe,pQe,F2,Wue,que=F(()=>{"use strict";Dm();Vue=Object.prototype,fQe=Vue.hasOwnProperty,pQe=Vue.toString,F2=Pa?Pa.toStringTag:void 0;s(mQe,"getRawTag");Wue=mQe});function vQe(e){return yQe.call(e)}var gQe,yQe,Hue,Uue=F(()=>{"use strict";gQe=Object.prototype,yQe=gQe.toString;s(vQe,"objectToString");Hue=vQe});function TQe(e){return e==null?e===void 0?bQe:xQe:Yue&&Yue in Object(e)?Wue(e):Hue(e)}var xQe,bQe,Yue,Gs,lf=F(()=>{"use strict";Dm();que();Uue();xQe="[object Null]",bQe="[object Undefined]",Yue=Pa?Pa.toStringTag:void 0;s(TQe,"baseGetTag");Gs=TQe});function CQe(e){return e!=null&&typeof e=="object"}var qi,lc=F(()=>{"use strict";s(CQe,"isObjectLike");qi=CQe});function wQe(e){return typeof e=="symbol"||qi(e)&&Gs(e)==SQe}var SQe,Yo,Im=F(()=>{"use strict";lf();lc();SQe="[object Symbol]";s(wQe,"isSymbol");Yo=wQe});function kQe(e,t){for(var r=-1,n=e==null?0:e.length,i=Array(n);++r{"use strict";s(kQe,"arrayMap");su=kQe});var EQe,Zr,na=F(()=>{"use strict";EQe=Array.isArray,Zr=EQe});function Kue(e){if(typeof e=="string")return e;if(Zr(e))return su(e,Kue)+"";if(Yo(e))return Xue?Xue.call(e):"";var t=e+"";return t=="0"&&1/e==-AQe?"-0":t}var AQe,jue,Xue,Zue,Que=F(()=>{"use strict";Dm();G2();na();Im();AQe=1/0,jue=Pa?Pa.prototype:void 0,Xue=jue?jue.toString:void 0;s(Kue,"baseToString");Zue=Kue});function _Qe(e){for(var t=e.length;t--&&RQe.test(e.charAt(t)););return t}var RQe,Jue,ehe=F(()=>{"use strict";RQe=/\s/;s(_Qe,"trimmedEndIndex");Jue=_Qe});function DQe(e){return e&&e.slice(0,Jue(e)+1).replace(LQe,"")}var LQe,the,rhe=F(()=>{"use strict";ehe();LQe=/^\s+/;s(DQe,"baseTrim");the=DQe});function IQe(e){var t=typeof e;return e!=null&&(t=="object"||t=="function")}var yi,Tl=F(()=>{"use strict";s(IQe,"isObject");yi=IQe});function BQe(e){if(typeof e=="number")return e;if(Yo(e))return nhe;if(yi(e)){var t=typeof e.valueOf=="function"?e.valueOf():e;e=yi(t)?t+"":t}if(typeof e!="string")return e===0?e:+e;e=the(e);var r=NQe.test(e);return r||PQe.test(e)?OQe(e.slice(2),r?2:8):MQe.test(e)?nhe:+e}var nhe,MQe,NQe,PQe,OQe,ihe,ahe=F(()=>{"use strict";rhe();Tl();Im();nhe=NaN,MQe=/^[-+]0x[0-9a-f]+$/i,NQe=/^0b[01]+$/i,PQe=/^0o[0-7]+$/i,OQe=parseInt;s(BQe,"toNumber");ihe=BQe});function FQe(e){if(!e)return e===0?e:0;if(e=ihe(e),e===she||e===-she){var t=e<0?-1:1;return t*$Qe}return e===e?e:0}var she,$Qe,zy,TO=F(()=>{"use strict";ahe();she=1/0,$Qe=17976931348623157e292;s(FQe,"toFinite");zy=FQe});function GQe(e){var t=zy(e),r=t%1;return t===t?r?t-r:t:0}var ohe,lhe=F(()=>{"use strict";TO();s(GQe,"toInteger");ohe=GQe});function zQe(e){return e}var uo,cf=F(()=>{"use strict";s(zQe,"identity");uo=zQe});function UQe(e){if(!yi(e))return!1;var t=Gs(e);return t==WQe||t==qQe||t==VQe||t==HQe}var VQe,WQe,qQe,HQe,ou,z2=F(()=>{"use strict";lf();Tl();VQe="[object AsyncFunction]",WQe="[object Function]",qQe="[object GeneratorFunction]",HQe="[object Proxy]";s(UQe,"isFunction");ou=UQe});var YQe,G4,che=F(()=>{"use strict";bl();YQe=Oi["__core-js_shared__"],G4=YQe});function jQe(e){return!!uhe&&uhe in e}var uhe,hhe,dhe=F(()=>{"use strict";che();uhe=(function(){var e=/[^.]+$/.exec(G4&&G4.keys&&G4.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""})();s(jQe,"isMasked");hhe=jQe});function ZQe(e){if(e!=null){try{return KQe.call(e)}catch{}try{return e+""}catch{}}return""}var XQe,KQe,Ah,CO=F(()=>{"use strict";XQe=Function.prototype,KQe=XQe.toString;s(ZQe,"toSource");Ah=ZQe});function aJe(e){if(!yi(e)||hhe(e))return!1;var t=ou(e)?iJe:JQe;return t.test(Ah(e))}var QQe,JQe,eJe,tJe,rJe,nJe,iJe,fhe,phe=F(()=>{"use strict";z2();dhe();Tl();CO();QQe=/[\\^$.*+?()[\]{}|]/g,JQe=/^\[object .+?Constructor\]$/,eJe=Function.prototype,tJe=Object.prototype,rJe=eJe.toString,nJe=tJe.hasOwnProperty,iJe=RegExp("^"+rJe.call(nJe).replace(QQe,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");s(aJe,"baseIsNative");fhe=aJe});function sJe(e,t){return e?.[t]}var mhe,ghe=F(()=>{"use strict";s(sJe,"getValue");mhe=sJe});function oJe(e,t){var r=mhe(e,t);return fhe(r)?r:void 0}var ho,uf=F(()=>{"use strict";phe();ghe();s(oJe,"getNative");ho=oJe});var lJe,z4,yhe=F(()=>{"use strict";uf();bl();lJe=ho(Oi,"WeakMap"),z4=lJe});var vhe,cJe,xhe,bhe=F(()=>{"use strict";Tl();vhe=Object.create,cJe=(function(){function e(){}return s(e,"object"),function(t){if(!yi(t))return{};if(vhe)return vhe(t);e.prototype=t;var r=new e;return e.prototype=void 0,r}})(),xhe=cJe});function uJe(e,t,r){switch(r.length){case 0:return e.call(t);case 1:return e.call(t,r[0]);case 2:return e.call(t,r[0],r[1]);case 3:return e.call(t,r[0],r[1],r[2])}return e.apply(t,r)}var The,Che=F(()=>{"use strict";s(uJe,"apply");The=uJe});function hJe(){}var She,whe=F(()=>{"use strict";s(hJe,"noop");She=hJe});function dJe(e,t){var r=-1,n=e.length;for(t||(t=Array(n));++r{"use strict";s(dJe,"copyArray");V4=dJe});function gJe(e){var t=0,r=0;return function(){var n=mJe(),i=pJe-(n-r);if(r=n,i>0){if(++t>=fJe)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}var fJe,pJe,mJe,khe,Ehe=F(()=>{"use strict";fJe=800,pJe=16,mJe=Date.now;s(gJe,"shortOut");khe=gJe});function yJe(e){return function(){return e}}var fo,wO=F(()=>{"use strict";s(yJe,"constant");fo=yJe});var vJe,Vy,kO=F(()=>{"use strict";uf();vJe=(function(){try{var e=ho(Object,"defineProperty");return e({},"",{}),e}catch{}})(),Vy=vJe});var xJe,Ahe,Rhe=F(()=>{"use strict";wO();kO();cf();xJe=Vy?function(e,t){return Vy(e,"toString",{configurable:!0,enumerable:!1,value:fo(t),writable:!0})}:uo,Ahe=xJe});var bJe,W4,EO=F(()=>{"use strict";Rhe();Ehe();bJe=khe(Ahe),W4=bJe});function TJe(e,t){for(var r=-1,n=e==null?0:e.length;++r{"use strict";s(TJe,"arrayEach");q4=TJe});function CJe(e,t,r,n){for(var i=e.length,a=r+(n?1:-1);n?a--:++a{"use strict";s(CJe,"baseFindIndex");H4=CJe});function SJe(e){return e!==e}var _he,Lhe=F(()=>{"use strict";s(SJe,"baseIsNaN");_he=SJe});function wJe(e,t,r){for(var n=r-1,i=e.length;++n{"use strict";s(wJe,"strictIndexOf");Dhe=wJe});function kJe(e,t,r){return t===t?Dhe(e,t,r):H4(e,_he,r)}var Mhe,Nhe=F(()=>{"use strict";RO();Lhe();Ihe();s(kJe,"baseIndexOf");Mhe=kJe});function EJe(e,t){var r=e==null?0:e.length;return!!r&&Mhe(e,t,0)>-1}var Phe,Ohe=F(()=>{"use strict";Nhe();s(EJe,"arrayIncludes");Phe=EJe});function _Je(e,t){var r=typeof e;return t=t??AJe,!!t&&(r=="number"||r!="symbol"&&RJe.test(e))&&e>-1&&e%1==0&&e{"use strict";AJe=9007199254740991,RJe=/^(?:0|[1-9]\d*)$/;s(_Je,"isIndex");hf=_Je});function LJe(e,t,r){t=="__proto__"&&Vy?Vy(e,t,{configurable:!0,enumerable:!0,value:r,writable:!0}):e[t]=r}var df,W2=F(()=>{"use strict";kO();s(LJe,"baseAssignValue");df=LJe});function DJe(e,t){return e===t||e!==e&&t!==t}var Cl,Mm=F(()=>{"use strict";s(DJe,"eq");Cl=DJe});function NJe(e,t,r){var n=e[t];(!(MJe.call(e,t)&&Cl(n,r))||r===void 0&&!(t in e))&&df(e,t,r)}var IJe,MJe,ff,q2=F(()=>{"use strict";W2();Mm();IJe=Object.prototype,MJe=IJe.hasOwnProperty;s(NJe,"assignValue");ff=NJe});function PJe(e,t,r,n){var i=!r;r||(r={});for(var a=-1,o=t.length;++a{"use strict";q2();W2();s(PJe,"copyObject");lu=PJe});function OJe(e,t,r){return t=Bhe(t===void 0?e.length-1:t,0),function(){for(var n=arguments,i=-1,a=Bhe(n.length-t,0),o=Array(a);++i{"use strict";Che();Bhe=Math.max;s(OJe,"overRest");U4=OJe});function BJe(e,t){return W4(U4(e,t,uo),e+"")}var pf,H2=F(()=>{"use strict";cf();_O();EO();s(BJe,"baseRest");pf=BJe});function FJe(e){return typeof e=="number"&&e>-1&&e%1==0&&e<=$Je}var $Je,qy,Y4=F(()=>{"use strict";$Je=9007199254740991;s(FJe,"isLength");qy=FJe});function GJe(e){return e!=null&&qy(e.length)&&!ou(e)}var Oa,cu=F(()=>{"use strict";z2();Y4();s(GJe,"isArrayLike");Oa=GJe});function zJe(e,t,r){if(!yi(r))return!1;var n=typeof t;return(n=="number"?Oa(r)&&hf(t,r.length):n=="string"&&t in r)?Cl(r[t],e):!1}var Rh,U2=F(()=>{"use strict";Mm();cu();V2();Tl();s(zJe,"isIterateeCall");Rh=zJe});function VJe(e){return pf(function(t,r){var n=-1,i=r.length,a=i>1?r[i-1]:void 0,o=i>2?r[2]:void 0;for(a=e.length>3&&typeof a=="function"?(i--,a):void 0,o&&Rh(r[0],r[1],o)&&(a=i<3?void 0:a,i=1),t=Object(t);++n{"use strict";H2();U2();s(VJe,"createAssigner");$he=VJe});function qJe(e){var t=e&&e.constructor,r=typeof t=="function"&&t.prototype||WJe;return e===r}var WJe,mf,Y2=F(()=>{"use strict";WJe=Object.prototype;s(qJe,"isPrototype");mf=qJe});function HJe(e,t){for(var r=-1,n=Array(e);++r{"use strict";s(HJe,"baseTimes");Ghe=HJe});function YJe(e){return qi(e)&&Gs(e)==UJe}var UJe,LO,Vhe=F(()=>{"use strict";lf();lc();UJe="[object Arguments]";s(YJe,"baseIsArguments");LO=YJe});var Whe,jJe,XJe,KJe,cc,Hy=F(()=>{"use strict";Vhe();lc();Whe=Object.prototype,jJe=Whe.hasOwnProperty,XJe=Whe.propertyIsEnumerable,KJe=LO((function(){return arguments})())?LO:function(e){return qi(e)&&jJe.call(e,"callee")&&!XJe.call(e,"callee")},cc=KJe});function ZJe(){return!1}var qhe,Hhe=F(()=>{"use strict";s(ZJe,"stubFalse");qhe=ZJe});var jhe,Uhe,QJe,Yhe,JJe,eet,uc,Uy=F(()=>{"use strict";bl();Hhe();jhe=typeof exports=="object"&&exports&&!exports.nodeType&&exports,Uhe=jhe&&typeof module=="object"&&module&&!module.nodeType&&module,QJe=Uhe&&Uhe.exports===jhe,Yhe=QJe?Oi.Buffer:void 0,JJe=Yhe?Yhe.isBuffer:void 0,eet=JJe||qhe,uc=eet});function Aet(e){return qi(e)&&qy(e.length)&&!!ci[Gs(e)]}var tet,ret,net,iet,aet,set,oet,cet,uet,het,det,fet,pet,met,get,yet,vet,xet,bet,Tet,Cet,wet,ket,Eet,ci,Xhe,Khe=F(()=>{"use strict";lf();Y4();lc();tet="[object Arguments]",ret="[object Array]",net="[object Boolean]",iet="[object Date]",aet="[object Error]",set="[object Function]",oet="[object Map]",cet="[object Number]",uet="[object Object]",het="[object RegExp]",det="[object Set]",fet="[object String]",pet="[object WeakMap]",met="[object ArrayBuffer]",get="[object DataView]",yet="[object Float32Array]",vet="[object Float64Array]",xet="[object Int8Array]",bet="[object Int16Array]",Tet="[object Int32Array]",Cet="[object Uint8Array]",wet="[object Uint8ClampedArray]",ket="[object Uint16Array]",Eet="[object Uint32Array]",ci={};ci[yet]=ci[vet]=ci[xet]=ci[bet]=ci[Tet]=ci[Cet]=ci[wet]=ci[ket]=ci[Eet]=!0;ci[tet]=ci[ret]=ci[met]=ci[net]=ci[get]=ci[iet]=ci[aet]=ci[set]=ci[oet]=ci[cet]=ci[uet]=ci[het]=ci[det]=ci[fet]=ci[pet]=!1;s(Aet,"baseIsTypedArray");Xhe=Aet});function Ret(e){return function(t){return e(t)}}var gf,j2=F(()=>{"use strict";s(Ret,"baseUnary");gf=Ret});var Zhe,X2,_et,DO,Let,_h,j4=F(()=>{"use strict";bO();Zhe=typeof exports=="object"&&exports&&!exports.nodeType&&exports,X2=Zhe&&typeof module=="object"&&module&&!module.nodeType&&module,_et=X2&&X2.exports===Zhe,DO=_et&&F4.process,Let=(function(){try{var e=X2&&X2.require&&X2.require("util").types;return e||DO&&DO.binding&&DO.binding("util")}catch{}})(),_h=Let});var Qhe,Det,yf,K2=F(()=>{"use strict";Khe();j2();j4();Qhe=_h&&_h.isTypedArray,Det=Qhe?gf(Qhe):Xhe,yf=Det});function Net(e,t){var r=Zr(e),n=!r&&cc(e),i=!r&&!n&&uc(e),a=!r&&!n&&!i&&yf(e),o=r||n||i||a,l=o?Ghe(e.length,String):[],u=l.length;for(var h in e)(t||Met.call(e,h))&&!(o&&(h=="length"||i&&(h=="offset"||h=="parent")||a&&(h=="buffer"||h=="byteLength"||h=="byteOffset")||hf(h,u)))&&l.push(h);return l}var Iet,Met,X4,IO=F(()=>{"use strict";zhe();Hy();na();Uy();V2();K2();Iet=Object.prototype,Met=Iet.hasOwnProperty;s(Net,"arrayLikeKeys");X4=Net});function Pet(e,t){return function(r){return e(t(r))}}var K4,MO=F(()=>{"use strict";s(Pet,"overArg");K4=Pet});var Oet,Jhe,ede=F(()=>{"use strict";MO();Oet=K4(Object.keys,Object),Jhe=Oet});function Fet(e){if(!mf(e))return Jhe(e);var t=[];for(var r in Object(e))$et.call(e,r)&&r!="constructor"&&t.push(r);return t}var Bet,$et,Yy,Z4=F(()=>{"use strict";Y2();ede();Bet=Object.prototype,$et=Bet.hasOwnProperty;s(Fet,"baseKeys");Yy=Fet});function Get(e){return Oa(e)?X4(e):Yy(e)}var Bi,Lh=F(()=>{"use strict";IO();Z4();cu();s(Get,"keys");Bi=Get});function zet(e){var t=[];if(e!=null)for(var r in Object(e))t.push(r);return t}var tde,rde=F(()=>{"use strict";s(zet,"nativeKeysIn");tde=zet});function qet(e){if(!yi(e))return tde(e);var t=mf(e),r=[];for(var n in e)n=="constructor"&&(t||!Wet.call(e,n))||r.push(n);return r}var Vet,Wet,nde,ide=F(()=>{"use strict";Tl();Y2();rde();Vet=Object.prototype,Wet=Vet.hasOwnProperty;s(qet,"baseKeysIn");nde=qet});function Het(e){return Oa(e)?X4(e,!0):nde(e)}var po,vf=F(()=>{"use strict";IO();ide();cu();s(Het,"keysIn");po=Het});function jet(e,t){if(Zr(e))return!1;var r=typeof e;return r=="number"||r=="symbol"||r=="boolean"||e==null||Yo(e)?!0:Yet.test(e)||!Uet.test(e)||t!=null&&e in Object(t)}var Uet,Yet,jy,Q4=F(()=>{"use strict";na();Im();Uet=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Yet=/^\w*$/;s(jet,"isKey");jy=jet});var Xet,Dh,Z2=F(()=>{"use strict";uf();Xet=ho(Object,"create"),Dh=Xet});function Ket(){this.__data__=Dh?Dh(null):{},this.size=0}var ade,sde=F(()=>{"use strict";Z2();s(Ket,"hashClear");ade=Ket});function Zet(e){var t=this.has(e)&&delete this.__data__[e];return this.size-=t?1:0,t}var ode,lde=F(()=>{"use strict";s(Zet,"hashDelete");ode=Zet});function ttt(e){var t=this.__data__;if(Dh){var r=t[e];return r===Qet?void 0:r}return ett.call(t,e)?t[e]:void 0}var Qet,Jet,ett,cde,ude=F(()=>{"use strict";Z2();Qet="__lodash_hash_undefined__",Jet=Object.prototype,ett=Jet.hasOwnProperty;s(ttt,"hashGet");cde=ttt});function itt(e){var t=this.__data__;return Dh?t[e]!==void 0:ntt.call(t,e)}var rtt,ntt,hde,dde=F(()=>{"use strict";Z2();rtt=Object.prototype,ntt=rtt.hasOwnProperty;s(itt,"hashHas");hde=itt});function stt(e,t){var r=this.__data__;return this.size+=this.has(e)?0:1,r[e]=Dh&&t===void 0?att:t,this}var att,fde,pde=F(()=>{"use strict";Z2();att="__lodash_hash_undefined__";s(stt,"hashSet");fde=stt});function Xy(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t{"use strict";sde();lde();ude();dde();pde();s(Xy,"Hash");Xy.prototype.clear=ade;Xy.prototype.delete=ode;Xy.prototype.get=cde;Xy.prototype.has=hde;Xy.prototype.set=fde;NO=Xy});function ott(){this.__data__=[],this.size=0}var gde,yde=F(()=>{"use strict";s(ott,"listCacheClear");gde=ott});function ltt(e,t){for(var r=e.length;r--;)if(Cl(e[r][0],t))return r;return-1}var xf,Q2=F(()=>{"use strict";Mm();s(ltt,"assocIndexOf");xf=ltt});function htt(e){var t=this.__data__,r=xf(t,e);if(r<0)return!1;var n=t.length-1;return r==n?t.pop():utt.call(t,r,1),--this.size,!0}var ctt,utt,vde,xde=F(()=>{"use strict";Q2();ctt=Array.prototype,utt=ctt.splice;s(htt,"listCacheDelete");vde=htt});function dtt(e){var t=this.__data__,r=xf(t,e);return r<0?void 0:t[r][1]}var bde,Tde=F(()=>{"use strict";Q2();s(dtt,"listCacheGet");bde=dtt});function ftt(e){return xf(this.__data__,e)>-1}var Cde,Sde=F(()=>{"use strict";Q2();s(ftt,"listCacheHas");Cde=ftt});function ptt(e,t){var r=this.__data__,n=xf(r,e);return n<0?(++this.size,r.push([e,t])):r[n][1]=t,this}var wde,kde=F(()=>{"use strict";Q2();s(ptt,"listCacheSet");wde=ptt});function Ky(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t{"use strict";yde();xde();Tde();Sde();kde();s(Ky,"ListCache");Ky.prototype.clear=gde;Ky.prototype.delete=vde;Ky.prototype.get=bde;Ky.prototype.has=Cde;Ky.prototype.set=wde;bf=Ky});var mtt,Tf,J4=F(()=>{"use strict";uf();bl();mtt=ho(Oi,"Map"),Tf=mtt});function gtt(){this.size=0,this.__data__={hash:new NO,map:new(Tf||bf),string:new NO}}var Ede,Ade=F(()=>{"use strict";mde();J2();J4();s(gtt,"mapCacheClear");Ede=gtt});function ytt(e){var t=typeof e;return t=="string"||t=="number"||t=="symbol"||t=="boolean"?e!=="__proto__":e===null}var Rde,_de=F(()=>{"use strict";s(ytt,"isKeyable");Rde=ytt});function vtt(e,t){var r=e.__data__;return Rde(t)?r[typeof t=="string"?"string":"hash"]:r.map}var Cf,eT=F(()=>{"use strict";_de();s(vtt,"getMapData");Cf=vtt});function xtt(e){var t=Cf(this,e).delete(e);return this.size-=t?1:0,t}var Lde,Dde=F(()=>{"use strict";eT();s(xtt,"mapCacheDelete");Lde=xtt});function btt(e){return Cf(this,e).get(e)}var Ide,Mde=F(()=>{"use strict";eT();s(btt,"mapCacheGet");Ide=btt});function Ttt(e){return Cf(this,e).has(e)}var Nde,Pde=F(()=>{"use strict";eT();s(Ttt,"mapCacheHas");Nde=Ttt});function Ctt(e,t){var r=Cf(this,e),n=r.size;return r.set(e,t),this.size+=r.size==n?0:1,this}var Ode,Bde=F(()=>{"use strict";eT();s(Ctt,"mapCacheSet");Ode=Ctt});function Zy(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t{"use strict";Ade();Dde();Mde();Pde();Bde();s(Zy,"MapCache");Zy.prototype.clear=Ede;Zy.prototype.delete=Lde;Zy.prototype.get=Ide;Zy.prototype.has=Nde;Zy.prototype.set=Ode;Nm=Zy});function PO(e,t){if(typeof e!="function"||t!=null&&typeof t!="function")throw new TypeError(Stt);var r=s(function(){var n=arguments,i=t?t.apply(this,n):n[0],a=r.cache;if(a.has(i))return a.get(i);var o=e.apply(this,n);return r.cache=a.set(i,o)||a,o},"memoized");return r.cache=new(PO.Cache||Nm),r}var Stt,$de,Fde=F(()=>{"use strict";e3();Stt="Expected a function";s(PO,"memoize");PO.Cache=Nm;$de=PO});function ktt(e){var t=$de(e,function(n){return r.size===wtt&&r.clear(),n}),r=t.cache;return t}var wtt,Gde,zde=F(()=>{"use strict";Fde();wtt=500;s(ktt,"memoizeCapped");Gde=ktt});var Ett,Att,Rtt,Vde,Wde=F(()=>{"use strict";zde();Ett=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Att=/\\(\\)?/g,Rtt=Gde(function(e){var t=[];return e.charCodeAt(0)===46&&t.push(""),e.replace(Ett,function(r,n,i,a){t.push(i?a.replace(Att,"$1"):n||r)}),t}),Vde=Rtt});function _tt(e){return e==null?"":Zue(e)}var t3,OO=F(()=>{"use strict";Que();s(_tt,"toString");t3=_tt});function Ltt(e,t){return Zr(e)?e:jy(e,t)?[e]:Vde(t3(e))}var Sf,tT=F(()=>{"use strict";na();Q4();Wde();OO();s(Ltt,"castPath");Sf=Ltt});function Itt(e){if(typeof e=="string"||Yo(e))return e;var t=e+"";return t=="0"&&1/e==-Dtt?"-0":t}var Dtt,uu,Qy=F(()=>{"use strict";Im();Dtt=1/0;s(Itt,"toKey");uu=Itt});function Mtt(e,t){t=Sf(t,e);for(var r=0,n=t.length;e!=null&&r{"use strict";tT();Qy();s(Mtt,"baseGet");wf=Mtt});function Ntt(e,t,r){var n=e==null?void 0:wf(e,t);return n===void 0?r:n}var qde,Hde=F(()=>{"use strict";rT();s(Ntt,"get");qde=Ntt});function Ptt(e,t){for(var r=-1,n=t.length,i=e.length;++r{"use strict";s(Ptt,"arrayPush");Jy=Ptt});function Ott(e){return Zr(e)||cc(e)||!!(Ude&&e&&e[Ude])}var Ude,Yde,jde=F(()=>{"use strict";Dm();Hy();na();Ude=Pa?Pa.isConcatSpreadable:void 0;s(Ott,"isFlattenable");Yde=Ott});function Xde(e,t,r,n,i){var a=-1,o=e.length;for(r||(r=Yde),i||(i=[]);++a0&&r(l)?t>1?Xde(l,t-1,r,n,i):Jy(i,l):n||(i[i.length]=l)}return i}var e1,n3=F(()=>{"use strict";r3();jde();s(Xde,"baseFlatten");e1=Xde});function Btt(e){var t=e==null?0:e.length;return t?e1(e,1):[]}var Sl,BO=F(()=>{"use strict";n3();s(Btt,"flatten");Sl=Btt});function $tt(e){return W4(U4(e,void 0,Sl),e+"")}var Kde,Zde=F(()=>{"use strict";BO();_O();EO();s($tt,"flatRest");Kde=$tt});var Ftt,t1,i3=F(()=>{"use strict";MO();Ftt=K4(Object.getPrototypeOf,Object),t1=Ftt});function Htt(e){if(!qi(e)||Gs(e)!=Gtt)return!1;var t=t1(e);if(t===null)return!0;var r=Wtt.call(t,"constructor")&&t.constructor;return typeof r=="function"&&r instanceof r&&Qde.call(r)==qtt}var Gtt,ztt,Vtt,Qde,Wtt,qtt,Jde,efe=F(()=>{"use strict";lf();i3();lc();Gtt="[object Object]",ztt=Function.prototype,Vtt=Object.prototype,Qde=ztt.toString,Wtt=Vtt.hasOwnProperty,qtt=Qde.call(Object);s(Htt,"isPlainObject");Jde=Htt});function ert(e){return Jtt.test(e)}var Utt,Ytt,jtt,Xtt,Ktt,Ztt,Qtt,Jtt,tfe,rfe=F(()=>{"use strict";Utt="\\ud800-\\udfff",Ytt="\\u0300-\\u036f",jtt="\\ufe20-\\ufe2f",Xtt="\\u20d0-\\u20ff",Ktt=Ytt+jtt+Xtt,Ztt="\\ufe0e\\ufe0f",Qtt="\\u200d",Jtt=RegExp("["+Qtt+Utt+Ktt+Ztt+"]");s(ert,"hasUnicode");tfe=ert});function trt(e,t,r,n){var i=-1,a=e==null?0:e.length;for(n&&a&&(r=e[++i]);++i{"use strict";s(trt,"arrayReduce");nfe=trt});function rrt(){this.__data__=new bf,this.size=0}var afe,sfe=F(()=>{"use strict";J2();s(rrt,"stackClear");afe=rrt});function nrt(e){var t=this.__data__,r=t.delete(e);return this.size=t.size,r}var ofe,lfe=F(()=>{"use strict";s(nrt,"stackDelete");ofe=nrt});function irt(e){return this.__data__.get(e)}var cfe,ufe=F(()=>{"use strict";s(irt,"stackGet");cfe=irt});function art(e){return this.__data__.has(e)}var hfe,dfe=F(()=>{"use strict";s(art,"stackHas");hfe=art});function ort(e,t){var r=this.__data__;if(r instanceof bf){var n=r.__data__;if(!Tf||n.length{"use strict";J2();J4();e3();srt=200;s(ort,"stackSet");ffe=ort});function r1(e){var t=this.__data__=new bf(e);this.size=t.size}var hu,nT=F(()=>{"use strict";J2();sfe();lfe();ufe();dfe();pfe();s(r1,"Stack");r1.prototype.clear=afe;r1.prototype.delete=ofe;r1.prototype.get=cfe;r1.prototype.has=hfe;r1.prototype.set=ffe;hu=r1});function lrt(e,t){return e&&lu(t,Bi(t),e)}var mfe,gfe=F(()=>{"use strict";Wy();Lh();s(lrt,"baseAssign");mfe=lrt});function crt(e,t){return e&&lu(t,po(t),e)}var yfe,vfe=F(()=>{"use strict";Wy();vf();s(crt,"baseAssignIn");yfe=crt});function hrt(e,t){if(t)return e.slice();var r=e.length,n=Tfe?Tfe(r):new e.constructor(r);return e.copy(n),n}var Cfe,xfe,urt,bfe,Tfe,a3,$O=F(()=>{"use strict";bl();Cfe=typeof exports=="object"&&exports&&!exports.nodeType&&exports,xfe=Cfe&&typeof module=="object"&&module&&!module.nodeType&&module,urt=xfe&&xfe.exports===Cfe,bfe=urt?Oi.Buffer:void 0,Tfe=bfe?bfe.allocUnsafe:void 0;s(hrt,"cloneBuffer");a3=hrt});function drt(e,t){for(var r=-1,n=e==null?0:e.length,i=0,a=[];++r{"use strict";s(drt,"arrayFilter");s3=drt});function frt(){return[]}var o3,GO=F(()=>{"use strict";s(frt,"stubArray");o3=frt});var prt,mrt,Sfe,grt,n1,l3=F(()=>{"use strict";FO();GO();prt=Object.prototype,mrt=prt.propertyIsEnumerable,Sfe=Object.getOwnPropertySymbols,grt=Sfe?function(e){return e==null?[]:(e=Object(e),s3(Sfe(e),function(t){return mrt.call(e,t)}))}:o3,n1=grt});function yrt(e,t){return lu(e,n1(e),t)}var wfe,kfe=F(()=>{"use strict";Wy();l3();s(yrt,"copySymbols");wfe=yrt});var vrt,xrt,c3,zO=F(()=>{"use strict";r3();i3();l3();GO();vrt=Object.getOwnPropertySymbols,xrt=vrt?function(e){for(var t=[];e;)Jy(t,n1(e)),e=t1(e);return t}:o3,c3=xrt});function brt(e,t){return lu(e,c3(e),t)}var Efe,Afe=F(()=>{"use strict";Wy();zO();s(brt,"copySymbolsIn");Efe=brt});function Trt(e,t,r){var n=t(e);return Zr(e)?n:Jy(n,r(e))}var u3,VO=F(()=>{"use strict";r3();na();s(Trt,"baseGetAllKeys");u3=Trt});function Crt(e){return u3(e,Bi,n1)}var iT,WO=F(()=>{"use strict";VO();l3();Lh();s(Crt,"getAllKeys");iT=Crt});function Srt(e){return u3(e,po,c3)}var Rfe,_fe=F(()=>{"use strict";VO();zO();vf();s(Srt,"getAllKeysIn");Rfe=Srt});var wrt,h3,Lfe=F(()=>{"use strict";uf();bl();wrt=ho(Oi,"DataView"),h3=wrt});var krt,d3,Dfe=F(()=>{"use strict";uf();bl();krt=ho(Oi,"Promise"),d3=krt});var Ert,kf,qO=F(()=>{"use strict";uf();bl();Ert=ho(Oi,"Set"),kf=Ert});var Ife,Art,Mfe,Nfe,Pfe,Ofe,Rrt,_rt,Lrt,Drt,Irt,Pm,jo,Om=F(()=>{"use strict";Lfe();J4();Dfe();qO();yhe();lf();CO();Ife="[object Map]",Art="[object Object]",Mfe="[object Promise]",Nfe="[object Set]",Pfe="[object WeakMap]",Ofe="[object DataView]",Rrt=Ah(h3),_rt=Ah(Tf),Lrt=Ah(d3),Drt=Ah(kf),Irt=Ah(z4),Pm=Gs;(h3&&Pm(new h3(new ArrayBuffer(1)))!=Ofe||Tf&&Pm(new Tf)!=Ife||d3&&Pm(d3.resolve())!=Mfe||kf&&Pm(new kf)!=Nfe||z4&&Pm(new z4)!=Pfe)&&(Pm=s(function(e){var t=Gs(e),r=t==Art?e.constructor:void 0,n=r?Ah(r):"";if(n)switch(n){case Rrt:return Ofe;case _rt:return Ife;case Lrt:return Mfe;case Drt:return Nfe;case Irt:return Pfe}return t},"getTag"));jo=Pm});function Prt(e){var t=e.length,r=new e.constructor(t);return t&&typeof e[0]=="string"&&Nrt.call(e,"index")&&(r.index=e.index,r.input=e.input),r}var Mrt,Nrt,Bfe,$fe=F(()=>{"use strict";Mrt=Object.prototype,Nrt=Mrt.hasOwnProperty;s(Prt,"initCloneArray");Bfe=Prt});var Ort,i1,HO=F(()=>{"use strict";bl();Ort=Oi.Uint8Array,i1=Ort});function Brt(e){var t=new e.constructor(e.byteLength);return new i1(t).set(new i1(e)),t}var a1,f3=F(()=>{"use strict";HO();s(Brt,"cloneArrayBuffer");a1=Brt});function $rt(e,t){var r=t?a1(e.buffer):e.buffer;return new e.constructor(r,e.byteOffset,e.byteLength)}var Ffe,Gfe=F(()=>{"use strict";f3();s($rt,"cloneDataView");Ffe=$rt});function Grt(e){var t=new e.constructor(e.source,Frt.exec(e));return t.lastIndex=e.lastIndex,t}var Frt,zfe,Vfe=F(()=>{"use strict";Frt=/\w*$/;s(Grt,"cloneRegExp");zfe=Grt});function zrt(e){return qfe?Object(qfe.call(e)):{}}var Wfe,qfe,Hfe,Ufe=F(()=>{"use strict";Dm();Wfe=Pa?Pa.prototype:void 0,qfe=Wfe?Wfe.valueOf:void 0;s(zrt,"cloneSymbol");Hfe=zrt});function Vrt(e,t){var r=t?a1(e.buffer):e.buffer;return new e.constructor(r,e.byteOffset,e.length)}var p3,UO=F(()=>{"use strict";f3();s(Vrt,"cloneTypedArray");p3=Vrt});function lnt(e,t,r){var n=e.constructor;switch(t){case Zrt:return a1(e);case Wrt:case qrt:return new n(+e);case Qrt:return Ffe(e,r);case Jrt:case ent:case tnt:case rnt:case nnt:case int:case ant:case snt:case ont:return p3(e,r);case Hrt:return new n;case Urt:case Xrt:return new n(e);case Yrt:return zfe(e);case jrt:return new n;case Krt:return Hfe(e)}}var Wrt,qrt,Hrt,Urt,Yrt,jrt,Xrt,Krt,Zrt,Qrt,Jrt,ent,tnt,rnt,nnt,int,ant,snt,ont,Yfe,jfe=F(()=>{"use strict";f3();Gfe();Vfe();Ufe();UO();Wrt="[object Boolean]",qrt="[object Date]",Hrt="[object Map]",Urt="[object Number]",Yrt="[object RegExp]",jrt="[object Set]",Xrt="[object String]",Krt="[object Symbol]",Zrt="[object ArrayBuffer]",Qrt="[object DataView]",Jrt="[object Float32Array]",ent="[object Float64Array]",tnt="[object Int8Array]",rnt="[object Int16Array]",nnt="[object Int32Array]",int="[object Uint8Array]",ant="[object Uint8ClampedArray]",snt="[object Uint16Array]",ont="[object Uint32Array]";s(lnt,"initCloneByTag");Yfe=lnt});function cnt(e){return typeof e.constructor=="function"&&!mf(e)?xhe(t1(e)):{}}var m3,YO=F(()=>{"use strict";bhe();i3();Y2();s(cnt,"initCloneObject");m3=cnt});function hnt(e){return qi(e)&&jo(e)==unt}var unt,Xfe,Kfe=F(()=>{"use strict";Om();lc();unt="[object Map]";s(hnt,"baseIsMap");Xfe=hnt});var Zfe,dnt,Qfe,Jfe=F(()=>{"use strict";Kfe();j2();j4();Zfe=_h&&_h.isMap,dnt=Zfe?gf(Zfe):Xfe,Qfe=dnt});function pnt(e){return qi(e)&&jo(e)==fnt}var fnt,epe,tpe=F(()=>{"use strict";Om();lc();fnt="[object Set]";s(pnt,"baseIsSet");epe=pnt});var rpe,mnt,npe,ipe=F(()=>{"use strict";tpe();j2();j4();rpe=_h&&_h.isSet,mnt=rpe?gf(rpe):epe,npe=mnt});function g3(e,t,r,n,i,a){var o,l=t&gnt,u=t&ynt,h=t&vnt;if(r&&(o=i?r(e,n,i,a):r(e)),o!==void 0)return o;if(!yi(e))return e;var d=Zr(e);if(d){if(o=Bfe(e),!l)return V4(e,o)}else{var f=jo(e),p=f==spe||f==Snt;if(uc(e))return a3(e,l);if(f==ope||f==ape||p&&!i){if(o=u||p?{}:m3(e),!l)return u?Efe(e,yfe(o,e)):wfe(e,mfe(o,e))}else{if(!Zn[f])return i?e:{};o=Yfe(e,f,l)}}a||(a=new hu);var m=a.get(e);if(m)return m;a.set(e,o),npe(e)?e.forEach(function(v){o.add(g3(v,t,r,v,e,a))}):Qfe(e)&&e.forEach(function(v,x){o.set(x,g3(v,t,r,x,e,a))});var g=h?u?Rfe:iT:u?po:Bi,y=d?void 0:g(e);return q4(y||e,function(v,x){y&&(x=v,v=e[x]),ff(o,x,g3(v,t,r,x,e,a))}),o}var gnt,ynt,vnt,ape,xnt,bnt,Tnt,Cnt,spe,Snt,wnt,knt,ope,Ent,Ant,Rnt,_nt,Lnt,Dnt,Int,Mnt,Nnt,Pnt,Ont,Bnt,$nt,Fnt,Gnt,znt,Zn,lpe,cpe=F(()=>{"use strict";nT();AO();q2();gfe();vfe();$O();SO();kfe();Afe();WO();_fe();Om();$fe();jfe();YO();na();Uy();Jfe();Tl();ipe();Lh();vf();gnt=1,ynt=2,vnt=4,ape="[object Arguments]",xnt="[object Array]",bnt="[object Boolean]",Tnt="[object Date]",Cnt="[object Error]",spe="[object Function]",Snt="[object GeneratorFunction]",wnt="[object Map]",knt="[object Number]",ope="[object Object]",Ent="[object RegExp]",Ant="[object Set]",Rnt="[object String]",_nt="[object Symbol]",Lnt="[object WeakMap]",Dnt="[object ArrayBuffer]",Int="[object DataView]",Mnt="[object Float32Array]",Nnt="[object Float64Array]",Pnt="[object Int8Array]",Ont="[object Int16Array]",Bnt="[object Int32Array]",$nt="[object Uint8Array]",Fnt="[object Uint8ClampedArray]",Gnt="[object Uint16Array]",znt="[object Uint32Array]",Zn={};Zn[ape]=Zn[xnt]=Zn[Dnt]=Zn[Int]=Zn[bnt]=Zn[Tnt]=Zn[Mnt]=Zn[Nnt]=Zn[Pnt]=Zn[Ont]=Zn[Bnt]=Zn[wnt]=Zn[knt]=Zn[ope]=Zn[Ent]=Zn[Ant]=Zn[Rnt]=Zn[_nt]=Zn[$nt]=Zn[Fnt]=Zn[Gnt]=Zn[znt]=!0;Zn[Cnt]=Zn[spe]=Zn[Lnt]=!1;s(g3,"baseClone");lpe=g3});function qnt(e){return lpe(e,Vnt|Wnt)}var Vnt,Wnt,jO,upe=F(()=>{"use strict";cpe();Vnt=1,Wnt=4;s(qnt,"cloneDeep");jO=qnt});function Unt(e){return this.__data__.set(e,Hnt),this}var Hnt,hpe,dpe=F(()=>{"use strict";Hnt="__lodash_hash_undefined__";s(Unt,"setCacheAdd");hpe=Unt});function Ynt(e){return this.__data__.has(e)}var fpe,ppe=F(()=>{"use strict";s(Ynt,"setCacheHas");fpe=Ynt});function y3(e){var t=-1,r=e==null?0:e.length;for(this.__data__=new Nm;++t{"use strict";e3();dpe();ppe();s(y3,"SetCache");y3.prototype.add=y3.prototype.push=hpe;y3.prototype.has=fpe;v3=y3});function jnt(e,t){for(var r=-1,n=e==null?0:e.length;++r{"use strict";s(jnt,"arraySome");mpe=jnt});function Xnt(e,t){return e.has(t)}var x3,KO=F(()=>{"use strict";s(Xnt,"cacheHas");x3=Xnt});function Qnt(e,t,r,n,i,a){var o=r&Knt,l=e.length,u=t.length;if(l!=u&&!(o&&u>l))return!1;var h=a.get(e),d=a.get(t);if(h&&d)return h==t&&d==e;var f=-1,p=!0,m=r&Znt?new v3:void 0;for(a.set(e,t),a.set(t,e);++f{"use strict";XO();gpe();KO();Knt=1,Znt=2;s(Qnt,"equalArrays");b3=Qnt});function Jnt(e){var t=-1,r=Array(e.size);return e.forEach(function(n,i){r[++t]=[i,n]}),r}var ype,vpe=F(()=>{"use strict";s(Jnt,"mapToArray");ype=Jnt});function eit(e){var t=-1,r=Array(e.size);return e.forEach(function(n){r[++t]=n}),r}var s1,T3=F(()=>{"use strict";s(eit,"setToArray");s1=eit});function pit(e,t,r,n,i,a,o){switch(r){case fit:if(e.byteLength!=t.byteLength||e.byteOffset!=t.byteOffset)return!1;e=e.buffer,t=t.buffer;case dit:return!(e.byteLength!=t.byteLength||!a(new i1(e),new i1(t)));case nit:case iit:case oit:return Cl(+e,+t);case ait:return e.name==t.name&&e.message==t.message;case lit:case uit:return e==t+"";case sit:var l=ype;case cit:var u=n&tit;if(l||(l=s1),e.size!=t.size&&!u)return!1;var h=o.get(e);if(h)return h==t;n|=rit,o.set(e,t);var d=b3(l(e),l(t),n,i,a,o);return o.delete(e),d;case hit:if(QO)return QO.call(e)==QO.call(t)}return!1}var tit,rit,nit,iit,ait,sit,oit,lit,cit,uit,hit,dit,fit,xpe,QO,bpe,Tpe=F(()=>{"use strict";Dm();HO();Mm();ZO();vpe();T3();tit=1,rit=2,nit="[object Boolean]",iit="[object Date]",ait="[object Error]",sit="[object Map]",oit="[object Number]",lit="[object RegExp]",cit="[object Set]",uit="[object String]",hit="[object Symbol]",dit="[object ArrayBuffer]",fit="[object DataView]",xpe=Pa?Pa.prototype:void 0,QO=xpe?xpe.valueOf:void 0;s(pit,"equalByTag");bpe=pit});function vit(e,t,r,n,i,a){var o=r&mit,l=iT(e),u=l.length,h=iT(t),d=h.length;if(u!=d&&!o)return!1;for(var f=u;f--;){var p=l[f];if(!(o?p in t:yit.call(t,p)))return!1}var m=a.get(e),g=a.get(t);if(m&&g)return m==t&&g==e;var y=!0;a.set(e,t),a.set(t,e);for(var v=o;++f{"use strict";WO();mit=1,git=Object.prototype,yit=git.hasOwnProperty;s(vit,"equalObjects");Cpe=vit});function Tit(e,t,r,n,i,a){var o=Zr(e),l=Zr(t),u=o?kpe:jo(e),h=l?kpe:jo(t);u=u==wpe?C3:u,h=h==wpe?C3:h;var d=u==C3,f=h==C3,p=u==h;if(p&&uc(e)){if(!uc(t))return!1;o=!0,d=!1}if(p&&!d)return a||(a=new hu),o||yf(e)?b3(e,t,r,n,i,a):bpe(e,t,u,r,n,i,a);if(!(r&xit)){var m=d&&Epe.call(e,"__wrapped__"),g=f&&Epe.call(t,"__wrapped__");if(m||g){var y=m?e.value():e,v=g?t.value():t;return a||(a=new hu),i(y,v,r,n,a)}}return p?(a||(a=new hu),Cpe(e,t,r,n,i,a)):!1}var xit,wpe,kpe,C3,bit,Epe,Ape,Rpe=F(()=>{"use strict";nT();ZO();Tpe();Spe();Om();na();Uy();K2();xit=1,wpe="[object Arguments]",kpe="[object Array]",C3="[object Object]",bit=Object.prototype,Epe=bit.hasOwnProperty;s(Tit,"baseIsEqualDeep");Ape=Tit});function _pe(e,t,r,n,i){return e===t?!0:e==null||t==null||!qi(e)&&!qi(t)?e!==e&&t!==t:Ape(e,t,r,n,_pe,i)}var S3,JO=F(()=>{"use strict";Rpe();lc();s(_pe,"baseIsEqual");S3=_pe});function wit(e,t,r,n){var i=r.length,a=i,o=!n;if(e==null)return!a;for(e=Object(e);i--;){var l=r[i];if(o&&l[2]?l[1]!==e[l[0]]:!(l[0]in e))return!1}for(;++i{"use strict";nT();JO();Cit=1,Sit=2;s(wit,"baseIsMatch");Lpe=wit});function kit(e){return e===e&&!yi(e)}var w3,e9=F(()=>{"use strict";Tl();s(kit,"isStrictComparable");w3=kit});function Eit(e){for(var t=Bi(e),r=t.length;r--;){var n=t[r],i=e[n];t[r]=[n,i,w3(i)]}return t}var Ipe,Mpe=F(()=>{"use strict";e9();Lh();s(Eit,"getMatchData");Ipe=Eit});function Ait(e,t){return function(r){return r==null?!1:r[e]===t&&(t!==void 0||e in Object(r))}}var k3,t9=F(()=>{"use strict";s(Ait,"matchesStrictComparable");k3=Ait});function Rit(e){var t=Ipe(e);return t.length==1&&t[0][2]?k3(t[0][0],t[0][1]):function(r){return r===e||Lpe(r,e,t)}}var Npe,Ppe=F(()=>{"use strict";Dpe();Mpe();t9();s(Rit,"baseMatches");Npe=Rit});function _it(e,t){return e!=null&&t in Object(e)}var Ope,Bpe=F(()=>{"use strict";s(_it,"baseHasIn");Ope=_it});function Lit(e,t,r){t=Sf(t,e);for(var n=-1,i=t.length,a=!1;++n{"use strict";tT();Hy();na();V2();Y4();Qy();s(Lit,"hasPath");E3=Lit});function Dit(e,t){return e!=null&&E3(e,t,Ope)}var A3,n9=F(()=>{"use strict";Bpe();r9();s(Dit,"hasIn");A3=Dit});function Nit(e,t){return jy(e)&&w3(t)?k3(uu(e),t):function(r){var n=qde(r,e);return n===void 0&&n===t?A3(r,e):S3(t,n,Iit|Mit)}}var Iit,Mit,$pe,Fpe=F(()=>{"use strict";JO();Hde();n9();Q4();e9();t9();Qy();Iit=1,Mit=2;s(Nit,"baseMatchesProperty");$pe=Nit});function Pit(e){return function(t){return t?.[e]}}var R3,i9=F(()=>{"use strict";s(Pit,"baseProperty");R3=Pit});function Oit(e){return function(t){return wf(t,e)}}var Gpe,zpe=F(()=>{"use strict";rT();s(Oit,"basePropertyDeep");Gpe=Oit});function Bit(e){return jy(e)?R3(uu(e)):Gpe(e)}var Vpe,Wpe=F(()=>{"use strict";i9();zpe();Q4();Qy();s(Bit,"property");Vpe=Bit});function $it(e){return typeof e=="function"?e:e==null?uo:typeof e=="object"?Zr(e)?$pe(e[0],e[1]):Npe(e):Vpe(e)}var vs,Ih=F(()=>{"use strict";Ppe();Fpe();cf();na();Wpe();s($it,"baseIteratee");vs=$it});function Fit(e){return function(t,r,n){for(var i=-1,a=Object(t),o=n(t),l=o.length;l--;){var u=o[e?l:++i];if(r(a[u],u,a)===!1)break}return t}}var qpe,Hpe=F(()=>{"use strict";s(Fit,"createBaseFor");qpe=Fit});var Git,o1,_3=F(()=>{"use strict";Hpe();Git=qpe(),o1=Git});function zit(e,t){return e&&o1(e,t,Bi)}var l1,L3=F(()=>{"use strict";_3();Lh();s(zit,"baseForOwn");l1=zit});function Vit(e,t){return function(r,n){if(r==null)return r;if(!Oa(r))return e(r,n);for(var i=r.length,a=t?i:-1,o=Object(r);(t?a--:++a{"use strict";cu();s(Vit,"createBaseEach");Upe=Vit});var Wit,Ef,aT=F(()=>{"use strict";L3();Ype();Wit=Upe(l1),Ef=Wit});var qit,D3,jpe=F(()=>{"use strict";bl();qit=s(function(){return Oi.Date.now()},"now"),D3=qit});var Xpe,Hit,Uit,a9,Kpe=F(()=>{"use strict";H2();Mm();U2();vf();Xpe=Object.prototype,Hit=Xpe.hasOwnProperty,Uit=pf(function(e,t){e=Object(e);var r=-1,n=t.length,i=n>2?t[2]:void 0;for(i&&Rh(t[0],t[1],i)&&(n=1);++r{"use strict";W2();Mm();s(Yit,"assignMergeValue");sT=Yit});function jit(e){return qi(e)&&Oa(e)}var I3,o9=F(()=>{"use strict";cu();lc();s(jit,"isArrayLikeObject");I3=jit});function Xit(e,t){if(!(t==="constructor"&&typeof e[t]=="function")&&t!="__proto__")return e[t]}var oT,l9=F(()=>{"use strict";s(Xit,"safeGet");oT=Xit});function Kit(e){return lu(e,po(e))}var Zpe,Qpe=F(()=>{"use strict";Wy();vf();s(Kit,"toPlainObject");Zpe=Kit});function Zit(e,t,r,n,i,a,o){var l=oT(e,r),u=oT(t,r),h=o.get(u);if(h){sT(e,r,h);return}var d=a?a(l,u,r+"",e,t,o):void 0,f=d===void 0;if(f){var p=Zr(u),m=!p&&uc(u),g=!p&&!m&&yf(u);d=u,p||m||g?Zr(l)?d=l:I3(l)?d=V4(l):m?(f=!1,d=a3(u,!0)):g?(f=!1,d=p3(u,!0)):d=[]:Jde(u)||cc(u)?(d=l,cc(l)?d=Zpe(l):(!yi(l)||ou(l))&&(d=m3(u))):f=!1}f&&(o.set(u,d),i(d,u,n,a,o),o.delete(u)),sT(e,r,d)}var Jpe,eme=F(()=>{"use strict";s9();$O();UO();SO();YO();Hy();na();o9();Uy();z2();Tl();efe();K2();l9();Qpe();s(Zit,"baseMergeDeep");Jpe=Zit});function tme(e,t,r,n,i){e!==t&&o1(t,function(a,o){if(i||(i=new hu),yi(a))Jpe(e,t,o,r,tme,n,i);else{var l=n?n(oT(e,o),a,o+"",e,t,i):void 0;l===void 0&&(l=a),sT(e,o,l)}},po)}var rme,nme=F(()=>{"use strict";nT();s9();_3();eme();Tl();vf();l9();s(tme,"baseMerge");rme=tme});function Qit(e,t,r){for(var n=-1,i=e==null?0:e.length;++n{"use strict";s(Qit,"arrayIncludesWith");ime=Qit});function Jit(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}var Af,sme=F(()=>{"use strict";s(Jit,"last");Af=Jit});function eat(e){return typeof e=="function"?e:uo}var c1,M3=F(()=>{"use strict";cf();s(eat,"castFunction");c1=eat});function tat(e,t){var r=Zr(e)?q4:Ef;return r(e,c1(t))}var at,c9=F(()=>{"use strict";AO();aT();M3();na();s(tat,"forEach");at=tat});var ome=F(()=>{"use strict";c9()});function rat(e,t){var r=[];return Ef(e,function(n,i,a){t(n,i,a)&&r.push(n)}),r}var lme,cme=F(()=>{"use strict";aT();s(rat,"baseFilter");lme=rat});function nat(e,t){var r=Zr(e)?s3:lme;return r(e,vs(t,3))}var zs,ume=F(()=>{"use strict";FO();cme();Ih();na();s(nat,"filter");zs=nat});function iat(e){return function(t,r,n){var i=Object(t);if(!Oa(t)){var a=vs(r,3);t=Bi(t),r=s(function(l){return a(i[l],l,i)},"predicate")}var o=e(t,r,n);return o>-1?i[a?t[o]:o]:void 0}}var hme,dme=F(()=>{"use strict";Ih();cu();Lh();s(iat,"createFind");hme=iat});function sat(e,t,r){var n=e==null?0:e.length;if(!n)return-1;var i=r==null?0:ohe(r);return i<0&&(i=aat(n+i,0)),H4(e,vs(t,3),i)}var aat,fme,pme=F(()=>{"use strict";RO();Ih();lhe();aat=Math.max;s(sat,"findIndex");fme=sat});var oat,u1,mme=F(()=>{"use strict";dme();pme();oat=hme(fme),u1=oat});function lat(e,t){var r=-1,n=Oa(e)?Array(e.length):[];return Ef(e,function(i,a,o){n[++r]=t(i,a,o)}),n}var N3,u9=F(()=>{"use strict";aT();cu();s(lat,"baseMap");N3=lat});function cat(e,t){var r=Zr(e)?su:N3;return r(e,vs(t,3))}var Fn,gme=F(()=>{"use strict";G2();Ih();u9();na();s(cat,"map");Fn=cat});function uat(e,t){return e==null?e:o1(e,c1(t),po)}var h9,yme=F(()=>{"use strict";_3();M3();vf();s(uat,"forIn");h9=uat});function hat(e,t){return e&&l1(e,c1(t))}var d9,vme=F(()=>{"use strict";L3();M3();s(hat,"forOwn");d9=hat});function dat(e,t){return e>t}var xme,bme=F(()=>{"use strict";s(dat,"baseGt");xme=dat});function mat(e,t){return e!=null&&pat.call(e,t)}var fat,pat,Tme,Cme=F(()=>{"use strict";fat=Object.prototype,pat=fat.hasOwnProperty;s(mat,"baseHas");Tme=mat});function gat(e,t){return e!=null&&E3(e,t,Tme)}var lT,Sme=F(()=>{"use strict";Cme();r9();s(gat,"has");lT=gat});function vat(e){return typeof e=="string"||!Zr(e)&&qi(e)&&Gs(e)==yat}var yat,wme,kme=F(()=>{"use strict";lf();na();lc();yat="[object String]";s(vat,"isString");wme=vat});function xat(e,t){return su(t,function(r){return e[r]})}var Eme,Ame=F(()=>{"use strict";G2();s(xat,"baseValues");Eme=xat});function bat(e){return e==null?[]:Eme(e,Bi(e))}var Xo,Rme=F(()=>{"use strict";Ame();Lh();s(bat,"values");Xo=bat});function kat(e){if(e==null)return!0;if(Oa(e)&&(Zr(e)||typeof e=="string"||typeof e.splice=="function"||uc(e)||yf(e)||cc(e)))return!e.length;var t=jo(e);if(t==Tat||t==Cat)return!e.size;if(mf(e))return!Yy(e).length;for(var r in e)if(wat.call(e,r))return!1;return!0}var Tat,Cat,Sat,wat,P3,_me=F(()=>{"use strict";Z4();Om();Hy();na();cu();Uy();Y2();K2();Tat="[object Map]",Cat="[object Set]",Sat=Object.prototype,wat=Sat.hasOwnProperty;s(kat,"isEmpty");P3=kat});function Eat(e){return e===void 0}var $i,Lme=F(()=>{"use strict";s(Eat,"isUndefined");$i=Eat});function Aat(e,t){return e{"use strict";s(Aat,"baseLt");O3=Aat});function Rat(e,t){var r={};return t=vs(t,3),l1(e,function(n,i,a){df(r,i,t(n,i,a))}),r}var Bm,Dme=F(()=>{"use strict";W2();L3();Ih();s(Rat,"mapValues");Bm=Rat});function _at(e,t,r){for(var n=-1,i=e.length;++n{"use strict";Im();s(_at,"baseExtremum");h1=_at});function Lat(e){return e&&e.length?h1(e,uo,xme):void 0}var mo,Ime=F(()=>{"use strict";B3();bme();cf();s(Lat,"max");mo=Lat});var Dat,d1,Mme=F(()=>{"use strict";nme();Fhe();Dat=$he(function(e,t,r){rme(e,t,r)}),d1=Dat});function Iat(e){return e&&e.length?h1(e,uo,O3):void 0}var Mh,Nme=F(()=>{"use strict";B3();f9();cf();s(Iat,"min");Mh=Iat});function Mat(e,t){return e&&e.length?h1(e,vs(t,2),O3):void 0}var $m,Pme=F(()=>{"use strict";B3();Ih();f9();s(Mat,"minBy");$m=Mat});function Nat(e,t,r,n){if(!yi(e))return e;t=Sf(t,e);for(var i=-1,a=t.length,o=a-1,l=e;l!=null&&++i{"use strict";q2();tT();V2();Tl();Qy();s(Nat,"baseSet");Ome=Nat});function Pat(e,t,r){for(var n=-1,i=t.length,a={};++n{"use strict";rT();Bme();tT();s(Pat,"basePickBy");$me=Pat});function Oat(e,t){var r=e.length;for(e.sort(t);r--;)e[r]=e[r].value;return e}var Gme,zme=F(()=>{"use strict";s(Oat,"baseSortBy");Gme=Oat});function Bat(e,t){if(e!==t){var r=e!==void 0,n=e===null,i=e===e,a=Yo(e),o=t!==void 0,l=t===null,u=t===t,h=Yo(t);if(!l&&!h&&!a&&e>t||a&&o&&u&&!l&&!h||n&&o&&u||!r&&u||!i)return 1;if(!n&&!a&&!h&&e{"use strict";Im();s(Bat,"compareAscending");Vme=Bat});function $at(e,t,r){for(var n=-1,i=e.criteria,a=t.criteria,o=i.length,l=r.length;++n=l)return u;var h=r[n];return u*(h=="desc"?-1:1)}}return e.index-t.index}var qme,Hme=F(()=>{"use strict";Wme();s($at,"compareMultiple");qme=$at});function Fat(e,t,r){t.length?t=su(t,function(a){return Zr(a)?function(o){return wf(o,a.length===1?a[0]:a)}:a}):t=[uo];var n=-1;t=su(t,gf(vs));var i=N3(e,function(a,o,l){var u=su(t,function(h){return h(a)});return{criteria:u,index:++n,value:a}});return Gme(i,function(a,o){return qme(a,o,r)})}var Ume,Yme=F(()=>{"use strict";G2();rT();Ih();u9();zme();j2();Hme();cf();na();s(Fat,"baseOrderBy");Ume=Fat});var Gat,jme,Xme=F(()=>{"use strict";i9();Gat=R3("length"),jme=Gat});function Qat(e){for(var t=Kme.lastIndex=0;Kme.test(e);)++t;return t}var Zme,zat,Vat,Wat,qat,Hat,Uat,p9,m9,Yat,Qme,Jme,ege,jat,tge,rge,Xat,Kat,Zat,Kme,nge,ige=F(()=>{"use strict";Zme="\\ud800-\\udfff",zat="\\u0300-\\u036f",Vat="\\ufe20-\\ufe2f",Wat="\\u20d0-\\u20ff",qat=zat+Vat+Wat,Hat="\\ufe0e\\ufe0f",Uat="["+Zme+"]",p9="["+qat+"]",m9="\\ud83c[\\udffb-\\udfff]",Yat="(?:"+p9+"|"+m9+")",Qme="[^"+Zme+"]",Jme="(?:\\ud83c[\\udde6-\\uddff]){2}",ege="[\\ud800-\\udbff][\\udc00-\\udfff]",jat="\\u200d",tge=Yat+"?",rge="["+Hat+"]?",Xat="(?:"+jat+"(?:"+[Qme,Jme,ege].join("|")+")"+rge+tge+")*",Kat=rge+tge+Xat,Zat="(?:"+[Qme+p9+"?",p9,Jme,ege,Uat].join("|")+")",Kme=RegExp(m9+"(?="+m9+")|"+Zat+Kat,"g");s(Qat,"unicodeSize");nge=Qat});function Jat(e){return tfe(e)?nge(e):jme(e)}var age,sge=F(()=>{"use strict";Xme();rfe();ige();s(Jat,"stringSize");age=Jat});function est(e,t){return $me(e,t,function(r,n){return A3(e,n)})}var oge,lge=F(()=>{"use strict";Fme();n9();s(est,"basePick");oge=est});var tst,Fm,cge=F(()=>{"use strict";lge();Zde();tst=Kde(function(e,t){return e==null?{}:oge(e,t)}),Fm=tst});function ist(e,t,r,n){for(var i=-1,a=nst(rst((t-e)/(r||1)),0),o=Array(a);a--;)o[n?a:++i]=e,e+=r;return o}var rst,nst,uge,hge=F(()=>{"use strict";rst=Math.ceil,nst=Math.max;s(ist,"baseRange");uge=ist});function ast(e){return function(t,r,n){return n&&typeof n!="number"&&Rh(t,r,n)&&(r=n=void 0),t=zy(t),r===void 0?(r=t,t=0):r=zy(r),n=n===void 0?t{"use strict";hge();U2();TO();s(ast,"createRange");dge=ast});var sst,wl,pge=F(()=>{"use strict";fge();sst=dge(),wl=sst});function ost(e,t,r,n,i){return i(e,function(a,o,l){r=n?(n=!1,a):t(r,a,o,l)}),r}var mge,gge=F(()=>{"use strict";s(ost,"baseReduce");mge=ost});function lst(e,t,r){var n=Zr(e)?nfe:mge,i=arguments.length<3;return n(e,vs(t,4),r,i,Ef)}var du,yge=F(()=>{"use strict";ife();aT();Ih();gge();na();s(lst,"reduce");du=lst});function hst(e){if(e==null)return 0;if(Oa(e))return wme(e)?age(e):e.length;var t=jo(e);return t==cst||t==ust?e.size:Yy(e).length}var cst,ust,g9,vge=F(()=>{"use strict";Z4();Om();cu();kme();sge();cst="[object Map]",ust="[object Set]";s(hst,"size");g9=hst});var dst,fu,xge=F(()=>{"use strict";n3();Yme();H2();U2();dst=pf(function(e,t){if(e==null)return[];var r=t.length;return r>1&&Rh(e,t[0],t[1])?t=[]:r>2&&Rh(t[0],t[1],t[2])&&(t=[t[0]]),Ume(e,e1(t,1),[])}),fu=dst});var fst,pst,bge,Tge=F(()=>{"use strict";qO();whe();T3();fst=1/0,pst=kf&&1/s1(new kf([,-0]))[1]==fst?function(e){return new kf(e)}:She,bge=pst});function gst(e,t,r){var n=-1,i=Phe,a=e.length,o=!0,l=[],u=l;if(r)o=!1,i=ime;else if(a>=mst){var h=t?null:bge(e);if(h)return s1(h);o=!1,i=x3,u=new v3}else u=t?[]:l;e:for(;++n{"use strict";XO();Ohe();ame();KO();Tge();T3();mst=200;s(gst,"baseUniq");Cge=gst});var yst,y9,wge=F(()=>{"use strict";n3();H2();Sge();o9();yst=pf(function(e){return Cge(e1(e,1,I3,!0))}),y9=yst});function xst(e){var t=++vst;return t3(e)+t}var vst,Gm,kge=F(()=>{"use strict";OO();vst=0;s(xst,"uniqueId");Gm=xst});function bst(e,t,r){for(var n=-1,i=e.length,a=t.length,o={};++n{"use strict";s(bst,"baseZipObject");Ege=bst});function Tst(e,t){return Ege(e||[],t||[],ff)}var $3,Rge=F(()=>{"use strict";q2();Age();s(Tst,"zipObject");$3=Tst});var Gn=F(()=>{"use strict";upe();wO();Kpe();ome();ume();mme();BO();c9();yme();vme();Sme();na();_me();z2();Lme();Lh();sme();gme();Dme();Ime();Mme();Nme();Pme();jpe();cge();pge();yge();vge();xge();wge();kge();Rme();Rge();});function Lge(e,t){e[t]?e[t]++:e[t]=1}function Dge(e,t){--e[t]||delete e[t]}function cT(e,t,r,n){var i=""+t,a=""+r;if(!e&&i>a){var o=i;i=a,a=o}return i+_ge+a+_ge+($i(n)?Cst:n)}function Sst(e,t,r,n){var i=""+t,a=""+r;if(!e&&i>a){var o=i;i=a,a=o}var l={v:i,w:a};return n&&(l.name=n),l}function v9(e,t){return cT(e,t.v,t.w,t.name)}var Cst,zm,_ge,Ln,x9=F(()=>{"use strict";Gn();Cst="\0",zm="\0",_ge="",Ln=class{static{s(this,"Graph")}constructor(t={}){this._isDirected=Object.prototype.hasOwnProperty.call(t,"directed")?t.directed:!0,this._isMultigraph=Object.prototype.hasOwnProperty.call(t,"multigraph")?t.multigraph:!1,this._isCompound=Object.prototype.hasOwnProperty.call(t,"compound")?t.compound:!1,this._label=void 0,this._defaultNodeLabelFn=fo(void 0),this._defaultEdgeLabelFn=fo(void 0),this._nodes={},this._isCompound&&(this._parent={},this._children={},this._children[zm]={}),this._in={},this._preds={},this._out={},this._sucs={},this._edgeObjs={},this._edgeLabels={}}isDirected(){return this._isDirected}isMultigraph(){return this._isMultigraph}isCompound(){return this._isCompound}setGraph(t){return this._label=t,this}graph(){return this._label}setDefaultNodeLabel(t){return ou(t)||(t=fo(t)),this._defaultNodeLabelFn=t,this}nodeCount(){return this._nodeCount}nodes(){return Bi(this._nodes)}sources(){var t=this;return zs(this.nodes(),function(r){return P3(t._in[r])})}sinks(){var t=this;return zs(this.nodes(),function(r){return P3(t._out[r])})}setNodes(t,r){var n=arguments,i=this;return at(t,function(a){n.length>1?i.setNode(a,r):i.setNode(a)}),this}setNode(t,r){return Object.prototype.hasOwnProperty.call(this._nodes,t)?(arguments.length>1&&(this._nodes[t]=r),this):(this._nodes[t]=arguments.length>1?r:this._defaultNodeLabelFn(t),this._isCompound&&(this._parent[t]=zm,this._children[t]={},this._children[zm][t]=!0),this._in[t]={},this._preds[t]={},this._out[t]={},this._sucs[t]={},++this._nodeCount,this)}node(t){return this._nodes[t]}hasNode(t){return Object.prototype.hasOwnProperty.call(this._nodes,t)}removeNode(t){if(Object.prototype.hasOwnProperty.call(this._nodes,t)){var r=s(n=>this.removeEdge(this._edgeObjs[n]),"removeEdge");delete this._nodes[t],this._isCompound&&(this._removeFromParentsChildList(t),delete this._parent[t],at(this.children(t),n=>{this.setParent(n)}),delete this._children[t]),at(Bi(this._in[t]),r),delete this._in[t],delete this._preds[t],at(Bi(this._out[t]),r),delete this._out[t],delete this._sucs[t],--this._nodeCount}return this}setParent(t,r){if(!this._isCompound)throw new Error("Cannot set parent in a non-compound graph");if($i(r))r=zm;else{r+="";for(var n=r;!$i(n);n=this.parent(n))if(n===t)throw new Error("Setting "+r+" as parent of "+t+" would create a cycle");this.setNode(r)}return this.setNode(t),this._removeFromParentsChildList(t),this._parent[t]=r,this._children[r][t]=!0,this}_removeFromParentsChildList(t){delete this._children[this._parent[t]][t]}parent(t){if(this._isCompound){var r=this._parent[t];if(r!==zm)return r}}children(t){if($i(t)&&(t=zm),this._isCompound){var r=this._children[t];if(r)return Bi(r)}else{if(t===zm)return this.nodes();if(this.hasNode(t))return[]}}predecessors(t){var r=this._preds[t];if(r)return Bi(r)}successors(t){var r=this._sucs[t];if(r)return Bi(r)}neighbors(t){var r=this.predecessors(t);if(r)return y9(r,this.successors(t))}isLeaf(t){var r;return this.isDirected()?r=this.successors(t):r=this.neighbors(t),r.length===0}filterNodes(t){var r=new this.constructor({directed:this._isDirected,multigraph:this._isMultigraph,compound:this._isCompound});r.setGraph(this.graph());var n=this;at(this._nodes,function(o,l){t(l)&&r.setNode(l,o)}),at(this._edgeObjs,function(o){r.hasNode(o.v)&&r.hasNode(o.w)&&r.setEdge(o,n.edge(o))});var i={};function a(o){var l=n.parent(o);return l===void 0||r.hasNode(l)?(i[o]=l,l):l in i?i[l]:a(l)}return s(a,"findParent"),this._isCompound&&at(r.nodes(),function(o){r.setParent(o,a(o))}),r}setDefaultEdgeLabel(t){return ou(t)||(t=fo(t)),this._defaultEdgeLabelFn=t,this}edgeCount(){return this._edgeCount}edges(){return Xo(this._edgeObjs)}setPath(t,r){var n=this,i=arguments;return du(t,function(a,o){return i.length>1?n.setEdge(a,o,r):n.setEdge(a,o),o}),this}setEdge(){var t,r,n,i,a=!1,o=arguments[0];typeof o=="object"&&o!==null&&"v"in o?(t=o.v,r=o.w,n=o.name,arguments.length===2&&(i=arguments[1],a=!0)):(t=o,r=arguments[1],n=arguments[3],arguments.length>2&&(i=arguments[2],a=!0)),t=""+t,r=""+r,$i(n)||(n=""+n);var l=cT(this._isDirected,t,r,n);if(Object.prototype.hasOwnProperty.call(this._edgeLabels,l))return a&&(this._edgeLabels[l]=i),this;if(!$i(n)&&!this._isMultigraph)throw new Error("Cannot set a named edge when isMultigraph = false");this.setNode(t),this.setNode(r),this._edgeLabels[l]=a?i:this._defaultEdgeLabelFn(t,r,n);var u=Sst(this._isDirected,t,r,n);return t=u.v,r=u.w,Object.freeze(u),this._edgeObjs[l]=u,Lge(this._preds[r],t),Lge(this._sucs[t],r),this._in[r][l]=u,this._out[t][l]=u,this._edgeCount++,this}edge(t,r,n){var i=arguments.length===1?v9(this._isDirected,arguments[0]):cT(this._isDirected,t,r,n);return this._edgeLabels[i]}hasEdge(t,r,n){var i=arguments.length===1?v9(this._isDirected,arguments[0]):cT(this._isDirected,t,r,n);return Object.prototype.hasOwnProperty.call(this._edgeLabels,i)}removeEdge(t,r,n){var i=arguments.length===1?v9(this._isDirected,arguments[0]):cT(this._isDirected,t,r,n),a=this._edgeObjs[i];return a&&(t=a.v,r=a.w,delete this._edgeLabels[i],delete this._edgeObjs[i],Dge(this._preds[r],t),Dge(this._sucs[t],r),delete this._in[r][i],delete this._out[t][i],this._edgeCount--),this}inEdges(t,r){var n=this._in[t];if(n){var i=Xo(n);return r?zs(i,function(a){return a.v===r}):i}}outEdges(t,r){var n=this._out[t];if(n){var i=Xo(n);return r?zs(i,function(a){return a.w===r}):i}}nodeEdges(t,r){var n=this.inEdges(t,r);if(n)return n.concat(this.outEdges(t,r))}};Ln.prototype._nodeCount=0;Ln.prototype._edgeCount=0;s(Lge,"incrementOrInitEntry");s(Dge,"decrementOrRemoveEntry");s(cT,"edgeArgsToId");s(Sst,"edgeArgsToObj");s(v9,"edgeObjToId")});var Ko=F(()=>{"use strict";x9()});function Ige(e){e._prev._next=e._next,e._next._prev=e._prev,delete e._next,delete e._prev}function wst(e,t){if(e!=="_next"&&e!=="_prev")return t}var F3,Mge=F(()=>{"use strict";F3=class{static{s(this,"List")}constructor(){var t={};t._next=t._prev=t,this._sentinel=t}dequeue(){var t=this._sentinel,r=t._prev;if(r!==t)return Ige(r),r}enqueue(t){var r=this._sentinel;t._prev&&t._next&&Ige(t),t._next=r._next,r._next._prev=t,r._next=t,t._prev=r}toString(){for(var t=[],r=this._sentinel,n=r._prev;n!==r;)t.push(JSON.stringify(n,wst)),n=n._prev;return"["+t.join(", ")+"]"}};s(Ige,"unlink");s(wst,"filterOutLinks")});function Nge(e,t){if(e.nodeCount()<=1)return[];var r=Ast(e,t||kst),n=Est(r.graph,r.buckets,r.zeroIdx);return Sl(Fn(n,function(i){return e.outEdges(i.v,i.w)}))}function Est(e,t,r){for(var n=[],i=t[t.length-1],a=t[0],o;e.nodeCount();){for(;o=a.dequeue();)b9(e,t,r,o);for(;o=i.dequeue();)b9(e,t,r,o);if(e.nodeCount()){for(var l=t.length-2;l>0;--l)if(o=t[l].dequeue(),o){n=n.concat(b9(e,t,r,o,!0));break}}}return n}function b9(e,t,r,n,i){var a=i?[]:void 0;return at(e.inEdges(n.v),function(o){var l=e.edge(o),u=e.node(o.v);i&&a.push({v:o.v,w:o.w}),u.out-=l,T9(t,r,u)}),at(e.outEdges(n.v),function(o){var l=e.edge(o),u=o.w,h=e.node(u);h.in-=l,T9(t,r,h)}),e.removeNode(n.v),a}function Ast(e,t){var r=new Ln,n=0,i=0;at(e.nodes(),function(l){r.setNode(l,{v:l,in:0,out:0})}),at(e.edges(),function(l){var u=r.edge(l.v,l.w)||0,h=t(l),d=u+h;r.setEdge(l.v,l.w,d),i=Math.max(i,r.node(l.v).out+=h),n=Math.max(n,r.node(l.w).in+=h)});var a=wl(i+n+3).map(function(){return new F3}),o=n+1;return at(r.nodes(),function(l){T9(a,o,r.node(l))}),{graph:r,buckets:a,zeroIdx:o}}function T9(e,t,r){r.out?r.in?e[r.out-r.in+t].enqueue(r):e[e.length-1].enqueue(r):e[0].enqueue(r)}var kst,Pge=F(()=>{"use strict";Gn();Ko();Mge();kst=fo(1);s(Nge,"greedyFAS");s(Est,"doGreedyFAS");s(b9,"removeNode");s(Ast,"buildState");s(T9,"assignBucket")});function Oge(e){var t=e.graph().acyclicer==="greedy"?Nge(e,r(e)):Rst(e);at(t,function(n){var i=e.edge(n);e.removeEdge(n),i.forwardName=n.name,i.reversed=!0,e.setEdge(n.w,n.v,i,Gm("rev"))});function r(n){return function(i){return n.edge(i).weight}}s(r,"weightFn")}function Rst(e){var t=[],r={},n={};function i(a){Object.prototype.hasOwnProperty.call(n,a)||(n[a]=!0,r[a]=!0,at(e.outEdges(a),function(o){Object.prototype.hasOwnProperty.call(r,o.w)?t.push(o):i(o.w)}),delete r[a])}return s(i,"dfs"),at(e.nodes(),i),t}function Bge(e){at(e.edges(),function(t){var r=e.edge(t);if(r.reversed){e.removeEdge(t);var n=r.forwardName;delete r.reversed,delete r.forwardName,e.setEdge(t.w,t.v,r,n)}})}var C9=F(()=>{"use strict";Gn();Pge();s(Oge,"run");s(Rst,"dfsFAS");s(Bge,"undo")});function pu(e,t,r,n){var i;do i=Gm(n);while(e.hasNode(i));return r.dummy=t,e.setNode(i,r),i}function Fge(e){var t=new Ln().setGraph(e.graph());return at(e.nodes(),function(r){t.setNode(r,e.node(r))}),at(e.edges(),function(r){var n=t.edge(r.v,r.w)||{weight:0,minlen:1},i=e.edge(r);t.setEdge(r.v,r.w,{weight:n.weight+i.weight,minlen:Math.max(n.minlen,i.minlen)})}),t}function G3(e){var t=new Ln({multigraph:e.isMultigraph()}).setGraph(e.graph());return at(e.nodes(),function(r){e.children(r).length||t.setNode(r,e.node(r))}),at(e.edges(),function(r){t.setEdge(r,e.edge(r))}),t}function S9(e,t){var r=e.x,n=e.y,i=t.x-r,a=t.y-n,o=e.width/2,l=e.height/2;if(!i&&!a)throw new Error("Not possible to find intersection inside of the rectangle");var u,h;return Math.abs(a)*o>Math.abs(i)*l?(a<0&&(l=-l),u=l*i/a,h=l):(i<0&&(o=-o),u=o,h=o*a/i),{x:r+u,y:n+h}}function Rf(e){var t=Fn(wl(k9(e)+1),function(){return[]});return at(e.nodes(),function(r){var n=e.node(r),i=n.rank;$i(i)||(t[i][n.order]=r)}),t}function Gge(e){var t=Mh(Fn(e.nodes(),function(r){return e.node(r).rank}));at(e.nodes(),function(r){var n=e.node(r);lT(n,"rank")&&(n.rank-=t)})}function zge(e){var t=Mh(Fn(e.nodes(),function(a){return e.node(a).rank})),r=[];at(e.nodes(),function(a){var o=e.node(a).rank-t;r[o]||(r[o]=[]),r[o].push(a)});var n=0,i=e.graph().nodeRankFactor;at(r,function(a,o){$i(a)&&o%i!==0?--n:n&&at(a,function(l){e.node(l).rank+=n})})}function w9(e,t,r,n){var i={width:0,height:0};return arguments.length>=4&&(i.rank=r,i.order=n),pu(e,"border",i,t)}function k9(e){return mo(Fn(e.nodes(),function(t){var r=e.node(t).rank;if(!$i(r))return r}))}function Vge(e,t){var r={lhs:[],rhs:[]};return at(e,function(n){t(n)?r.lhs.push(n):r.rhs.push(n)}),r}function Wge(e,t){var r=D3();try{return t()}finally{console.log(e+" time: "+(D3()-r)+"ms")}}function qge(e,t){return t()}var mu=F(()=>{"use strict";Gn();Ko();s(pu,"addDummyNode");s(Fge,"simplify");s(G3,"asNonCompoundGraph");s(S9,"intersectRect");s(Rf,"buildLayerMatrix");s(Gge,"normalizeRanks");s(zge,"removeEmptyRanks");s(w9,"addBorderNode");s(k9,"maxRank");s(Vge,"partition");s(Wge,"time");s(qge,"notime")});function Uge(e){function t(r){var n=e.children(r),i=e.node(r);if(n.length&&at(n,t),Object.prototype.hasOwnProperty.call(i,"minRank")){i.borderLeft=[],i.borderRight=[];for(var a=i.minRank,o=i.maxRank+1;a{"use strict";Gn();mu();s(Uge,"addBorderSegments");s(Hge,"addBorderNode")});function Xge(e){var t=e.graph().rankdir.toLowerCase();(t==="lr"||t==="rl")&&Zge(e)}function Kge(e){var t=e.graph().rankdir.toLowerCase();(t==="bt"||t==="rl")&&_st(e),(t==="lr"||t==="rl")&&(Lst(e),Zge(e))}function Zge(e){at(e.nodes(),function(t){jge(e.node(t))}),at(e.edges(),function(t){jge(e.edge(t))})}function jge(e){var t=e.width;e.width=e.height,e.height=t}function _st(e){at(e.nodes(),function(t){E9(e.node(t))}),at(e.edges(),function(t){var r=e.edge(t);at(r.points,E9),Object.prototype.hasOwnProperty.call(r,"y")&&E9(r)})}function E9(e){e.y=-e.y}function Lst(e){at(e.nodes(),function(t){A9(e.node(t))}),at(e.edges(),function(t){var r=e.edge(t);at(r.points,A9),Object.prototype.hasOwnProperty.call(r,"x")&&A9(r)})}function A9(e){var t=e.x;e.x=e.y,e.y=t}var Qge=F(()=>{"use strict";Gn();s(Xge,"adjust");s(Kge,"undo");s(Zge,"swapWidthHeight");s(jge,"swapWidthHeightOne");s(_st,"reverseY");s(E9,"reverseYOne");s(Lst,"swapXY");s(A9,"swapXYOne")});function Jge(e){e.graph().dummyChains=[],at(e.edges(),function(t){Ist(e,t)})}function Ist(e,t){var r=t.v,n=e.node(r).rank,i=t.w,a=e.node(i).rank,o=t.name,l=e.edge(t),u=l.labelRank;if(a!==n+1){e.removeEdge(t);var h=void 0,d,f;for(f=0,++n;n{"use strict";Gn();mu();s(Jge,"run");s(Ist,"normalizeEdge");s(e0e,"undo")});function hT(e){var t={};function r(n){var i=e.node(n);if(Object.prototype.hasOwnProperty.call(t,n))return i.rank;t[n]=!0;var a=Mh(Fn(e.outEdges(n),function(o){return r(o.w)-e.edge(o).minlen}));return(a===Number.POSITIVE_INFINITY||a===void 0||a===null)&&(a=0),i.rank=a}s(r,"dfs"),at(e.sources(),r)}function Vm(e,t){return e.node(t.w).rank-e.node(t.v).rank-e.edge(t).minlen}var z3=F(()=>{"use strict";Gn();s(hT,"longestPath");s(Vm,"slack")});function V3(e){var t=new Ln({directed:!1}),r=e.nodes()[0],n=e.nodeCount();t.setNode(r,{});for(var i,a;Mst(t,e){"use strict";Gn();Ko();z3();s(V3,"feasibleTree");s(Mst,"tightTree");s(Nst,"findMinSlackEdge");s(Pst,"shiftRanks")});var r0e=F(()=>{"use strict"});var L9=F(()=>{"use strict"});var i6r,D9=F(()=>{"use strict";Gn();L9();i6r=fo(1)});var n0e=F(()=>{"use strict";D9()});var I9=F(()=>{"use strict"});var i0e=F(()=>{"use strict";I9()});var m6r,a0e=F(()=>{"use strict";Gn();m6r=fo(1)});function M9(e){var t={},r={},n=[];function i(a){if(Object.prototype.hasOwnProperty.call(r,a))throw new dT;Object.prototype.hasOwnProperty.call(t,a)||(r[a]=!0,t[a]=!0,at(e.predecessors(a),i),delete r[a],n.push(a))}if(s(i,"visit"),at(e.sinks(),i),g9(t)!==e.nodeCount())throw new dT;return n}function dT(){}var N9=F(()=>{"use strict";Gn();M9.CycleException=dT;s(M9,"topsort");s(dT,"CycleException");dT.prototype=new Error});var s0e=F(()=>{"use strict";N9()});function W3(e,t,r){Zr(t)||(t=[t]);var n=(e.isDirected()?e.successors:e.neighbors).bind(e),i=[],a={};return at(t,function(o){if(!e.hasNode(o))throw new Error("Graph does not have node: "+o);o0e(e,o,r==="post",a,n,i)}),i}function o0e(e,t,r,n,i,a){Object.prototype.hasOwnProperty.call(n,t)||(n[t]=!0,r||a.push(t),at(i(t),function(o){o0e(e,o,r,n,i,a)}),r&&a.push(t))}var P9=F(()=>{"use strict";Gn();s(W3,"dfs");s(o0e,"doDfs")});function O9(e,t){return W3(e,t,"post")}var l0e=F(()=>{"use strict";P9();s(O9,"postorder")});function B9(e,t){return W3(e,t,"pre")}var c0e=F(()=>{"use strict";P9();s(B9,"preorder")});var u0e=F(()=>{"use strict";L9();x9()});var h0e=F(()=>{"use strict";r0e();D9();n0e();i0e();a0e();s0e();l0e();c0e();u0e();I9();N9()});function Lf(e){e=Fge(e),hT(e);var t=V3(e);F9(t),$9(t,e);for(var r,n;r=m0e(t);)n=g0e(t,e,r),y0e(t,e,r,n)}function $9(e,t){var r=O9(e,e.nodes());r=r.slice(0,r.length-1),at(r,function(n){Gst(e,t,n)})}function Gst(e,t,r){var n=e.node(r),i=n.parent;e.edge(r,i).cutvalue=f0e(e,t,r)}function f0e(e,t,r){var n=e.node(r),i=n.parent,a=!0,o=t.edge(r,i),l=0;return o||(a=!1,o=t.edge(i,r)),l=o.weight,at(t.nodeEdges(r),function(u){var h=u.v===r,d=h?u.w:u.v;if(d!==i){var f=h===a,p=t.edge(u).weight;if(l+=f?p:-p,Vst(e,r,d)){var m=e.edge(r,d).cutvalue;l+=f?-m:m}}}),l}function F9(e,t){arguments.length<2&&(t=e.nodes()[0]),p0e(e,{},1,t)}function p0e(e,t,r,n,i){var a=r,o=e.node(n);return t[n]=!0,at(e.neighbors(n),function(l){Object.prototype.hasOwnProperty.call(t,l)||(r=p0e(e,t,r,l,n))}),o.low=a,o.lim=r++,i?o.parent=i:delete o.parent,r}function m0e(e){return u1(e.edges(),function(t){return e.edge(t).cutvalue<0})}function g0e(e,t,r){var n=r.v,i=r.w;t.hasEdge(n,i)||(n=r.w,i=r.v);var a=e.node(n),o=e.node(i),l=a,u=!1;a.lim>o.lim&&(l=o,u=!0);var h=zs(t.edges(),function(d){return u===d0e(e,e.node(d.v),l)&&u!==d0e(e,e.node(d.w),l)});return $m(h,function(d){return Vm(t,d)})}function y0e(e,t,r,n){var i=r.v,a=r.w;e.removeEdge(i,a),e.setEdge(n.v,n.w,{}),F9(e),$9(e,t),zst(e,t)}function zst(e,t){var r=u1(e.nodes(),function(i){return!t.node(i).parent}),n=B9(e,r);n=n.slice(1),at(n,function(i){var a=e.node(i).parent,o=t.edge(i,a),l=!1;o||(o=t.edge(a,i),l=!0),t.node(i).rank=t.node(a).rank+(l?o.minlen:-o.minlen)})}function Vst(e,t,r){return e.hasEdge(t,r)}function d0e(e,t,r){return r.low<=t.lim&&t.lim<=r.lim}var v0e=F(()=>{"use strict";Gn();h0e();mu();_9();z3();Lf.initLowLimValues=F9;Lf.initCutValues=$9;Lf.calcCutValue=f0e;Lf.leaveEdge=m0e;Lf.enterEdge=g0e;Lf.exchangeEdges=y0e;s(Lf,"networkSimplex");s($9,"initCutValues");s(Gst,"assignCutValue");s(f0e,"calcCutValue");s(F9,"initLowLimValues");s(p0e,"dfsAssignLowLim");s(m0e,"leaveEdge");s(g0e,"enterEdge");s(y0e,"exchangeEdges");s(zst,"updateRanks");s(Vst,"isTreeEdge");s(d0e,"isDescendant")});function G9(e){switch(e.graph().ranker){case"network-simplex":x0e(e);break;case"tight-tree":qst(e);break;case"longest-path":Wst(e);break;default:x0e(e)}}function qst(e){hT(e),V3(e)}function x0e(e){Lf(e)}var Wst,z9=F(()=>{"use strict";_9();v0e();z3();s(G9,"rank");Wst=hT;s(qst,"tightTreeRanker");s(x0e,"networkSimplexRanker")});function b0e(e){var t=pu(e,"root",{},"_root"),r=Hst(e),n=mo(Xo(r))-1,i=2*n+1;e.graph().nestingRoot=t,at(e.edges(),function(o){e.edge(o).minlen*=i});var a=Ust(e)+1;at(e.children(),function(o){T0e(e,t,i,a,n,r,o)}),e.graph().nodeRankFactor=i}function T0e(e,t,r,n,i,a,o){var l=e.children(o);if(!l.length){o!==t&&e.setEdge(t,o,{weight:0,minlen:r});return}var u=w9(e,"_bt"),h=w9(e,"_bb"),d=e.node(o);e.setParent(u,o),d.borderTop=u,e.setParent(h,o),d.borderBottom=h,at(l,function(f){T0e(e,t,r,n,i,a,f);var p=e.node(f),m=p.borderTop?p.borderTop:f,g=p.borderBottom?p.borderBottom:f,y=p.borderTop?n:2*n,v=m!==g?1:i-a[o]+1;e.setEdge(u,m,{weight:y,minlen:v,nestingEdge:!0}),e.setEdge(g,h,{weight:y,minlen:v,nestingEdge:!0})}),e.parent(o)||e.setEdge(t,u,{weight:0,minlen:i+a[o]})}function Hst(e){var t={};function r(n,i){var a=e.children(n);a&&a.length&&at(a,function(o){r(o,i+1)}),t[n]=i}return s(r,"dfs"),at(e.children(),function(n){r(n,1)}),t}function Ust(e){return du(e.edges(),function(t,r){return t+e.edge(r).weight},0)}function C0e(e){var t=e.graph();e.removeNode(t.nestingRoot),delete t.nestingRoot,at(e.edges(),function(r){var n=e.edge(r);n.nestingEdge&&e.removeEdge(r)})}var S0e=F(()=>{"use strict";Gn();mu();s(b0e,"run");s(T0e,"dfs");s(Hst,"treeDepths");s(Ust,"sumWeights");s(C0e,"cleanup")});function w0e(e,t,r){var n={},i;at(r,function(a){for(var o=e.parent(a),l,u;o;){if(l=e.parent(o),l?(u=n[l],n[l]=o):(u=i,i=o),u&&u!==o){t.setEdge(u,o);return}o=l}})}var k0e=F(()=>{"use strict";Gn();s(w0e,"addSubgraphConstraints")});function E0e(e,t,r){var n=jst(e),i=new Ln({compound:!0}).setGraph({root:n}).setDefaultNodeLabel(function(a){return e.node(a)});return at(e.nodes(),function(a){var o=e.node(a),l=e.parent(a);(o.rank===t||o.minRank<=t&&t<=o.maxRank)&&(i.setNode(a),i.setParent(a,l||n),at(e[r](a),function(u){var h=u.v===a?u.w:u.v,d=i.edge(h,a),f=$i(d)?0:d.weight;i.setEdge(h,a,{weight:e.edge(u).weight+f})}),Object.prototype.hasOwnProperty.call(o,"minRank")&&i.setNode(a,{borderLeft:o.borderLeft[t],borderRight:o.borderRight[t]}))}),i}function jst(e){for(var t;e.hasNode(t=Gm("_root")););return t}var A0e=F(()=>{"use strict";Gn();Ko();s(E0e,"buildLayerGraph");s(jst,"createRootNode")});function R0e(e,t){for(var r=0,n=1;n0;)d%2&&(f+=l[d+1]),d=d-1>>1,l[d]+=h.weight;u+=h.weight*f})),u}var _0e=F(()=>{"use strict";Gn();s(R0e,"crossCount");s(Xst,"twoLayerCrossCount")});function L0e(e){var t={},r=zs(e.nodes(),function(l){return!e.children(l).length}),n=mo(Fn(r,function(l){return e.node(l).rank})),i=Fn(wl(n+1),function(){return[]});function a(l){if(!lT(t,l)){t[l]=!0;var u=e.node(l);i[u.rank].push(l),at(e.successors(l),a)}}s(a,"dfs");var o=fu(r,function(l){return e.node(l).rank});return at(o,a),i}var D0e=F(()=>{"use strict";Gn();s(L0e,"initOrder")});function I0e(e,t){return Fn(t,function(r){var n=e.inEdges(r);if(n.length){var i=du(n,function(a,o){var l=e.edge(o),u=e.node(o.v);return{sum:a.sum+l.weight*u.order,weight:a.weight+l.weight}},{sum:0,weight:0});return{v:r,barycenter:i.sum/i.weight,weight:i.weight}}else return{v:r}})}var M0e=F(()=>{"use strict";Gn();s(I0e,"barycenter")});function N0e(e,t){var r={};at(e,function(i,a){var o=r[i.v]={indegree:0,in:[],out:[],vs:[i.v],i:a};$i(i.barycenter)||(o.barycenter=i.barycenter,o.weight=i.weight)}),at(t.edges(),function(i){var a=r[i.v],o=r[i.w];!$i(a)&&!$i(o)&&(o.indegree++,a.out.push(r[i.w]))});var n=zs(r,function(i){return!i.indegree});return Kst(n)}function Kst(e){var t=[];function r(a){return function(o){o.merged||($i(o.barycenter)||$i(a.barycenter)||o.barycenter>=a.barycenter)&&Zst(a,o)}}s(r,"handleIn");function n(a){return function(o){o.in.push(a),--o.indegree===0&&e.push(o)}}for(s(n,"handleOut");e.length;){var i=e.pop();t.push(i),at(i.in.reverse(),r(i)),at(i.out,n(i))}return Fn(zs(t,function(a){return!a.merged}),function(a){return Fm(a,["vs","i","barycenter","weight"])})}function Zst(e,t){var r=0,n=0;e.weight&&(r+=e.barycenter*e.weight,n+=e.weight),t.weight&&(r+=t.barycenter*t.weight,n+=t.weight),e.vs=t.vs.concat(e.vs),e.barycenter=r/n,e.weight=n,e.i=Math.min(t.i,e.i),t.merged=!0}var P0e=F(()=>{"use strict";Gn();s(N0e,"resolveConflicts");s(Kst,"doResolveConflicts");s(Zst,"mergeEntries")});function B0e(e,t){var r=Vge(e,function(d){return Object.prototype.hasOwnProperty.call(d,"barycenter")}),n=r.lhs,i=fu(r.rhs,function(d){return-d.i}),a=[],o=0,l=0,u=0;n.sort(Qst(!!t)),u=O0e(a,i,u),at(n,function(d){u+=d.vs.length,a.push(d.vs),o+=d.barycenter*d.weight,l+=d.weight,u=O0e(a,i,u)});var h={vs:Sl(a)};return l&&(h.barycenter=o/l,h.weight=l),h}function O0e(e,t,r){for(var n;t.length&&(n=Af(t)).i<=r;)t.pop(),e.push(n.vs),r++;return r}function Qst(e){return function(t,r){return t.barycenterr.barycenter?1:e?r.i-t.i:t.i-r.i}}var $0e=F(()=>{"use strict";Gn();mu();s(B0e,"sort");s(O0e,"consumeUnsortable");s(Qst,"compareWithBias")});function V9(e,t,r,n){var i=e.children(t),a=e.node(t),o=a?a.borderLeft:void 0,l=a?a.borderRight:void 0,u={};o&&(i=zs(i,function(g){return g!==o&&g!==l}));var h=I0e(e,i);at(h,function(g){if(e.children(g.v).length){var y=V9(e,g.v,r,n);u[g.v]=y,Object.prototype.hasOwnProperty.call(y,"barycenter")&&eot(g,y)}});var d=N0e(h,r);Jst(d,u);var f=B0e(d,n);if(o&&(f.vs=Sl([o,f.vs,l]),e.predecessors(o).length)){var p=e.node(e.predecessors(o)[0]),m=e.node(e.predecessors(l)[0]);Object.prototype.hasOwnProperty.call(f,"barycenter")||(f.barycenter=0,f.weight=0),f.barycenter=(f.barycenter*f.weight+p.order+m.order)/(f.weight+2),f.weight+=2}return f}function Jst(e,t){at(e,function(r){r.vs=Sl(r.vs.map(function(n){return t[n]?t[n].vs:n}))})}function eot(e,t){$i(e.barycenter)?(e.barycenter=t.barycenter,e.weight=t.weight):(e.barycenter=(e.barycenter*e.weight+t.barycenter*t.weight)/(e.weight+t.weight),e.weight+=t.weight)}var F0e=F(()=>{"use strict";Gn();M0e();P0e();$0e();s(V9,"sortSubgraph");s(Jst,"expandSubgraphs");s(eot,"mergeBarycenters")});function V0e(e){var t=k9(e),r=G0e(e,wl(1,t+1),"inEdges"),n=G0e(e,wl(t-1,-1,-1),"outEdges"),i=L0e(e);z0e(e,i);for(var a=Number.POSITIVE_INFINITY,o,l=0,u=0;u<4;++l,++u){tot(l%2?r:n,l%4>=2),i=Rf(e);var h=R0e(e,i);h{"use strict";Gn();Ko();mu();k0e();A0e();_0e();D0e();F0e();s(V0e,"order");s(G0e,"buildLayerGraphs");s(tot,"sweepLayerGraphs");s(z0e,"assignOrder")});function q0e(e){var t=not(e);at(e.graph().dummyChains,function(r){for(var n=e.node(r),i=n.edgeObj,a=rot(e,t,i.v,i.w),o=a.path,l=a.lca,u=0,h=o[u],d=!0;r!==i.w;){if(n=e.node(r),d){for(;(h=o[u])!==l&&e.node(h).maxRanko||l>t[u].lim));for(h=u,u=n;(u=e.parent(u))!==h;)a.push(u);return{path:i.concat(a.reverse()),lca:h}}function not(e){var t={},r=0;function n(i){var a=r;at(e.children(i),n),t[i]={low:a,lim:r++}}return s(n,"dfs"),at(e.children(),n),t}var H0e=F(()=>{"use strict";Gn();s(q0e,"parentDummyChains");s(rot,"findPath");s(not,"postorder")});function iot(e,t){var r={};function n(i,a){var o=0,l=0,u=i.length,h=Af(a);return at(a,function(d,f){var p=sot(e,d),m=p?e.node(p).order:u;(p||d===h)&&(at(a.slice(l,f+1),function(g){at(e.predecessors(g),function(y){var v=e.node(y),x=v.order;(xh)&&U0e(r,p,d)})})}s(n,"scan");function i(a,o){var l=-1,u,h=0;return at(o,function(d,f){if(e.node(d).dummy==="border"){var p=e.predecessors(d);p.length&&(u=e.node(p[0]).order,n(o,h,f,l,u),h=f,l=u)}n(o,h,o.length,u,a.length)}),o}return s(i,"visitLayer"),du(t,i),r}function sot(e,t){if(e.node(t).dummy)return u1(e.predecessors(t),function(r){return e.node(r).dummy})}function U0e(e,t,r){if(t>r){var n=t;t=r,r=n}Object.prototype.hasOwnProperty.call(e,t)||Object.defineProperty(e,t,{enumerable:!0,configurable:!0,value:{},writable:!0});var i=e[t];Object.defineProperty(i,r,{enumerable:!0,configurable:!0,value:!0,writable:!0})}function oot(e,t,r){if(t>r){var n=t;t=r,r=n}return!!e[t]&&Object.prototype.hasOwnProperty.call(e[t],r)}function lot(e,t,r,n){var i={},a={},o={};return at(t,function(l){at(l,function(u,h){i[u]=u,a[u]=u,o[u]=h})}),at(t,function(l){var u=-1;at(l,function(h){var d=n(h);if(d.length){d=fu(d,function(y){return o[y]});for(var f=(d.length-1)/2,p=Math.floor(f),m=Math.ceil(f);p<=m;++p){var g=d[p];a[h]===h&&u{"use strict";Gn();Ko();mu();s(iot,"findType1Conflicts");s(aot,"findType2Conflicts");s(sot,"findOtherInnerSegmentNode");s(U0e,"addConflict");s(oot,"hasConflict");s(lot,"verticalAlignment");s(cot,"horizontalCompaction");s(uot,"buildBlockGraph");s(hot,"findSmallestWidthAlignment");s(dot,"alignCoordinates");s(fot,"balance");s(Y0e,"positionX");s(pot,"sep");s(mot,"width")});function X0e(e){e=G3(e),got(e),d9(Y0e(e),function(t,r){e.node(r).x=t})}function got(e){var t=Rf(e),r=e.graph().ranksep,n=0;at(t,function(i){var a=mo(Fn(i,function(o){return e.node(o).height}));at(i,function(o){e.node(o).y=n+a/2}),n+=a+r})}var K0e=F(()=>{"use strict";Gn();mu();j0e();s(X0e,"position");s(got,"positionY")});function fT(e,t){var r=t&&t.debugTiming?Wge:qge;r("layout",()=>{var n=r(" buildLayoutGraph",()=>Aot(e));r(" runLayout",()=>yot(n,r)),r(" updateInputGraph",()=>vot(e,n))})}function yot(e,t){t(" makeSpaceForEdgeLabels",()=>Rot(e)),t(" removeSelfEdges",()=>Bot(e)),t(" acyclic",()=>Oge(e)),t(" nestingGraph.run",()=>b0e(e)),t(" rank",()=>G9(G3(e))),t(" injectEdgeLabelProxies",()=>_ot(e)),t(" removeEmptyRanks",()=>zge(e)),t(" nestingGraph.cleanup",()=>C0e(e)),t(" normalizeRanks",()=>Gge(e)),t(" assignRankMinMax",()=>Lot(e)),t(" removeEdgeLabelProxies",()=>Dot(e)),t(" normalize.run",()=>Jge(e)),t(" parentDummyChains",()=>q0e(e)),t(" addBorderSegments",()=>Uge(e)),t(" order",()=>V0e(e)),t(" insertSelfEdges",()=>$ot(e)),t(" adjustCoordinateSystem",()=>Xge(e)),t(" position",()=>X0e(e)),t(" positionSelfEdges",()=>Fot(e)),t(" removeBorderNodes",()=>Oot(e)),t(" normalize.undo",()=>e0e(e)),t(" fixupEdgeLabelCoords",()=>Not(e)),t(" undoCoordinateSystem",()=>Kge(e)),t(" translateGraph",()=>Iot(e)),t(" assignNodeIntersects",()=>Mot(e)),t(" reversePoints",()=>Pot(e)),t(" acyclic.undo",()=>Bge(e))}function vot(e,t){at(e.nodes(),function(r){var n=e.node(r),i=t.node(r);n&&(n.x=i.x,n.y=i.y,t.children(r).length&&(n.width=i.width,n.height=i.height))}),at(e.edges(),function(r){var n=e.edge(r),i=t.edge(r);n.points=i.points,Object.prototype.hasOwnProperty.call(i,"x")&&(n.x=i.x,n.y=i.y)}),e.graph().width=t.graph().width,e.graph().height=t.graph().height}function Aot(e){var t=new Ln({multigraph:!0,compound:!0}),r=q9(e.graph());return t.setGraph(d1({},bot,W9(r,xot),Fm(r,Tot))),at(e.nodes(),function(n){var i=q9(e.node(n));t.setNode(n,a9(W9(i,Cot),Sot)),t.setParent(n,e.parent(n))}),at(e.edges(),function(n){var i=q9(e.edge(n));t.setEdge(n,d1({},kot,W9(i,wot),Fm(i,Eot)))}),t}function Rot(e){var t=e.graph();t.ranksep/=2,at(e.edges(),function(r){var n=e.edge(r);n.minlen*=2,n.labelpos.toLowerCase()!=="c"&&(t.rankdir==="TB"||t.rankdir==="BT"?n.width+=n.labeloffset:n.height+=n.labeloffset)})}function _ot(e){at(e.edges(),function(t){var r=e.edge(t);if(r.width&&r.height){var n=e.node(t.v),i=e.node(t.w),a={rank:(i.rank-n.rank)/2+n.rank,e:t};pu(e,"edge-proxy",a,"_ep")}})}function Lot(e){var t=0;at(e.nodes(),function(r){var n=e.node(r);n.borderTop&&(n.minRank=e.node(n.borderTop).rank,n.maxRank=e.node(n.borderBottom).rank,t=mo(t,n.maxRank))}),e.graph().maxRank=t}function Dot(e){at(e.nodes(),function(t){var r=e.node(t);r.dummy==="edge-proxy"&&(e.edge(r.e).labelRank=r.rank,e.removeNode(t))})}function Iot(e){var t=Number.POSITIVE_INFINITY,r=0,n=Number.POSITIVE_INFINITY,i=0,a=e.graph(),o=a.marginx||0,l=a.marginy||0;function u(h){var d=h.x,f=h.y,p=h.width,m=h.height;t=Math.min(t,d-p/2),r=Math.max(r,d+p/2),n=Math.min(n,f-m/2),i=Math.max(i,f+m/2)}s(u,"getExtremes"),at(e.nodes(),function(h){u(e.node(h))}),at(e.edges(),function(h){var d=e.edge(h);Object.prototype.hasOwnProperty.call(d,"x")&&u(d)}),t-=o,n-=l,at(e.nodes(),function(h){var d=e.node(h);d.x-=t,d.y-=n}),at(e.edges(),function(h){var d=e.edge(h);at(d.points,function(f){f.x-=t,f.y-=n}),Object.prototype.hasOwnProperty.call(d,"x")&&(d.x-=t),Object.prototype.hasOwnProperty.call(d,"y")&&(d.y-=n)}),a.width=r-t+o,a.height=i-n+l}function Mot(e){at(e.edges(),function(t){var r=e.edge(t),n=e.node(t.v),i=e.node(t.w),a,o;r.points?(a=r.points[0],o=r.points[r.points.length-1]):(r.points=[],a=i,o=n),r.points.unshift(S9(n,a)),r.points.push(S9(i,o))})}function Not(e){at(e.edges(),function(t){var r=e.edge(t);if(Object.prototype.hasOwnProperty.call(r,"x"))switch((r.labelpos==="l"||r.labelpos==="r")&&(r.width-=r.labeloffset),r.labelpos){case"l":r.x-=r.width/2+r.labeloffset;break;case"r":r.x+=r.width/2+r.labeloffset;break}})}function Pot(e){at(e.edges(),function(t){var r=e.edge(t);r.reversed&&r.points.reverse()})}function Oot(e){at(e.nodes(),function(t){if(e.children(t).length){var r=e.node(t),n=e.node(r.borderTop),i=e.node(r.borderBottom),a=e.node(Af(r.borderLeft)),o=e.node(Af(r.borderRight));r.width=Math.abs(o.x-a.x),r.height=Math.abs(i.y-n.y),r.x=a.x+r.width/2,r.y=n.y+r.height/2}}),at(e.nodes(),function(t){e.node(t).dummy==="border"&&e.removeNode(t)})}function Bot(e){at(e.edges(),function(t){if(t.v===t.w){var r=e.node(t.v);r.selfEdges||(r.selfEdges=[]),r.selfEdges.push({e:t,label:e.edge(t)}),e.removeEdge(t)}})}function $ot(e){var t=Rf(e);at(t,function(r){var n=0;at(r,function(i,a){var o=e.node(i);o.order=a+n,at(o.selfEdges,function(l){pu(e,"selfedge",{width:l.label.width,height:l.label.height,rank:o.rank,order:a+ ++n,e:l.e,label:l.label},"_se")}),delete o.selfEdges})})}function Fot(e){at(e.nodes(),function(t){var r=e.node(t);if(r.dummy==="selfedge"){var n=e.node(r.e.v),i=n.x+n.width/2,a=n.y,o=r.x-i,l=n.height/2;e.setEdge(r.e,r.label),e.removeNode(t),r.label.points=[{x:i+2*o/3,y:a-l},{x:i+5*o/6,y:a-l},{x:i+o,y:a},{x:i+5*o/6,y:a+l},{x:i+2*o/3,y:a+l}],r.label.x=r.x,r.label.y=r.y}})}function W9(e,t){return Bm(Fm(e,t),Number)}function q9(e){var t={};return at(e,function(r,n){t[n.toLowerCase()]=r}),t}var xot,bot,Tot,Cot,Sot,wot,kot,Eot,Z0e=F(()=>{"use strict";Gn();Ko();Yge();Qge();C9();R9();z9();S0e();W0e();H0e();K0e();mu();s(fT,"layout");s(yot,"runLayout");s(vot,"updateInputGraph");xot=["nodesep","edgesep","ranksep","marginx","marginy"],bot={ranksep:50,edgesep:20,nodesep:50,rankdir:"tb"},Tot=["acyclicer","ranker","rankdir","align"],Cot=["width","height"],Sot={width:0,height:0},wot=["minlen","weight","width","height","labeloffset"],kot={minlen:1,weight:1,width:0,height:0,labeloffset:10,labelpos:"r"},Eot=["labelpos"];s(Aot,"buildLayoutGraph");s(Rot,"makeSpaceForEdgeLabels");s(_ot,"injectEdgeLabelProxies");s(Lot,"assignRankMinMax");s(Dot,"removeEdgeLabelProxies");s(Iot,"translateGraph");s(Mot,"assignNodeIntersects");s(Not,"fixupEdgeLabelCoords");s(Pot,"reversePointsForReversedEdges");s(Oot,"removeBorderNodes");s(Bot,"removeSelfEdges");s($ot,"insertSelfEdges");s(Fot,"positionSelfEdges");s(W9,"selectNumberAttrs");s(q9,"canonicalize")});var H9=F(()=>{"use strict";C9();Z0e();R9();z9()});var Q0e=F(()=>{"use strict"});var eye={};sr(eye,{captureNodeSizes:()=>qot,shouldCaptureSizes:()=>zot});function J0e(){if(!(typeof globalThis>"u"))return globalThis}function zot(){return!!J0e()?.mermaidCaptureSizes}function Vot(){return typeof location>"u"?"browser-dev":`${location.pathname}${location.search}`}function Wot(e,t){let r=J0e();if(!r)return;let n=t.node(),a=((n&&"ownerSVGElement"in n?n.ownerSVGElement:null)??n)?.id??"(unknown)";r.mermaidCapturedSizes??=[];let o={svgId:a,sizes:e};r.mermaidCapturedSizes.push(o),r.mermaidLastCapturedSizes=o}function qot(e,t){let r=[];for(let n of t.nodes)n.isGroup||r.push({id:n.id,width:n.width??0,height:n.height??0});r.length!==0&&Wot({metadata:{captureVersion:1,capturedAt:new Date().toISOString(),capturedFrom:Vot()},nodes:r},e)}var tye=F(()=>{"use strict";Q0e();s(J0e,"getCaptureGlobal");s(zot,"shouldCaptureSizes");s(Vot,"capturedFromLocation");s(Wot,"emitCapturedSizes");s(qot,"captureNodeSizes")});function U9(e,{edgePathsClass:t="edges edgePaths"}={}){let r=e.insert("g").attr("class","root"),n=r.insert("g").attr("class","clusters"),i=r.insert("g").attr("class",t),a=r.insert("g").attr("class","edgeLabels"),o=r.insert("g").attr("class","nodes");return{clusters:n,edgePaths:i,edgeLabels:a,nodes:o,rootGroups:r}}async function Hot(e,t){if(t.label){let{shapeSvg:r,bbox:n}=await yt(e,t);t.labelBBox={width:n.width,height:n.height},r.remove()}else t.labelBBox={width:0,height:0}}async function Y9(e,t,r){let n=await Eh(e,t,r),i=n.node()?.getBBox()??{width:0,height:0};return t.width=i.width,t.height=i.height,n}async function rye(e,t){let r=new Ln({multigraph:!0,compound:!0}),n=[...t.edges],i=Pe(),a=U9(e),{edgeLabels:o,nodes:l}=a,u=new Map,h=e.node()!=null;await Promise.all(t.nodes.map(async d=>{if(d.isGroup)h&&await Hot(l,d),r.setNode(d.id,{...d});else{if(h){let f=await Y9(l,d,{config:i,dir:d.dir});u.set(d.id,f)}r.setNode(d.id,{...d})}}));for(let d of n)h&&O4(d)&&await oc(o,d),r.setEdge(d.start,d.end,{...d},d.id),t.edges.some(p=>p.id===d.id)||t.edges.push(d);if(globalThis.mermaidCaptureSizes){let{captureNodeSizes:d}=await Promise.resolve().then(()=>(tye(),eye));d(e,t)}return{graph:r,groups:a,nodeElements:u}}var j9=F(()=>{"use strict";Ko();Jt();Fy();Lm();Ht();s(U9,"createLayoutElementGroups");s(Hot,"measureGroupLabel");s(Y9,"insertMeasuredNode");s(rye,"createGraphWithElements")});var qr,Wm,iye,aye,pT,Uot,sye,oye,Yot,qm,nye,lye,cye,uye,q3,hye,jot,X9=F(()=>{"use strict";St();Ko();qr=new Map,Wm=new Map,iye=new Map,aye=s(()=>{Wm.clear(),iye.clear(),qr.clear()},"clear"),pT=s((e,t)=>{let r=Wm.get(t)||[];return se.trace("In isDescendant",t," ",e," = ",r.includes(e)),r.includes(e)},"isDescendant"),Uot=s((e,t)=>{let r=Wm.get(t)||[];return se.info("Descendants of ",t," is ",r),se.info("Edge is ",e),e.v===t||e.w===t?!1:r?r.includes(e.v)||pT(e.v,t)||pT(e.w,t)||r.includes(e.w):(se.debug("Tilt, ",t,",not in descendants"),!1)},"edgeInCluster"),sye=s((e,t,r,n)=>{se.debug("Copying children of ",e,"root",n,"data",t.node(e),n);let i=t.children(e)||[];e!==n&&i.push(e),se.debug("Copying (nodes) clusterId",e,"nodes",i),i.forEach(a=>{if(t.children(a).length>0)sye(a,t,r,n);else{let o=t.node(a);se.info("cp ",a," to ",n," with parent ",e),r.setNode(a,o),n!==t.parent(a)&&(se.debug("Setting parent",a,t.parent(a)),r.setParent(a,t.parent(a))),e!==n&&a!==e?(se.debug("Setting parent",a,e),r.setParent(a,e)):(se.info("In copy ",e,"root",n,"data",t.node(e),n),se.debug("Not Setting parent for node=",a,"cluster!==rootId",e!==n,"node!==clusterId",a!==e));let l=t.edges(a);se.debug("Copying Edges",l),l.forEach(u=>{se.info("Edge",u);let h=t.edge(u.v,u.w,u.name);se.info("Edge data",h,n);try{Uot(u,n)?(se.info("Copying as ",u.v,u.w,h,u.name),r.setEdge(u.v,u.w,h,u.name),se.info("newGraph edges ",r.edges(),r.edge(r.edges()[0]))):se.info("Skipping copy of edge ",u.v,"-->",u.w," rootId: ",n," clusterId:",e)}catch(d){se.error(d)}})}se.debug("Removing node",a),t.removeNode(a)})},"copy"),oye=s((e,t)=>{let r=t.children(e),n=[...r];for(let i of r)iye.set(i,e),n=[...n,...oye(i,t)];return n},"extractDescendants"),Yot=s((e,t,r)=>{let n=e.edges().filter(u=>u.v===t||u.w===t),i=e.edges().filter(u=>u.v===r||u.w===r),a=n.map(u=>({v:u.v===t?r:u.v,w:u.w===t?t:u.w})),o=i.map(u=>({v:u.v,w:u.w}));return a.filter(u=>o.some(h=>u.v===h.v&&u.w===h.w))},"findCommonEdges"),qm=s((e,t,r)=>{let n=t.children(e);if(se.trace("Searching children of id ",e,n),n.length<1)return e;let i;for(let a of n){let o=qm(a,t,r),l=Yot(t,r,o);if(o)if(l.length>0)i=o;else return o}return i},"findNonClusterChild"),nye=s(e=>!qr.has(e)||!qr.get(e).externalConnections?e:qr.has(e)?qr.get(e).id:e,"getAnchorId"),lye=s((e,t)=>{if(!e||t>10){se.debug("Opting out, no graph ");return}else se.debug("Opting in, graph ");e.nodes().forEach(function(r){e.children(r).length>0&&(se.debug("Cluster identified",r," Replacement id in edges: ",qm(r,e,r)),Wm.set(r,oye(r,e)),qr.set(r,{id:qm(r,e,r),clusterData:e.node(r)}))}),e.nodes().forEach(function(r){let n=e.children(r),i=e.edges();n.length>0?(se.debug("Cluster identified",r,Wm),i.forEach(a=>{let o=pT(a.v,r),l=pT(a.w,r);o^l&&(se.debug("Edge: ",a," leaves cluster ",r),se.debug("Descendants of XXX ",r,": ",Wm.get(r)),qr.get(r).externalConnections=!0)})):se.debug("Not a cluster ",r,Wm)});for(let r of qr.keys()){let n=qr.get(r).id,i=e.parent(n);i!==r&&qr.has(i)&&!qr.get(i).externalConnections&&(qr.get(r).id=i);let a=e.edges().some(o=>o.v===r);if(n&&qr.get(r)?.externalConnections&&a&&hye(e,n,r)){let o=jot(e,r,e.parent(n));o&&(qr.get(r).id=o)}}e.edges().forEach(function(r){let n=e.edge(r);se.debug("Edge "+r.v+" -> "+r.w+": "+JSON.stringify(r)),se.debug("Edge "+r.v+" -> "+r.w+": "+JSON.stringify(e.edge(r)));let i=r.v,a=r.w;if(se.debug("Fix XXX",qr,"ids:",r.v,r.w,"Translating: ",qr.get(r.v)," --- ",qr.get(r.w)),qr.get(r.v)||qr.get(r.w)){if(se.debug("Fixing and trying - removing XXX",r.v,r.w,r.name),i=nye(r.v),a=nye(r.w),e.removeEdge(r.v,r.w,r.name),i!==r.v){let o=e.parent(i);qr.get(o).externalConnections=!0,n.fromCluster=r.v}if(a!==r.w){let o=e.parent(a);qr.get(o).externalConnections=!0,n.toCluster=r.w}se.debug("Fix Replacing with XXX",i,a,r.name),e.setEdge(i,a,n,r.name)}}),cye(e,0),se.trace(qr)},"adjustClustersAndEdges"),cye=s((e,t)=>{if(t>10){se.error("Bailing out");return}let r=e.nodes(),n=!1;for(let i of r){let a=e.children(i);n=n||a.length>0}if(!n){se.debug("Done, no node has children",e.nodes());return}se.debug("Nodes = ",r,t);for(let i of r)if(se.debug("Extracting node",i,qr,qr.has(i)&&!qr.get(i).externalConnections,!e.parent(i),e.node(i),e.children("D")," Depth ",t),!qr.has(i))se.debug("Not a cluster",i,t);else if(!qr.get(i).externalConnections&&e.children(i)&&e.children(i).length>0){se.debug("Cluster without external connections, without a parent and with children",i,t);let o=e.graph().rankdir==="TB"?"LR":"TB";qr.get(i)?.clusterData?.dir&&(o=qr.get(i).clusterData.dir,se.debug("Fixing dir",qr.get(i).clusterData.dir,o));let l=new Ln({multigraph:!0,compound:!0}).setGraph({rankdir:o,nodesep:50,ranksep:50,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}});sye(i,e,l,i),e.setNode(i,{clusterNode:!0,id:i,clusterData:qr.get(i).clusterData,label:qr.get(i).label,graph:l})}else se.debug("Cluster ** ",i," **not meeting the criteria !externalConnections:",!qr.get(i).externalConnections," no parent: ",!e.parent(i)," children ",e.children(i)&&e.children(i).length>0,e.children("D"),t),se.debug(qr);r=e.nodes(),se.debug("New list of nodes",r);for(let i of r){let a=e.node(i);se.debug(" Now next level",i,a),a?.clusterNode&&cye(a.graph,t+1)}},"extractor"),uye=s((e,t)=>{if(t.length===0)return[];let r=Object.assign([],t);return t.forEach(n=>{let i=e.children(n),a=uye(e,i);r=[...r,...a]}),r},"sorter"),q3=s(e=>uye(e,e.children()),"sortNodesByHierarchy"),hye=s((e,t,r)=>{let n=e.parent(t);for(;n&&n!==r;){let i=qr.get(n);if(i&&!i.externalConnections)return!0;n=e.parent(n)}return!1},"isNodeInExtractableCluster"),jot=s((e,t,r)=>{let n=e.children(t)??[];for(let i of n){if(i===r||pT(i,r))continue;let a=qm(i,e,t);if(a&&!hye(e,a,t))return a}return null},"findSafeAnchorNode")});function f1({prepareLayout:e,measureLayout:t,runLayoutCore:r,paintLayout:n,afterPaint:i,paintOptions:a}){let o=t??Z9;return s(async function(u,h,d,f){let p=h.select("g");(d?.insertMarkers??Gy)(p,u.markers,u.type,u.diagramId),K9();let m={element:p,helpers:d,options:f};m.preparedLayout=await e?.(u,m);let g=await o(u,m),y=await r(u,m),v={...m,measure:g};n?await n(u,v,y):await Q9(u,v,a),await i?.(u,v,y)},"render")}function K9(){Fue(),Bue(),Rue(),aye()}async function Z9(e,{element:t}){return await rye(t,e)}async function Q9(e,t,r={}){let{measure:n}=t,{groups:i}=n;for(let o of r.getNodes?.(e,t)??e.nodes)r.skipNode?.(o,t)||await Xot(i,o,t,r);let a=Zot(e.nodes);for(let o of e.edges)Qot(o,r)||await Jot(i,o,a,e,r,t)}async function Xot(e,t,r,n){t.clusterNode?au(t):Kot(t,r,n)?await sf(e.clusters,t):au(t)}function Kot(e,t,r){return e.isGroup===!0&&(r.isCluster?.(e,t)??!0)}function Zot(e){let t=new Map;for(let r of e)r?.id&&t.set(r.id,r);return t}function Qot(e,t){return e.isLayoutOnly||!!t.skipEdge?.(e)}async function Jot(e,t,r,n,i,a){let o=of(e.edgePaths,{...t},i.clusterDb??new Map,n.type,dye(t.start,t,r,a,i),dye(t.end,t,r,a,i),n.diagramId,elt(t,i));O4(t)&&(_m.has(t.id)||await oc(e.edgeLabels,t),tlt(t,o))}function dye(e,t,r,n,i){return i.getEdgeNode?.(e,t,n)??(e?r.get(e)??{}:{})}function elt(e,t){return typeof t.skipIntersect=="function"?t.skipIntersect(e):t.skipIntersect??!1}function tlt(e,t){let r=t?.updatedPath??t?.originalPath,n=Lt(),{subGraphTitleTotalMargin:i}=kh({flowchart:n.flowchart??{}});if(e.label){let a=_m.get(e.id),o=e.x,l=e.y;if(r){let u=or.calcLabelPosition(r);se.debug("Moving label "+e.label+" from (",o,",",l,") to (",u.x,",",u.y,") abc88"),t?.updatedPath&&(o=u.x,l=u.y)}a.attr("transform",`translate(${o}, ${l+i/2})`)}if(e?.startLabelLeft){let a=Pi.get(e.id).startLeft,o=e?.x,l=e?.y;if(r){let u=or.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_left",r);o=u.x,l=u.y}a.attr("transform",`translate(${o}, ${l})`)}if(e.startLabelRight){let a=Pi.get(e.id).startRight,o=e.x,l=e.y;if(r){let u=or.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_right",r);o=u.x,l=u.y}a.attr("transform",`translate(${o}, ${l})`)}if(e.endLabelLeft){let a=Pi.get(e.id).endLeft,o=e.x,l=e.y;if(r){let u=or.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_left",r);o=u.x,l=u.y}a.attr("transform",`translate(${o}, ${l})`)}if(e.endLabelRight){let a=Pi.get(e.id).endRight,o=e.x,l=e.y;if(r){let u=or.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_right",r);o=u.x,l=u.y}a.attr("transform",`translate(${o}, ${l})`)}}var H3=F(()=>{"use strict";St();My();br();er();O2();j9();B2();Fy();B4();Lm();X9();s(f1,"createCommonLayoutRenderer");s(K9,"clearLayoutRenderState");s(Z9,"defaultMeasureLayout");s(Q9,"paintLayoutData");s(Xot,"paintLayoutNode");s(Kot,"shouldPaintAsCluster");s(Zot,"buildNodeLookup");s(Qot,"shouldSkipPaintEdge");s(Jot,"paintLayoutEdge");s(dye,"getRenderedNode");s(elt,"shouldSkipIntersect");s(tlt,"positionRenderedEdgeLabel")});var Tye={};sr(Tye,{applyDagreLayoutResult:()=>vye,getEdgesToRender:()=>J9,measureDagreLayout:()=>xye,prepareLayoutForDagre:()=>eB,render:()=>flt,runDagreLayoutCore:()=>bye});var fye,pye,rlt,nlt,ilt,alt,slt,J9,gye,yye,olt,mye,llt,vye,clt,ult,eB,xye,bye,hlt,dlt,flt,Cye=F(()=>{"use strict";H9();Ko();j9();H3();My();Ht();X9();Lm();B2();Fy();St();O2();Jt();fye=s((e,t,r)=>Math.max(t,Math.min(r,e)),"clamp"),pye=s((e="TB")=>{switch(e){case"BT":return"bottom";case"LR":return"right";case"RL":return"left";case"TB":default:return"top"}},"getDefaultSelfLoopSide"),rlt=s(e=>e==="flowchart"||e==="flowchart-v2"||e==="stateDiagram"||e==="er"||e==="classDiagram","shouldMergeSelfLoopSegments"),nlt=["x","y","width","height","labelBBox","intersect","calcIntersect","diff","clusterNode"],ilt=s((e,t,r,n,i)=>{let a=[],o=new Set;if(r.forEach(({start:d,end:f})=>{d!==n&&o.add(d),f!==n&&o.add(f)}),o.forEach(d=>{let f=e.node(d);typeof f?.x=="number"&&typeof f?.y=="number"&&a.push(f)}),a.length===0&&r.forEach(({edge:d})=>{(d.points??[]).forEach(f=>{typeof f?.x=="number"&&typeof f?.y=="number"&&a.push(f)})}),a.length===0)return pye(i);let l=a.reduce((d,f)=>({x:d.x+f.x/a.length,y:d.y+f.y/a.length}),{x:0,y:0}),u=l.x-t.x,h=l.y-t.y;return Math.abs(u)>Math.abs(h)?u>0?"right":"left":Math.abs(h)>0?h>0?"bottom":"top":pye(i)},"getSelfLoopSide"),alt=s((e,t="top",r=0,n=0)=>{let i=e.x,a=e.y-r,o=e.width/2,l=e.height/2,u=Math.max(36,Math.min(100,e.width*.8)),h=fye(Math.max(n,e.width*.35),36,u),d=fye(Math.min(e.width,e.height)*.45,24,48);switch(t){case"bottom":{let f=a+l;return[{x:i-h/2,y:f},{x:i-h/2,y:f+d},{x:i+h/2,y:f+d},{x:i+h/2,y:f}]}case"right":{let f=i+o;return[{x:f,y:a-h/2},{x:f+d,y:a-h/2},{x:f+d,y:a+h/2},{x:f,y:a+h/2}]}case"left":{let f=i-o;return[{x:f,y:a-h/2},{x:f-d,y:a-h/2},{x:f-d,y:a+h/2},{x:f,y:a+h/2}]}case"top":default:{let f=a-l;return[{x:i-h/2,y:f},{x:i-h/2,y:f-d},{x:i+h/2,y:f-d},{x:i+h/2,y:f}]}}},"getSelfLoopPoints"),slt=s((e,t,r="top",n=0,i={})=>{let o=e.x,l=e.y-n,u=i.width??0,h=i.height??0;switch(r){case"bottom":return{x:o,y:Math.max(...t.map(d=>d.y))+h/2+4};case"right":return{x:Math.max(...t.map(d=>d.x))+u/2+4,y:l};case"left":return{x:Math.min(...t.map(d=>d.x))-u/2-4,y:l};case"top":default:return{x:o,y:Math.min(...t.map(d=>d.y))-h/2-4}}},"getSelfLoopLabelPosition"),J9=s((e,t=0,{mergeSelfLoops:r=!0}={})=>{let n=new Map,i=[],a=e.graph()?.rankdir;return e.edges().forEach(o=>{let l=e.edge(o);if(r&&l.selfLoop){let u=l.selfLoop.id;n.has(u)||n.set(u,[]),n.get(u).push({edge:l,start:o.v,end:o.w})}else i.push({edge:l,start:o.v,end:o.w})}),n.forEach(o=>{if(o.length!==3){o.forEach(x=>i.push(x));return}o.sort((x,b)=>x.edge.selfLoop.order-b.edge.selfLoop.order);let[l,u,h]=o,d=l.edge.originalEdge??u.edge.originalEdge??h.edge.originalEdge??u.edge,f=e.node(d.start);if(!f){o.forEach(x=>i.push(x));return}let p={width:u.edge.width,height:u.edge.height},m=ilt(e,f,o,d.start,a),g=alt(f,m,t,p.width??0),y=slt(f,g,m,t,p),v={...u.edge,...d,id:d.id,points:g,start:d.start,end:d.end,x:y.x,y:y.y,width:p.width,height:p.height,labelStyle:u.edge.labelStyle,fromCluster:l.edge.fromCluster??u.edge.fromCluster??h.edge.fromCluster,toCluster:l.edge.toCluster??u.edge.toCluster??h.edge.toCluster};delete v.selfLoop,delete v.originalEdge,i.push({edge:v,start:v.start,end:v.end})}),i},"getEdgesToRender"),gye=s(async({element:e,graph:t,diagramType:r,id:n,parentCluster:i,siteConfig:a})=>{let o=t.graph().rankdir;se.trace("Dir in recursive render - dir:",o);let{clusters:l,edgePaths:u,edgeLabels:h,nodes:d,rootGroups:f}=U9(e,{edgePathsClass:"edgePaths"});t.nodes()?se.info("Recursive render XXX",t.nodes()):se.info("No nodes found for",t),t.edges().length>0&&se.info("Recursive edges",t.edge(t.edges()[0]));let p=rlt(r);await Promise.all(t.nodes().map(async function(y){let v=t.node(y);if(i!==void 0){let x=JSON.parse(JSON.stringify(i.clusterData));se.trace(`Setting data for parent cluster XXX + Node.id = `,y,` + data=`,x.height,` +Parent cluster`,i.height),t.setNode(i.id,x),t.parent(y)||(se.trace("Setting parent",y,i.id),t.setParent(y,i.id,x))}if(se.info("(Insert) Node XXX"+y+": "+JSON.stringify(t.node(y))),v?.clusterNode){se.info("Cluster identified XBX",y,v.width,t.node(y));let{ranksep:x,nodesep:b}=t.graph();v.graph.setGraph({...v.graph.graph(),ranksep:x+25,nodesep:b});let T=await ult({element:d,graph:v.graph,diagramType:r,id:n,parentCluster:t.node(y),siteConfig:a}),w=T.elem;ht(v,w),v.diff=T.diff||0,se.info("New compound node after recursive render XAX",y,"width",v.width,"height",v.height),$ue(w,v)}else t.children(y).length>0?(se.trace("Cluster - the non recursive path XBX",y,v.id,v,v.width,"Graph:",t),se.trace(qm(v.id,t)),qr.set(v.id,{id:qm(v.id,t),node:v})):(se.trace("Node - the non recursive path XAX",y,d,t.node(y),o),await Y9(d,t.node(y),{config:a,dir:o}))})),await s(async()=>{let y=t.edges().map(async function(v){let x=t.edge(v.v,v.w,v.name);if(se.info("Edge "+v.v+" -> "+v.w+": "+JSON.stringify(v)),se.info("Edge "+v.v+" -> "+v.w+": ",v," ",JSON.stringify(t.edge(v))),se.info("Fix",qr,"ids:",v.v,v.w,"Translating: ",qr.get(v.v),qr.get(v.w)),p&&x.selfLoop){if(x.selfLoop.order!==1)return;let b={...x.originalEdge,...x,id:x.selfLoop.id,startLabelLeft:x.originalEdge?.startLabelLeft??x.startLabelLeft,startLabelRight:x.originalEdge?.startLabelRight??x.startLabelRight,endLabelLeft:x.originalEdge?.endLabelLeft??x.endLabelLeft,endLabelRight:x.originalEdge?.endLabelRight??x.endLabelRight};await oc(h,b),x.width=b.width,x.height=b.height,x.labelStyle=b.labelStyle;return}await oc(h,x)});await Promise.all(y)},"processEdges")();let{subGraphTitleTotalMargin:g}=kh(a);return{elem:f,graph:t,groups:{clusters:l,edgePaths:u,edgeLabels:h,nodes:d,rootGroups:f},diagramType:r,id:n,mergeSelfLoops:p,subGraphTitleTotalMargin:g}},"measureDagreGraph"),yye=s(e=>{se.info("############################################# XXX"),se.info("### Layout ### XXX"),se.info("############################################# XXX"),fT(e)},"runDagreGraphLayout"),olt=s((e,t,r)=>{let n=e.node(t);if(!n)return;let i={...n};return n?.clusterNode?i.y=(n.y??0)+r:e.children(t).length>0?i.height=(n.height??0)+r:i.y=(n.y??0)+r/2,i},"normalizeDagreNode"),mye=s((e,t)=>{nlt.forEach(r=>{t[r]!==void 0&&(e[r]=t[r])})},"applyDagreNodeLayout"),llt=s((e,t,r,n)=>({...e,start:e.start??t,end:e.end??r,points:(e.points??[]).map(i=>({...i,y:typeof i.y=="number"?i.y+n:i.y}))}),"normalizeDagreEdge"),vye=s((e,t)=>{let{graph:r,mergeSelfLoops:n,subGraphTitleTotalMargin:i=0}=t,a=new Map(e.nodes.map(l=>[l.id,l]));q3(r).forEach(l=>{let u=olt(r,l,i);if(!u)return;mye(r.node(l),u);let h=a.get(l);h&&mye(h,u)});let o=i/2;return e.edges=J9(r,o,{mergeSelfLoops:n}).map(({edge:l,start:u,end:h})=>llt(l,u,h,o)),e},"applyDagreLayoutResult"),clt=s(async({elem:e,graph:t,groups:{clusters:r,edgePaths:n},diagramType:i,id:a,mergeSelfLoops:o,subGraphTitleTotalMargin:l})=>{let u=0;await Promise.all(q3(t).map(async function(f){let p=t.node(f);if(se.info("Position XBX => "+f+": ("+p.x,","+p.y,") width: ",p.width," height: ",p.height),p?.clusterNode)p.y+=l,se.info("A tainted cluster node XBX1",f,p.id,p.width,p.height,p.x,p.y,t.parent(f)),qr.get(p.id).node=p,au(p);else if(t.children(f).length>0){se.info("A pure cluster node XBX1",f,p.id,p.x,p.y,p.width,p.height,t.parent(f)),p.height+=l,t.node(p.parentId);let m=p?.padding/2||0,g=p?.labelBBox?.height||0,y=g-m||0;se.debug("OffsetY",y,"labelHeight",g,"halfPadding",m),await sf(r,p),qr.get(p.id).node=p}else{let m=t.node(p.parentId);p.y+=l/2,se.info("A regular node XBX1 - using the padding",p.id,"parent",p.parentId,p.width,p.height,p.x,p.y,"offsetY",p.offsetY,"parent",m,m?.offsetY,p),au(p)}}));let h=l/2;return J9(t,h,{mergeSelfLoops:o}).forEach(function({edge:f,start:p,end:m}){se.info("Edge "+p+" -> "+m+": "+JSON.stringify(f),f),f.points.forEach(x=>x.y+=h);let g=t.node(p),y=t.node(m),v=of(n,f,qr,i,g,y,a);$y(f,v)}),t.nodes().forEach(function(f){let p=t.node(f);se.info(f,p.type,p.diff),p.isGroup&&(u=p.diff)}),se.warn("Returning from recursive render XAX",e,u),{elem:e,diff:u}},"paintDagreLayoutCore"),ult=s(async e=>{let t=await gye(e);return yye(t.graph),await clt(t)},"renderDagreSubgraph"),eB=s(e=>{let t=new Ln({multigraph:!0,compound:!0}).setGraph({rankdir:e.direction,nodesep:e.config?.nodeSpacing||e.nodeSpacing||e.config?.flowchart?.nodeSpacing,ranksep:e.config?.rankSpacing||e.rankSpacing||e.config?.flowchart?.rankSpacing,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}});return e.nodes.forEach(r=>{t.setNode(r.id,{...r}),r.parentId&&t.setParent(r.id,r.parentId)}),se.debug("Edges:",e.edges),e.edges.forEach(r=>{if(r.start===r.end){let n=r.start,i=n+"---"+n+"---1",a=n+"---"+n+"---2",o=t.node(n);t.setNode(i,{domId:i,id:i,parentId:o.parentId,labelStyle:"",label:"",padding:0,shape:"labelRect",style:"",width:10,height:10}),t.setParent(i,o.parentId),t.setNode(a,{domId:a,id:a,parentId:o.parentId,labelStyle:"",padding:0,shape:"labelRect",label:"",style:"",width:10,height:10}),t.setParent(a,o.parentId);let l=structuredClone(r),u=structuredClone(r),h=structuredClone(r),d=structuredClone(r);u.originalEdge=l,u.selfLoop={id:l.id,order:0},h.originalEdge=l,h.selfLoop={id:l.id,order:1},d.originalEdge=l,d.selfLoop={id:l.id,order:2},u.label="",u.arrowTypeEnd="none",u.endLabelLeft="",u.endLabelRight="",u.startLabelLeft="",u.id=n+"-cyclic-special-1",h.startLabelRight="",h.startLabelLeft="",h.endLabelLeft="",h.endLabelRight="",h.arrowTypeStart="none",h.arrowTypeEnd="none",h.id=n+"-cyclic-special-mid",d.label="",d.startLabelRight="",d.startLabelLeft="",d.arrowTypeStart="none",o.isGroup&&(u.fromCluster=n,d.toCluster=n),d.id=n+"-cyclic-special-2",d.arrowTypeStart="none",t.setEdge(n,i,u,n+"-cyclic-special-0"),t.setEdge(i,a,h,n+"-cyclic-special-1"),t.setEdge(a,n,d,n+"-cyclic-special-2")}else t.setEdge(r.start,r.end,{...r},r.id)}),lye(t),{graph:t}},"prepareLayoutForDagre"),xye=s(async(e,{element:t,preparedLayout:r})=>{let n=r??eB(e),i=Pe(),a=await gye({element:t,graph:n.graph,diagramType:e.type,id:e.diagramId,parentCluster:void 0,siteConfig:i});return n.measuredLayout=a,a},"measureDagreLayout"),bye=s((e,t)=>{let r=t.preparedLayout?.measuredLayout;if(!r)throw new Error("runDagreLayoutCore requires measureDagreLayout to run first");return yye(r.graph),vye(e,r),r},"runDagreLayoutCore"),hlt=s((e,{measure:t})=>q3(t.graph).map(r=>t.graph.node(r)).filter(Boolean),"getDagrePaintNodes"),dlt=s((e,t,{measure:r})=>e?r.graph.node(e):void 0,"getDagreEdgeNode"),flt=f1({prepareLayout:eB,measureLayout:xye,runLayoutCore:bye,paintOptions:{clusterDb:qr,getNodes:hlt,getEdgeNode:dlt,skipNode:s((e,{measure:t})=>!t.graph.hasNode(e.id),"skipNode"),isCluster:s((e,{measure:t})=>t.graph.hasNode(e.id)&&(t.graph.children(e.id)??[]).length>0,"isCluster")}})});function tB(e){let t=[];for(let r=0;r=1-Y3||p<=Y3||p>=1-Y3?null:{point:{x:e.x+f*i,y:e.y+f*a},tA:f,tB:p}}function wye(e){return Math.abs(e.b.x-e.a.x)>=Math.abs(e.b.y-e.a.y)}function mlt(e){let t=[];for(let r=0;r=Math.abs(r)?t>=0?1:0:r>=0?1:0}function vlt(e,t){if(e.length<2)return e.map(a=>({...a}));let r=e.map(a=>({...a})),n=t.arrowTypeStart&&ra[t.arrowTypeStart];if(n){let a=e[0],o=e[1],l=Math.atan2(o.y-a.y,o.x-a.x);r[0].x=a.x+n*Math.cos(l),r[0].y=a.y+n*Math.sin(l)}let i=t.arrowTypeEnd&&ra[t.arrowTypeEnd];if(i){let a=e.length,o=e[a-2],l=e[a-1],u=Math.atan2(l.y-o.y,l.x-o.x);r[a-1].x=l.x-i*Math.cos(u),r[a-1].y=l.y-i*Math.sin(u)}return r}function xlt(e,t,r,n,i){let a=e.point.x,o=e.point.y,l={x:a-t*e.r,y:o-r*e.r},u={x:a+t*e.r,y:o+r*e.r},h=[`L${mT(l)}`];return i==="arc"?h.push(`A${gu(e.r)},${gu(e.r)} 0 0 ${n} ${mT(u)}`):h.push(`M${mT(u)}`),h}function kye(e,t,r,n){let i=t.x-e.x,a=t.y-e.y,o=r.x-t.x,l=r.y-t.y,u=Math.hypot(i,a),h=Math.hypot(o,l);if(u0){let T=kye(i[h-1],i[h],i[h+1]??i[h],Sye);T&&(y=T.cutLen)}let v=f,x=null;a&&hT.t-w.t);for(let T of b)T.r=Math.min(T.r,T.d-y,v-T.d);for(let T=0;Tw){let C=w/2;b[T].r=Math.min(b[T].r,C),b[T+1].r=Math.min(b[T+1].r,C)}}for(let T of b)T.r=2?n:null}catch{return null}}function Eye(e,t,r){if(!r.enabled)return;let n=e.node();if(!n)return;let i=new Map;for(let h of t)i.set(h.id,h);let a=[],o=new Map;for(let h of t){let d=typeof CSS<"u"&&CSS.escape?CSS.escape(h.id):h.id,f=n.querySelector(`path[data-id="${d}"]`);if(!f)continue;o.set(h.id,f);let m=Slt(f.getAttribute("data-points"))??h.points;a.push({...h,points:m})}let l=mlt(a);if(l.length===0)return;let u=new Map;for(let h of l){let d=u.get(h.jumpEdgeId)??[];d.push(h),u.set(h.jumpEdgeId,d)}for(let h of a){let d=u.get(h.id);if(!d||d.length===0)continue;let p=i.get(h.id)?.curve;if(p!==void 0&&!Clt(p))continue;let m=o.get(h.id);if(!m)continue;if(p===void 0){let T=m.getAttribute("d")??"";if(!Tlt(T))continue}let g=m.getAttribute("style")??"",y=/stroke-dasharray\s*:\s*0\s+([\d.]+)\s+[\d.]+\s+([\d.]+)/.exec(g),v=y?Number.parseFloat(y[1]):null,x=y?Number.parseFloat(y[2]):null,b=blt(h,d,r);if(m.setAttribute("d",b),v!==null&&x!==null&&typeof m.getTotalLength=="function"){let T=m.getTotalLength(),w=Math.max(0,T-v-x),C=`0 ${v} ${w} ${x}`,S=g.replace(/stroke-dasharray\s*:[^;]*;?/g,`stroke-dasharray: ${C};`).replace(/;\s*;+/g,";");m.setAttribute("style",S)}}}var Sye,U3,Y3,ylt,Aye=F(()=>{"use strict";xO();Sye=5,U3=1e-5,Y3=1e-6;s(tB,"buildSegmentList");s(plt,"segmentIntersection");s(wye,"isHorizontalSeg");s(mlt,"findEdgeIntersections");s(gu,"fmt");s(mT,"pointToString");s(glt,"getArcSweepFlag");ylt=.001;s(vlt,"applyMarkerOffsets");s(xlt,"emitJump");s(kye,"computeRoundedCorner");s(blt,"rewriteEdgePath");s(Tlt,"isStraightPath");s(Clt,"curveSupportsLineHops");s(Slt,"decodeDataPoints");s(Eye,"applyLineJumpsToSvg")});function Rye(e,{measure:t}){let r=e.config?.swimlane?.lineHops;if(r===!1)return;let n=r==="gap"?"gap":"arc",i=e.edges.filter(a=>Array.isArray(a.points)&&a.points.length>=2).map(a=>({id:a.id,points:a.points,curve:a.curve,arrowTypeStart:a.arrowTypeStart,arrowTypeEnd:a.arrowTypeEnd}));Eye(t.groups.edgePaths,i,{enabled:!0,jumpRadius:6,jumpStyle:n})}var _ye=F(()=>{"use strict";Aye();s(Rye,"applySwimlaneLineJumps")});function Lye(e){return Math.max(e.padding??20,20)}function wlt(e){let{x:t,y:r,width:n,height:i}=e,a=e.swimlaneContentTop;if(typeof t!="number"||typeof r!="number"||typeof n!="number"||typeof i!="number"||typeof a!="number"||!Number.isFinite(t)||!Number.isFinite(r)||!Number.isFinite(n)||!Number.isFinite(i)||!Number.isFinite(a)||n<=0||i<=0){delete e.groupTitleRect;return}let o=r-i/2,l=Math.min(a,r+i/2),u=Math.min(21,Math.max(0,l-o)),h=o+u;if(h<=o){delete e.groupTitleRect;return}e.groupTitleRect={left:t-n/2,right:t+n/2,top:o,bottom:h}}function Dye(e){let t=e.direction,r=e.nodes??=[];for(let a of e.nodes??[])a.isGroup&&!a.parentId&&(a.shape="swimlane",t&&(a.direction=t));let n=r.filter(a=>!a.isGroup&&!a.parentId);if(n.length===0)return;let i=r.find(a=>a.id===rB);i?i.isGroup&&(i.shape="swimlane",t&&(i.direction=t)):(i={id:rB,label:"",isGroup:!0,shape:"swimlane",padding:20,...t?{direction:t}:{}},r.push(i));for(let a of n)a.parentId=rB}function Iye(e){let t=new Map;for(let u of e.nodes??[])t.set(u.id,u);let r=[];for(let u of e.edges??[]){let h=typeof u.start=="string"?u.start:void 0,d=typeof u.end=="string"?u.end:void 0;!h||!d||u.labelNodeId||r.push({id:u.id,src:h,dst:d,ref:u})}let n=e.nodes??[],i=n.filter(u=>u.isGroup),a=n.filter(u=>!u.isGroup);return{nodes:[...[...i].reverse(),...a].map(u=>u.id),edges:r,layout:e,nodeById:t}}function Mye(e,t,r,n){let{layout:i}=e,a=e.nodeById,o=n?.layerGap??100,l=n?.nodeGap??40,u=0;for(let p of t.layers){let m=0;for(let g of p){let y=a.get(g);if(!y){m++;continue}y.layer=u,y.order=m;let v=r.x[g]??m*l,x=r.y[g]??u*o;y.x=v,y.y=x,m++}u++}let h=i.nodes??[],d=new Map,f=[];for(let p of h){if(!p?.isGroup)continue;p.parentId||f.push(p);let m=h.filter(b=>b.parentId===p.id),g=1/0,y=-1/0,v=1/0,x=-1/0;for(let b of m){let T=b.x??r.x[b.id],w=b.y??r.y[b.id],C=b.width??0,S=b.height??0;T!=null&&w!=null&&(g=Math.min(g,T-C/2),y=Math.max(y,T+C/2),v=Math.min(v,w-S/2),x=Math.max(x,w+S/2))}if(g===1/0||v===1/0)p.x=p.x??0,p.y=p.y??0,p.width=p.width??0,p.height=p.height??0;else{let b=p.padding??20,T=p.parentId?b:2*Lye(p),w=b,C=Math.max(0,y-g)+T,S=Math.max(0,x-v)+w,k=(g+y)/2,R=(v+x)/2;p.x=k,p.y=R,p.width=C,p.height=S,d.set(p.id,{minX:g,maxX:y,minY:v,maxY:x})}}if(f.length>0&&d.size>0){let p=1/0,m=-1/0,g=0;for(let y of f){let v=y.padding??20;v>g&&(g=v);let x=d.get(y.id);x&&(p=Math.min(p,x.minY),m=Math.max(m,x.maxY))}if(p!==1/0&&m!==-1/0){let y=Math.max(0,m-p),x=Math.max(g,36),b=y+2*x,T=(p+m)/2;for(let M of f)M.y=T,M.height=b,M.swimlaneContentTop=p;let w=[...f].sort((M,P)=>{let N=M.x??0,_=P.x??0;return N-_}),C=[],S=[],k=[];for(let M of w){let P=d.get(M.id);if(!P)continue;let N=Math.max(0,P.maxX-P.minX)+2*Lye(M),_=(P.minX+P.maxX)/2;C.push(M.id),S.push(_),k.push(N)}let R=C.length;if(R>0){let M=new Map;if(R===1)M.set(C[0],k[0]);else{let P=[];for(let D=0;D{"use strict";rB="__swimlane_default__";s(Lye,"topLaneHorizontalPadding");s(wlt,"assignTopLaneTitleRect");s(Dye,"prepareLayoutForSwimlanes");s(Iye,"toGraphView");s(Mye,"writeBackToLayoutData")});function Nye(e){let t=[],r=[],n=new Map;for(let o of e.nodes)n.set(o.id,o);for(let o of e.edges){if(!o.label||o.label.length===0||o.isLayoutOnly||o.labelNodeId)continue;let l=o.start?n.get(o.start):void 0,u=o.end?n.get(o.end):void 0;if(!l||!u){se.warn(klt,`Edge ${o.id} has missing source or target node`);continue}let h=`edge-label-${o.start}-${o.end}-${o.id}`,f=l.parentId!==u.parentId?u.parentId:l.parentId,p={id:h,label:o.label,edgeStart:o.start??"",edgeEnd:o.end??"",shape:"labelRect",width:0,height:0,isEdgeLabel:!0,isDummy:!0,parentId:f,isGroup:!1,labelStyle:Array.isArray(o.labelStyle)?o.labelStyle[0]:o.labelStyle??"",...l.dir?{dir:l.dir}:{}};t.push(p),o.labelNodeId=h,o.label=void 0,o.text=void 0;let m={id:`${o.id}-to-label`,start:o.start,end:h,type:"normal",isLayoutOnly:!0},g={id:`${o.id}-from-label`,start:h,end:o.end,type:"normal",isLayoutOnly:!0};r.push(m,g)}let i=[...e.nodes,...t],a=[...e.edges,...r];return{...e,nodes:i,edges:a}}var klt,Pye=F(()=>{"use strict";St();klt="[EdgeLabelNodes]";s(Nye,"createEdgeLabelNodes")});function Bye(e){let t=e.x??0,r=e.y??0,n=e.width??0,i=e.height??0;return n>0&&i>0?{cx:t,cy:r,rect:p1(t,r,n,i)}:void 0}function $ye(e){if(e.isGroup)return;let t=Bye(e);return t?{id:String(e.id??""),cx:t.cx,cy:t.cy,rect:t.rect}:void 0}function go(e,t,r=.001){return Math.abs(e.x-t.x)r}function Ei(e,t,r=.001){return tn(e,t,r)&&Math.abs(e.y-t.y)>r}function xs(e,t,r,n){return Math.max(0,Math.min(Math.max(e,t),Math.max(r,n))-Math.max(Math.min(e,t),Math.min(r,n)))}function hc(e,t,r=.001){return e.horizontal&&t.horizontal&&an(e.a,t.a,r)?xs(e.a.x,e.b.x,t.a.x,t.b.x):e.vertical&&t.vertical&&tn(e.a,t.a,r)?xs(e.a.y,e.b.y,t.a.y,t.b.y):0}function Df(e,t=.001){let r=[];for(let n=0;n0?r[r.length-1]:void 0;(!i||!go(i,n,t))&&r.push({x:n.x,y:n.y})}return r}function j3(e,t=.001){if(!e||e.length!==4)return;let[r,n,i,a]=e;return ki(r,n,t)&&Ei(n,i,t)&&ki(i,a,t)?{kind:"HVH",p0:r,p1:n,p2:i,p3:a}:Ei(r,n,t)&&ki(n,i,t)&&Ei(i,a,t)?{kind:"VHV",p0:r,p1:n,p2:i,p3:a}:void 0}function gT(e,t,r,n=0){let i=Math.min(e.x,t.x),a=Math.max(e.x,t.x),o=Math.min(e.y,t.y),l=Math.max(e.y,t.y);return a>r.left-n&&ir.top-n&&ot.left+r&&e.xt.top+r&&e.y=t.right&&e.top<=t.top&&e.bottom>=t.bottom}function K3(e,t){return e.leftt.left&&e.topt.top}function iB(e,t){return{left:e.left-t,right:e.right+t,top:e.top-t,bottom:e.bottom+t}}function p1(e,t,r,n){return{left:e-r/2,right:e+r/2,top:t-n/2,bottom:t+n/2}}function Ba(e){return Bye(e)?.rect}function If(e,t){switch(t){case"top":return{x:e.cx,y:e.rect.top};case"bottom":return{x:e.cx,y:e.rect.bottom};case"left":return{x:e.rect.left,y:e.cy};case"right":return{x:e.rect.right,y:e.cy}}}function Z3(e,t,r,n,i,a=.001){let o=t==="left"||t==="right",l=n==="left"||n==="right";if(o&&l){if(t==="right"&&n==="left"&&e.xr.x){if(an(e,r,a))return[e,r];let f=(e.x+r.x)/2;return[e,{x:f,y:e.y},{x:f,y:r.y},r]}if(t===n){if(an(e,r,a))return;let f=t==="left"?Math.min(e.x,r.x)-i:Math.max(e.x,r.x)+i;return[e,{x:f,y:e.y},{x:f,y:r.y},r]}return}if(!o&&!l){if(t===n){if(tn(e,r,a))return;let p=t==="top"?Math.min(e.y,r.y)-i:Math.max(e.y,r.y)+i;return[e,{x:e.x,y:p},{x:r.x,y:p},r]}if(!(t==="bottom"&&n==="top"&&e.yr.y))return;if(tn(e,r,a))return[e,r];let f=(e.y+r.y)/2;return[e,{x:e.x,y:f},{x:r.x,y:f},r]}if(o&&!l){let d=t==="right"&&r.x>e.x||t==="left"&&r.xr.y;return d&&f?[e,{x:r.x,y:e.y},r]:void 0}let u=t==="bottom"&&r.y>e.y||t==="top"&&r.yr.x;return u&&h?[e,{x:e.x,y:r.y},r]:void 0}function Q3(e,t,r,n){return t==="left"||t==="right"?[e,{x:n,y:e.y},{x:n,y:r.y},r]:[e,{x:e.x,y:n},{x:r.x,y:n},r]}function m1(e){let t=new Map,r=[];for(let n of e){if(n.isEdgeLabel)continue;let i=$ye(n);i&&(t.set(i.id,i),r.push({id:i.id,rect:i.rect}))}return{nodeInfoById:t,realNodeRects:r}}function yu(e){let t=[],r=[];for(let n of e){let i=$ye(n);if(!i)continue;let a={id:i.id,rect:i.rect};n.isEdgeLabel?r.push(a):t.push(a)}return{realNodeRects:t,labelNodeRects:r}}function Gye(e,{includeEdgeLabels:t=!0}={}){let r=[];for(let n of e){if(n.isGroup||!t&&n.isEdgeLabel)continue;let i=n.x??0,a=n.y??0,o=n.width??0,l=n.height??0;r.push({nodeId:n.id,...p1(i,a,o,l)})}return r}function J3(e,t,r=.001){let n=e.start,i=e.end;if(!n||!i)return;let a=t.get(n),o=t.get(i);if(!(!a||!o))return{srcId:n,dstId:i,srcInfo:a,dstInfo:o,collinearX:Math.abs(a.cx-o.cx)g||px)return!1;let b=Math.abs(y-d.a.x)i:a&&l&&an(e,r,i)?xs(e.x,t.x,r.x,n.x)>i:!1}function yT(e,t,r,n,{epsilon:i=.001,skipDegenerateOther:a=!1}={}){for(let o of r){if(o===n||o.isLayoutOnly)continue;let l=o.points;if(!(!l||l.length<2))for(let u=0;up+i&&gy+i&&fn+.001&&e=2?t[t.length-2]:void 0,u=(o?tn(o,i):!1)?{x:i.x,y:a.y}:{x:a.x,y:i.y};t.push(u)}t.push(a)}let r=[];for(let n of t){let i=r[r.length-1];(!i||!go(i,n))&&r.push(n)}return r}function kl(e){if(e.length<3)return e;let t=[...e];for(let r=0;r<32;r++){let n=Rlt(t);if(t=n.points,!n.changed)break}return t}var fc=F(()=>{"use strict";s(Bye,"measuredNodeRect");s($ye,"nodeBoundsInfoFor");s(go,"samePoint");s(tn,"sameX");s(an,"sameY");s(ki,"isHorizontalSegment");s(Ei,"isVerticalSegment");s(xs,"overlapLength");s(hc,"sameAxisSegmentOverlapLength");s(Df,"orthogonalSegmentsForPoints");s(yo,"countOrthogonalBends");s(rn,"dedupeConsecutivePoints");s(j3,"classifyThreeSegmentRoute");s(gT,"segmentBoundsOverlapRect");s(X3,"pointInsideRect");s(Fye,"rectContainsRect");s(K3,"rectsOverlap");s(iB,"inflateRect");s(p1,"rectFromCenterSize");s(Ba,"rectOfNodeBounds");s(If,"portForRectSide");s(Z3,"buildOrthogonalPortPath");s(Q3,"buildSameSideTrackPath");s(m1,"collectRealNodeBounds");s(yu,"collectNodeRectEntries");s(Gye,"collectLayoutNodeRects");s(J3,"getNodePairGeometry");s(Qn,"segmentHitsAnyRect");s(aB,"orthogonalSegmentsCross");s(Elt,"sameAxisSegmentsOverlap");s(yT,"segmentConflictsWithAnyEdge");s(dc,"orthogonalSegmentsStrictlyCross");s(Oye,"strictlyBetween");s(Alt,"isCollinearIntermediate");s(Rlt,"simplifyPolylineOnce");s(vT,"orthogonalizePolyline");s(kl,"simplifyPolyline")});function Zye(e,t,r){let n=e;if(n.isLayoutOnly||!n.points||n.points.length=0&&i=e.length)return e;let a=i-n;if(a<0||a>=e.length)return e;let o=Llt(e[i],e[a],t);return r?[o,...e.slice(i)]:[...e.slice(0,i+1),o]}function Qye(e,t){for(let r of e){let n=Zye(r,t,2);if(!n)continue;let i=[...n.points];n.srcRect&&(i=Vye(i,n.srcRect,!0)),n.dstRect&&(i=Vye(i,n.dstRect,!1)),i=kl(vT(i)),i=Jye(i,n.srcRect,n.dstRect),n.edge.points=kl(vT(i))}}function Wye(e,t,r,n=!1){if(an(e,t,Dr)){if(t.yr.bottom+Dr)return t;if(n){if(e.xr.right+Dr)return{x:r.right,y:e.y}}return{x:Math.abs(t.x-r.left)<=Math.abs(t.x-r.right)?r.left:r.right,y:e.y}}if(tn(e,t,Dr)){if(t.xr.right+Dr)return t;if(n){if(e.yr.bottom+Dr)return{x:e.x,y:r.bottom}}let i=Math.abs(t.y-r.top)<=Math.abs(t.y-r.bottom);return{x:e.x,y:i?r.top:r.bottom}}return t}function sB(e,t,r){let n=e[t];for(let i=t+r;i>=0&&in.lo)),r=Math.min(...e.map(n=>n.hi));if(!(t>r))return{lo:t,hi:r}}function Hye(e,t){return t==="left"||t==="right"?oB(e.top,e.bottom):oB(e.left,e.right)}function lB(e,t,r){let n=e.y>=r.top-Dr&&e.y<=r.bottom+Dr,i=e.x>=r.left-Dr&&e.x<=r.right+Dr;if(an(e,t,Dr)&&n){if(Math.abs(e.x-r.left)0?Dlt(a):void 0}function Uye(e,t,r,n,i){let a=Ilt(e,t,r,n,i);if(!a)return;let o=i?e.y:e.x,l=Math.min(a.hi,Math.max(a.lo,o));if(!(Math.abs(l-o)({...l}));for(let l=t;l>=0&&l=r.left-Dr&&Math.max(e.x,t.x)<=r.right+Dr,i=Math.min(e.y,t.y)>=r.top-Dr&&Math.max(e.y,t.y)<=r.bottom+Dr;if(Math.abs(e.y-r.top)n.bottom+Dr;case"left":return an(t,r,Dr)&&r.xn.right+Dr}}function Kye(e,t,r){if(e.length<3)return e;if(r){let a=jye(e[0],e[1],t);return a&&Xye(a,e[1],e[2],t)?e.slice(1):e}let n=e.length-1,i=jye(e[n-1],e[n],t);return i&&Xye(i,e[n-1],e[n-2],t)?e.slice(0,n):e}function Plt(e,t,r){let n=e;if(t){let a=sB(n,0,1);if(a){let o=Wye(a,n[0],t);o!==n[0]&&(n=[o,...n.slice(1)])}n=Kye(n,t,!0)}if(r){let a=n.length-1,o=sB(n,a,-1);if(o){let l=Wye(o,n[a],r,!0);l!==n[a]&&(n=[...n.slice(0,a),l])}n=Kye(n,r,!1)}let i=Jye(n,t,r);return i!==n||n.length===2?i:(t&&(n=Yye(n,t,!0)),r&&(n=Yye(n,r,!1)),n)}function cB(e,t){for(let r of e){let n=Zye(r,t,2);if(!n)continue;let i=rn(n.points,Dr),a=Plt(i,n.srcRect,n.dstRect);if(a.length<3){n.edge.points=a;continue}let o=[a[0],{...a[0]},...a.slice(1,-1),a[a.length-1],{...a[a.length-1]}];n.edge.points=o}}var Dr,_lt,zye,e1e=F(()=>{"use strict";fc();Dr=.001,_lt=.5,zye=4;s(Zye,"endpointContextFor");s(Llt,"segmentEnterPoint");s(Vye,"clipEndpoint");s(Qye,"clipEdgeEndpointsToNodeBoundaries");s(Wye,"snapEndpointToBoundary");s(sB,"firstDistinctAdjacent");s(oB,"cornerClearanceRange");s(qye,"clampToCornerClearance");s(Dlt,"intersectRanges");s(Hye,"clearanceRangeForSide");s(lB,"terminalSideForSegment");s(e5,"isHorizontalSide");s(Ilt,"straightClearanceRange");s(Uye,"clearStraightEndpointCornerAxis");s(Jye,"clearStraightEndpointCornerConnections");s(Mlt,"cornerClearedEndpoint");s(Nlt,"moveCollinearEndpointRun");s(Yye,"clearEndpointCornerConnection");s(jye,"borderSideForSegment");s(Xye,"leavesOutward");s(Kye,"collapseOwnBorderStub");s(Plt,"snapAndCollapseEndpoints");s(cB,"prepareEdgeEndpointsForRenderer")});function r1e(e){return new Map(e.map(t=>[t.id,t]))}function Olt(e,t){let r=e.parentId,n=null;for(;r;){let i=t.get(r);if(!i?.isGroup)break;n=i.id,r=i.parentId}return n}function t1e(e,t){let r=0,n=e.parentId;for(;n;){let i=t.get(n);if(!i?.isGroup)break;r++,n=i.parentId}return r}function n1e(e){let t=1/0,r=-1/0,n=1/0,i=-1/0;for(let a of e){let o=a.x,l=a.y;if(typeof o!="number"||typeof l!="number")continue;let u=a.width??0,h=a.height??0;t=Math.min(t,o-u/2),r=Math.max(r,o+u/2),n=Math.min(n,l-h/2),i=Math.max(i,l+h/2)}return t===1/0||n===1/0?null:{minX:t,maxX:r,minY:n,maxY:i}}function Blt(e,t){let r=e.padding??20;e.x=(t.minX+t.maxX)/2,e.y=(t.minY+t.maxY)/2,e.width=Math.max(0,t.maxX-t.minX)+r,e.height=Math.max(0,t.maxY-t.minY)+r}function $lt(e){let t=r1e(e),r=e.filter(n=>n.isGroup&&n.parentId).sort((n,i)=>t1e(i,t)-t1e(n,t));for(let n of r){let i=e.filter(o=>o.parentId===n.id),a=n1e(i);a&&Blt(n,a)}}function uB(e,t){let r=e.nodes??[],n=e.edges??[],i=r.filter(u=>!u.isGroup),a=1/0,o=-1/0;for(let u of i){let h=u[t];typeof h=="number"&&(a=Math.min(a,h),o=Math.max(o,h))}if(!Number.isFinite(a)||!Number.isFinite(o))return!1;let l=s(u=>a+o-u,"mirror");for(let u of r){let h=u[t];typeof h=="number"&&(u[t]=l(h));let d=u.groupTitleRect;d&&(u.groupTitleRect=t==="x"?{...d,left:l(d.right),right:l(d.left)}:{...d,top:l(d.bottom),bottom:l(d.top)})}for(let u of n)for(let h of u.points??[])h[t]=l(h[t]);return!0}function i1e(e){return(e.nodes??[]).some(r=>!r.isGroup)?uB(e,"y"):!0}function a1e(e,t="LR"){let r=e.nodes??[],n=e.edges??[],i=r.filter(A=>!A.isGroup),a=1/0,o=1/0;for(let A of i){let I=A.x??0,D=A.y??0;I0?Math.max(1,d/f):1;for(let A of i){let I=A.x??0,L=((A.y??0)-o)*p+l,B=I-a;A.x=L,A.y=B}for(let A of n)if(A.points)for(let I of A.points){let D=I.x,B=(I.y-o)*p+l,O=D-a;I.x=B,I.y=O}$lt(r);let m=r.filter(A=>A.isGroup&&!A.parentId);if(m.length===0)return t==="RL"&&uB(e,"x"),!0;let g=r1e(r),y=new Map;for(let A of r){if(A.isGroup)continue;let I=Olt(A,g);if(!I)continue;let D=y.get(I)??[];D.push(A),y.set(I,D)}let v=0;for(let A of m){let I=A.padding??0;I>v&&(v=I)}let x=[],b=1/0,T=-1/0;for(let A of m){let I=y.get(A.id)??[],D=n1e(I);D&&(b=Math.min(b,D.minX),T=Math.max(T,D.maxX),x.push({lane:A,contentTop:D.minY,contentBottom:D.maxY,centerY:(D.minY+D.maxY)/2}))}if(b===1/0||T===-1/0)return!0;let w=Math.max(0,T-b),C=Math.max(v,10),S=w+2*C,k=l+S,P=(b+T)/2-S/2-l,N=P+k/2,_=Math.max(v,l);x.sort((A,I)=>A.centerY-I.centerY);for(let A=0;A{"use strict";s(r1e,"buildNodeMap");s(Olt,"resolveTopLevelGroupId");s(t1e,"groupDepth");s(n1e,"boundsForChildren");s(Blt,"applyGroupBounds");s($lt,"recomputeNestedGroupBounds");s(uB,"mirrorAxis");s(i1e,"applyBtDirectionTransform");s(a1e,"applyLrDirectionTransform")});function o1e(e,t){let{nodeInfoById:r,realNodeRects:n}=m1(t);for(let i of e){if(i.isLayoutOnly)continue;let a=i.points;if(!a||a.length<4)continue;let o=j3(rn(a,pc),pc);if(!o)continue;let{p3:l}=o,u=o.kind==="HVH",h=J3(i,r,pc);if(!h)continue;let{srcId:d,dstId:f,srcInfo:p,dstInfo:m,collinearX:g,collinearY:y}=h;if(g||y)continue;let v,x=p.rect;for(let b of Glt){let T,w,C;if(u){let N=m.cy>p.cy?x.bottom:x.top,_=p.cx+b;if(_<=x.left+pc||_>=x.right-pc)continue;T={x:_,y:N},w={x:_,y:l.y},C={x:l.x,y:l.y}}else{let N=m.cx>p.cx?x.right:x.left,_=p.cy+b;if(_<=x.top+pc||_>=x.bottom-pc)continue;T={x:N,y:_},w={x:l.x,y:_},C={x:l.x,y:l.y}}let S=go(T,w,pc),k=go(w,C,pc);if(S&&k||!S&&Qn(T,w,n,[d],1)||!k&&Qn(w,C,n,[f],1))continue;let R=!S&&yT(T,w,e,i,{epsilon:pc,skipDegenerateOther:!0}),M=!k&&yT(w,C,e,i,{epsilon:pc,skipDegenerateOther:!0});if(!(R||M)){S?v=[w,C]:k?v=[T,w]:v=[T,w,C];break}}v&&(i.points=v)}}var pc,Flt,t5,Glt,l1e=F(()=>{"use strict";fc();pc=1e-6,Flt=8,t5=Flt,Glt=[0,t5,-t5,2*t5,-2*t5];s(o1e,"portSwapToLShape")});function c1e(e,t){let{realNodeRects:a,labelNodeRects:o}=yu(t.values());for(let l of e){if(l.isLayoutOnly)continue;let u=l.points;if(!u||u.length<4)continue;let h=rn(u,.001);if(h.length<4)continue;let d=h.length-1,f=h[d],p=h[d-1],m=h[d-2],g=f.x-p.x,y=f.y-p.y,v=Math.hypot(g,y);if(v>=10||v<.001)continue;let x=p.x-m.x,b=p.y-m.y;if(Math.hypot(x,b)<.001)continue;let w=ki(p,f,.001),C=Ei(p,f,.001),S=ki(m,p,.001),k=Ei(m,p,.001);if(!(w&&k||C&&S))continue;let R=l.end,M=l.start,P=R?t.get(R):void 0;if(!P)continue;let N=P.x??0,_=P.y??0,A=Ba(P);if(!A)continue;let I,D;if(k){let z=b<0;I={x:N,y:m.y},D={x:N,y:z?A.bottom:A.top}}else{let z=x>0;I={x:m.x,y:_},D={x:z?A.right:A.left,y:_}}if(Qn(I,D,a,R?[R]:[],-2)||Qn(I,D,o,[],-2))continue;if(M){let z=t.get(M),H=z?Ba(z):void 0;if(H&&X3(I,H,2))continue}let L=s((z,H)=>`${z.x.toFixed(3)},${z.y.toFixed(3)}|${H.x.toFixed(3)},${H.y.toFixed(3)}`,"ownSegmentKey"),B=new Set;for(let z=0;z{for(let K of e){if(K===l||K.isLayoutOnly)continue;let J=K.points;if(!(!J||J.length<2))for(let ne=0;ne=0){let z=h[d-3],H=[M,R].filter(K=>!!K);if(Qn(z,I,a,H,-2)||O(z,I))continue}let W=[...h.slice(0,d-2),I,D];l.points=W;let V=l.labelNodeId;if(V){let z=t.get(V);if(z){let H=z.width??0,K=z.height??0;if(H>0&&K>0){let J,ne,Z=-1;for(let pe=0;pe=H+2||Ee&&ce>=K+2)&&ce>Z&&(Z=ce,J=(te.x+fe.x)/2,ne=(te.y+fe.y)/2)}J!==void 0&&ne!==void 0&&(z.x=J,z.y=ne)}}}}}var u1e=F(()=>{"use strict";fc();s(c1e,"collapseShortTerminalStub")});function h1e(e,t){let i=s((m,g)=>{let y=m.x??0,v=m.y??0,x=g.x-y,b=g.y-v,T=(m.width??0)/2,w=(m.height??0)/2;return Math.abs(b)*T>Math.abs(x)*w?(b<0&&(w=-w),{x:y+(b===0?0:w*x/b),y:v+w}):(x<0&&(T=-T),{x:y+T,y:v+(x===0?0:T*b/x)})},"rectIntersect"),a=s((m,g)=>{let y=rn(m.points??[]);if(y.length<2)return;let v=g?m.start:m.end,x=v?t.get(v):void 0,b=x?Ba(x):void 0;if(!x||!v||!b)return;let T=g?y[0]:y[y.length-1],w=g?y[1]:y[y.length-2],C=i(x,T),S=T;if(hB(w,C)&&(S=w),tn(C,S,ir))return{edge:m,edgeId:String(m.id??""),nodeId:v,atStart:g,orientation:"V",coord:C.x,min:Math.min(C.y,S.y),max:Math.max(C.y,S.y),boundary:C,railEnd:S,rect:b};if(an(C,S,ir))return{edge:m,edgeId:String(m.id??""),nodeId:v,atStart:g,orientation:"H",coord:C.y,min:Math.min(C.x,S.x),max:Math.max(C.x,S.x),boundary:C,railEnd:S,rect:b}},"terminalLaneFor"),o=s((m,g)=>Math.max(0,Math.min(m.max,g.max)-Math.max(m.min,g.min)),"projectedOverlapLength"),l=s((m,g)=>m.nodeId!==g.nodeId||m.orientation!==g.orientation?!1:m.orientation==="H"?(Math.abs(m.boundary.x-m.rect.left)<1||Math.abs(m.boundary.x-m.rect.right)<1)&&tn(m.boundary,g.boundary,1):(Math.abs(m.boundary.y-m.rect.top)<1||Math.abs(m.boundary.y-m.rect.bottom)<1)&&an(m.boundary,g.boundary,1),"sameTerminalFace"),u=s((m,g)=>m.nodeId!==g.nodeId||m.orientation!==g.orientation?!1:o(m,g)>=$a&&Math.abs(m.coord-g.coord)<.5,"exactTerminalLaneConflict"),h=s((m,g)=>{if(m.nodeId!==g.nodeId||m.orientation!==g.orientation||m.orientation!=="H"||m.atStart===g.atStart)return!1;let y=o(m,g);if(y<$a)return!1;let v=m.rect.bottom-m.rect.top;return y2*v?!1:l(m,g)&&Math.abs(m.coord-g.coord)<16},"nearTerminalLaneConflict"),d=s((m,g)=>{let y=rn(m.edge.points??[]);if(y.length<2)return;let v=m.orientation==="V"?{x:m.boundary.x+g,y:m.boundary.y}:{x:m.boundary.x,y:m.boundary.y+g},x=m.orientation==="V"?{x:m.railEnd.x+g,y:m.railEnd.y}:{x:m.railEnd.x,y:m.railEnd.y+g};if(!s(()=>Math.abs(m.boundary.y-m.rect.top)<1||Math.abs(m.boundary.y-m.rect.bottom)<1?an(v,m.boundary,ir)&&v.x>=m.rect.left+1&&v.x<=m.rect.right-1:Math.abs(m.boundary.x-m.rect.left)<1||Math.abs(m.boundary.x-m.rect.right)<1?tn(v,m.boundary,ir)&&v.y>=m.rect.top+1&&v.y<=m.rect.bottom-1:!1,"boundaryStaysOnSameFace")())return;if(m.atStart){let S=y.length>1&&go(y[1],m.railEnd,ir),k=y.slice(S?2:1),R=k[0];return R&&!hB(R,x)?void 0:[v,x,...k]}let T=y.length>1&&go(y[y.length-2],m.railEnd,ir),w=y.slice(0,T?-2:-1),C=w[w.length-1];if(!(C&&!hB(C,x)))return[...w,x,v]},"shiftedCandidate"),f=s(m=>{let g=m.edge,y=rn(g.points??[]);if(y.length!==2)return!1;let v=g.start,x=g.end,b=v?t.get(v):void 0,T=x?t.get(x):void 0;if(!b||!T)return!1;let w=b.x??0,C=b.y??0,S=T.x??0,k=T.y??0,[R,M]=y;return an(R,M,ir)&&Math.abs(C-k)<1&&Math.abs(w-S)>1||tn(R,M,ir)&&Math.abs(w-S)<1&&Math.abs(C-k)>1},"laneIsStraightCollinearConnector"),p=[-7,7,-14,14,-21,21];for(let m=0;m<8;m++){let g=e.filter(v=>!v.isLayoutOnly).flatMap(v=>[a(v,!0),a(v,!1)]).filter(v=>!!v),y=!1;for(let v=0;v{let R=f(S),M=f(k);return R!==M?Number(R)-Number(M):+!k.atStart-+!S.atStart});for(let S of C){for(let k of p){let R=d(S,k);if(!R)continue;let M=a({...S.edge,points:R},S.atStart);if(!(!M||g.some(P=>P.edge!==S.edge&&(u(M,P)||w&&h(M,P))))){S.edge.points=R,y=!0;break}}if(y)break}}if(!y)return}}function d1e(e,t){let{realNodeRects:i,labelNodeRects:a}=yu(t.values()),o=s((u,h)=>{let d=u.start,f=u.end,p=Qr(h);if(p.length!==h.length-1)return!1;let m=[d,f].filter(g=>!!g);for(let g of p)if(Qn(g.a,g.b,i,m,-2)||Qn(g.a,g.b,a,[],-2))return!1;for(let g of e){if(g===u||g.isLayoutOnly)continue;let y=g.points;if(!(!y||y.length<2)){for(let v of p)for(let x of Qr(rn(y)))if(hc(v,x,.5)>=$a||dc(v.a,v.b,x.a,x.b,ir))return!1}}return!0},"candidateIsSafe"),l=s((u,h)=>{if(h+4>=u.length)return;let d=u[h],f=u[h+1],p=u[h+2],m=u[h+3],g=u[h+4],y=ki(d,f)&&Ei(f,p)&&ki(p,m)&&Ei(m,g)&&tn(d,m,ir)&&tn(d,g,ir)&&tn(f,p,ir)&&(f.x-d.x)*(m.x-p.x)<0,v=Ei(d,f)&&ki(f,p)&&Ei(p,m)&&ki(m,g)&&an(d,m,ir)&&an(d,g,ir)&&an(f,p,ir)&&(f.y-d.y)*(m.y-p.y)<0;if(y||v)return rn([...u.slice(0,h+1),g,...u.slice(h+5)]);if(h+5>=u.length)return;let x=u[h+5],b=Ei(d,f)&&ki(f,p)&&Ei(p,m)&&ki(m,g)&&Ei(g,x)&&tn(d,g,ir)&&tn(d,x,ir)&&tn(p,m,ir)&&(p.x-f.x)*(g.x-m.x)<0,T=ki(d,f)&&Ei(f,p)&&ki(p,m)&&Ei(m,g)&&ki(g,x)&&an(d,g,ir)&&an(d,x,ir)&&an(p,m,ir)&&(p.y-f.y)*(g.y-m.y)<0;if(!(!b&&!T))return rn([...u.slice(0,h+1),x,...u.slice(h+6)])},"withoutDogleg");for(let u=0;u<8;u++){let h=!1;for(let d of e){if(d.isLayoutOnly)continue;let f=rn(d.points??[]);for(let p=0;p<=f.length-5;p++){let m=l(f,p);if(!(!m||!o(d,m))){d.points=m,h=!0;break}}if(h)break}if(!h)return}}function dB(e,t){let{realNodeRects:a,labelNodeRects:o}=yu(t.values()),l=e.filter(g=>!g.isLayoutOnly),u=s((g,y,v)=>rn(g===y?v??[]:g.points??[]),"pointsFor"),h=s((g,y)=>{let v=0;for(let x=0;x{let y=Qr(g);if(y.length!==3)return;let v=y[1];if(!(y[0].horizontal===v.horizontal||y[2].horizontal===v.horizontal))return{index:v.index,horizontal:v.horizontal,vertical:v.vertical,segment:v}},"middleRail"),f=s((g,y)=>{let v=[g.start,g.end].filter(x=>!!x);return a.filter(x=>{if(v.includes(x.id))return!1;let b=x.rect;return y.horizontal?xs(y.a.x,y.b.x,b.left,b.right)>=$a&&y.a.y>=b.top-2&&y.a.y<=b.bottom+2:xs(y.a.y,y.b.y,b.top,b.bottom)>=$a&&y.a.x>=b.left-2&&y.a.x<=b.right+2})},"blockingRectsFor"),p=s((g,y,v)=>{let x=g.map(T=>({...T}));if(y.horizontal)x[y.index].y=v,x[y.index+1].y=v;else if(y.vertical)x[y.index].x=v,x[y.index+1].x=v;else return;let b=kl(rn(x));return Qr(b).length===b.length-1?b:void 0},"candidateByMovingRail"),m=s((g,y,v)=>{let x=[g.start,g.end].filter(T=>!!T),b=Qr(y);if(b.length!==y.length-1)return!1;for(let T of b)if(Qn(T.a,T.b,a,x,-2)||Qn(T.a,T.b,o,[],-2))return!1;for(let T of l)if(T!==g){for(let w of b)for(let C of Qr(u(T)))if(hc(w,C,.5)>=$a)return!1}return h(g,y)<=v},"candidateIsSafe");for(let g=0;g<8;g++){let y=h(),v=!1;for(let x of l){let b=u(x),T=d(b);if(!T)continue;let w=f(x,T.segment);if(w.length===0)continue;let C=T.horizontal?[Math.min(...w.map(S=>S.rect.top))-20,Math.max(...w.map(S=>S.rect.bottom))+20]:[Math.min(...w.map(S=>S.rect.left))-20,Math.max(...w.map(S=>S.rect.right))+20];for(let S of C){let k=p(b,T.segment,S);if(!(!k||!m(x,k,y))){x.points=k,v=!0;break}}if(v)break}if(!v)return}}function fB(e,t){let n=s(u=>{let h=u.groupTitleRect;if(!(!h||typeof h.left!="number"||typeof h.right!="number"||typeof h.top!="number"||typeof h.bottom!="number"||!Number.isFinite(h.left)||!Number.isFinite(h.right)||!Number.isFinite(h.top)||!Number.isFinite(h.bottom)||h.right<=h.left||h.bottom<=h.top))return{left:h.left,right:h.right,top:h.top,bottom:h.bottom}},"validTitleRect"),i=s(u=>{if(!u.isGroup||u.parentId)return;let h=u.direction,d=typeof h=="string"?h.toUpperCase():"";if(d==="LR"||d==="RL"||d==="BT")return;let f=n(u),p=u.y,m=u.height;if(!f||typeof p!="number"||typeof m!="number"||!Number.isFinite(p)||!Number.isFinite(m)||m<=0)return;let g=f.right-f.left,y=f.bottom-f.top;if(!(y<=0||g{if(!u.horizontal)return!1;let d=u.a.y;return d<=h.top+ir||d>=h.bottom-ir?!1:xs(u.a.x,u.b.x,h.left,h.right)>=$a},"horizontalSegmentIntersectsTitle"),o=[...t.values()].map(i).filter(u=>!!u);if(o.length===0)return;let l=0;for(let u of e){if(u.isLayoutOnly)continue;let h=rn(u.points??[]);for(let d of Qr(h))for(let f of o)a(d,f.rect)&&(l=Math.max(l,f.rect.bottom-d.a.y+4))}if(!(l<=ir))for(let u of o){let h=u.node.y,d=u.node.height;typeof h!="number"||typeof d!="number"||!Number.isFinite(h)||!Number.isFinite(d)||d<=0||(u.node.y=h-l/2,u.node.height=d+l,u.node.groupTitleRect={...u.rect,top:u.rect.top-l,bottom:u.rect.bottom-l})}}function pB(e,t){let n=s(h=>{let d=h.groupTitleRect;if(!(!d||typeof d.left!="number"||typeof d.right!="number"||typeof d.top!="number"||typeof d.bottom!="number"||!Number.isFinite(d.left)||!Number.isFinite(d.right)||!Number.isFinite(d.top)||!Number.isFinite(d.bottom)||d.right<=d.left||d.bottom<=d.top))return{left:d.left,right:d.right,top:d.top,bottom:d.bottom}},"validTitleRect"),i=s(h=>{if(!h.isGroup||h.parentId||h.direction!=="LR")return;let f=n(h),p=h.x,m=h.width;if(!f||typeof p!="number"||typeof m!="number"||!Number.isFinite(p)||!Number.isFinite(m)||m<=0)return;let g=f.right-f.left,y=f.bottom-f.top;if(!(g<=0||y{if(!h.vertical)return!1;let f=h.a.x;return f<=d.left+ir||f>=d.right-ir?!1:xs(h.a.y,h.b.y,d.top,d.bottom)>=$a},"verticalSegmentIntersectsTitle"),o=s((h,d)=>{if(!h.horizontal)return!1;let f=h.a.y;return f<=d.top+ir||f>=d.bottom-ir?!1:xs(h.a.x,h.b.x,d.left,d.right)>=$a},"horizontalSegmentIntersectsTitle"),l=[...t.values()].map(i).filter(h=>!!h);if(l.length===0)return;let u=0;for(let h of e){if(h.isLayoutOnly)continue;let d=rn(h.points??[]);for(let f of Qr(d))for(let p of l)if(a(f,p.rect))u=Math.max(u,p.rect.right-f.a.x+4);else if(o(f,p.rect)){let m=Math.min(f.a.x,f.b.x);u=Math.max(u,p.rect.right-m+4)}}if(!(u<=ir))for(let h of l){let d=h.node.x,f=h.node.width;typeof d!="number"||typeof f!="number"||!Number.isFinite(d)||!Number.isFinite(f)||f<=0||(h.node.x=d-u/2,h.node.width=f+u,h.node.groupTitleRect={...h.rect,left:h.rect.left-u,right:h.rect.right-u})}}function f1e(e,t){let{realNodeRects:i}=yu(t.values()),a=e.filter(y=>!y.isLayoutOnly),o=s((y,v=new Map)=>rn(v.get(y)??y.points??[]),"replacementPointsFor"),l=s((y=new Map)=>{let v=0;for(let x=0;xa.reduce((v,x)=>v+yo(o(x,y)),0),"totalBends"),h=s(y=>{let v=o(y);if(v.length<4)return;let x=v[v.length-2],b=v[v.length-1];if(!(!ki(x,b,ir)&&!Ei(x,b,ir)))return{tailStart:x,terminal:b}},"terminalTailFor"),d=s((y,v)=>{let x=o(y);if(x.length<3)return;let b=x[0],T=x[1],w;if(ki(b,T,ir))w={x:T.x,y:v.tailStart.y};else if(Ei(b,T,ir))w={x:v.tailStart.x,y:T.y};else return;let C=kl(rn([b,T,w,v.tailStart,v.terminal]));return Qr(C).length===C.length-1?C:void 0},"candidateWithDestinationTail"),f=s((y,v)=>{let x=[y.start,y.end].filter(b=>!!b);for(let b of Qr(v))if(Qn(b.a,b.b,i,x,-2))return!0;return!1},"pathHasNodeHit"),p=s((y,v,x)=>{for(let b of a)if(b!==y){for(let T of Qr(v))for(let w of Qr(o(b,x)))if(hc(T,w,.5)>=$a)return!0}return!1},"pathHasSharedTrack"),m=s((y,v,x)=>!f(y,v)&&!p(y,v,x),"candidateIsSafe"),g=s(()=>{let y=new Map;for(let v of a){let x=v.end;if(!x||!t.has(x)||o(v).length<4)continue;let T=y.get(x)??[];T.push(v),y.set(x,T)}return y},"edgesByDestination");for(let y=0;y<4;y++){let v=l();if(v===0)return;let x=u(),b,T=v,w=x;for(let C of g().values())for(let S=0;S=v||D>T||D===T&&L>=w||(b=I,T=D,w=L)}if(!b)return;for(let[C,S]of b)C.points=S}}function p1e(e,t){let{realNodeRects:o,labelNodeRects:l}=yu(t.values()),u=e.filter(C=>!C.isLayoutOnly),h=s((C,S=new Map)=>rn(S.get(C)??C.points??[]),"replacementPointsFor"),d=s((C=new Map)=>{let S=0;for(let k=0;ku.reduce((S,k)=>S+yo(h(k,C)),0),"totalBends"),p=s(C=>{let S=C.start,k=C.end,R=S?t.get(S):void 0,M=k?t.get(k):void 0,P=R?Ba(R):void 0,N=M?Ba(M):void 0;return P&&N?{src:P,dst:N}:void 0},"endpointRectsFor"),m=s((C,S,k)=>{if(k.index<=0||k.index+1>=S.length-1)return;let R=p(C);if(R){if(k.vertical){let M=k.a.x,P=Math.min(R.src.left,R.dst.left),N=Math.max(R.src.right,R.dst.right),_=MN+ir?"right":void 0;return _?{edge:C,points:S,segmentIndex:k.index,axis:"vertical",side:_,coord:M,min:Math.min(k.a.y,k.b.y),max:Math.max(k.a.y,k.b.y)}:void 0}if(k.horizontal){let M=k.a.y,P=Math.min(R.src.top,R.dst.top),N=Math.max(R.src.bottom,R.dst.bottom),_=MN+ir?"bottom":void 0;return _?{edge:C,points:S,segmentIndex:k.index,axis:"horizontal",side:_,coord:M,min:Math.min(k.a.x,k.b.x),max:Math.max(k.a.x,k.b.x)}:void 0}}},"externalRailForSegment"),g=s(()=>{let C=[];for(let S of u){let k=h(S);for(let R of Qr(k)){let M=m(S,k,R);M&&C.push(M)}}return C},"collectExternalRails"),y=s((C,S)=>C.edge!==S.edge&&C.axis===S.axis&&C.side===S.side&&xs(C.min,C.max,S.min,S.max)>=$a,"railsInteract"),v=s(C=>{let S=[],k=new Set;for(let R of C){if(k.has(R))continue;let M=[R],P=[];for(k.add(R);M.length>0;){let N=M.pop();P.push(N);for(let _ of C)!k.has(_)&&y(N,_)&&(k.add(_),M.push(_))}P.length>1&&S.push(P)}return S},"connectedComponents"),x=s(C=>{let S=[];for(let k of C)S.some(R=>Math.abs(R-k.coord){let S=C.map(M=>M.coord),k=x(C),R=[];if(C.length<=6){let M=new Array(k.length).fill(!1),P=[],N=s(()=>{if(P.length===C.length){P.some((_,A)=>Math.abs(_-S[A])>=ir)&&R.push([...P]);return}for(let[_,A]of k.entries())M[_]||(M[_]=!0,P.push(A),N(),P.pop(),M[_]=!1)},"visit");return N(),R}for(let M=0;M{let k=new Map;for(let[M,P]of C.entries()){let N=S[M],_=k.get(P.edge)??P.points.map(A=>({x:A.x,y:A.y}));P.axis==="vertical"?(_[P.segmentIndex].x=N,_[P.segmentIndex+1].x=N):(_[P.segmentIndex].y=N,_[P.segmentIndex+1].y=N),k.set(P.edge,_)}let R=new Map;for(let[M,P]of k){let N=kl(rn(P));if(Qr(N).length!==N.length-1)return;R.set(M,N)}return R},"replacementsForAssignment"),w=s(C=>{for(let[S,k]of C){let R=[S.start,S.end].filter(M=>!!M);for(let M of Qr(k))if(Qn(M.a,M.b,o,R,-2)||Qn(M.a,M.b,l,[],-2))return!1}for(let S=0;S=$a)return!1}}return!0},"candidateIsSafe");for(let C=0;C<4;C++){let S=d();if(S===0)return;let k,R=S,M=f(),P=Number.POSITIVE_INFINITY;for(let N of v(g()))for(let _ of b(N)){let A=T(N,_);if(!A||!w(A))continue;let I=d(A);if(I>=S)continue;let D=f(A),L=N.reduce((B,O,$)=>B+Math.abs(_[$]-O.coord),0);I>R||I===R&&(D>M||D===M&&L>=P)||(k=A,R=I,M=D,P=L)}if(!k)return;for(let[N,_]of k)N.points=_}}function m1e(e,t){let{realNodeRects:i,labelNodeRects:a}=yu(t.values()),o=e.filter(g=>!g.isLayoutOnly),l=s((g,y,v)=>rn(g===y?v??[]:g.points??[]),"pointsFor"),u=s(g=>Qr(g).reduce((y,v)=>{let x=v.a.x-v.b.x,b=v.a.y-v.b.y;return y+Math.hypot(x,b)},0),"pathLength"),h=s((g,y)=>{let v=0;for(let x=0;x{if(g.horizontal){let v=g.a.y;return(Math.abs(v-y.top)<1||Math.abs(v-y.bottom)<1)&&xs(g.a.x,g.b.x,y.left,y.right)>=$a}if(g.vertical){let v=g.a.x;return(Math.abs(v-y.left)<1||Math.abs(v-y.right)<1)&&xs(g.a.y,g.b.y,y.top,y.bottom)>=$a}return!1},"segmentRunsAlongRectBorder"),f=s(g=>{let y=[g.start,g.end].filter(x=>!!x),v=[];for(let x of y){let b=t.get(x),T=b?Ba(b):void 0;T&&v.push(T)}return v},"endpointRectsFor"),p=s((g,y)=>{if(y+3>=g.length)return[];let v=g[y],x=g[y+1],b=g[y+2],T=g[y+3],w=ki(v,x,ir)&&Ei(x,b,ir)&&ki(b,T,ir),C=Ei(v,x,ir)&&ki(x,b,ir)&&Ei(b,T,ir);if(!w&&!C)return[];if(!(w?Math.sign(x.x-v.x)!==Math.sign(T.x-b.x):Math.sign(x.y-v.y)!==Math.sign(T.y-b.y)))return[];let k=tn(v,T,ir)||an(v,T,ir)?[]:[{x:v.x,y:T.y},{x:T.x,y:v.y}],R=k.length===0?[[...g.slice(0,y+1),...g.slice(y+3)]]:k.map(P=>[...g.slice(0,y+1),P,...g.slice(y+3)]),M=new Set;return R.map(P=>kl(rn(P))).filter(P=>{if(Qr(P).length!==P.length-1||!P.some(_=>go(_,T,ir)))return!1;let N=P.map(_=>`${_.x.toFixed(3)},${_.y.toFixed(3)}`).join("|");return M.has(N)?!1:(M.add(N),!0)})},"shortcutCandidatesAt"),m=s((g,y,v)=>{let x=[g.start,g.end].filter(T=>!!T),b=f(g);for(let T of Qr(y))if(Qn(T.a,T.b,i,x,-2)||Qn(T.a,T.b,a,[],-2)||b.some(w=>d(T,w)))return!1;for(let T of o)if(T!==g){for(let w of Qr(y))for(let C of Qr(l(T)))if(hc(w,C,.5)>=$a)return!1}return h(g,y)<=v},"candidateIsSafe");for(let g=0;g<8;g++){let y=h(),v,x,b=y,T=Number.POSITIVE_INFINITY,w=Number.POSITIVE_INFINITY;for(let C of o){let S=l(C),k=yo(S,ir),R=u(S);for(let M=0;M<=S.length-4;M++)for(let P of p(S,M)){let N=yo(P,ir),_=u(P);if(!(Nb||I===b&&(N>T||N===T&&_>=w)||(v=C,x=P,b=I,T=N,w=_)}}if(!v||!x)return;v.points=x}}function g1e(e,t){let o=[];for(let ye of t.values()){if(ye.isGroup||ye.isEdgeLabel)continue;let Be=ye.x??0,Ie=ye.y??0,Ne=Ba(ye);Ne&&o.push({id:String(ye.id??""),cx:Be,cy:Ie,rect:Ne})}if(o.length===0)return;let l=new Map(o.map(ye=>[ye.id,ye])),u=o.map(ye=>({id:ye.id,rect:ye.rect})),h=["top","bottom","left","right"],d={top:Math.min(...o.map(ye=>ye.rect.top))-20,bottom:Math.max(...o.map(ye=>ye.rect.bottom))+20,left:Math.min(...o.map(ye=>ye.rect.left))-20,right:Math.max(...o.map(ye=>ye.rect.right))+20},f=e.filter(ye=>!ye.isLayoutOnly),p=new Map(f.map((ye,Be)=>[ye,Be])),m=s(ye=>{let Be=ye==="left"||ye==="top"?-1:1,Ie=[];for(let Ne=0;Ne<=2;Ne++)Ie.push(d[ye]+Be*20*Ne);return Ie},"outwardTracksForSide"),g=s((ye,Be=new Map)=>rn(Be.get(ye)??ye.points??[]),"replacementPointsFor"),y=s((ye,Be)=>{let Ie=0;for(let Ne of ye)for(let re of Be)dc(Ne.a,Ne.b,re.a,re.b,ir)&&Ie++;return Ie},"crossingCountBetweenSegments"),v=s((ye,Be)=>y(Qr(ye),Qr(Be)),"crossingCountBetweenPaths"),x=s((ye=new Map)=>{let Be=0,Ie=[],Ne=new Set,re=[],ue=s(le=>{Ne.has(le)||(Ne.add(le),re.push(le))},"addEdge");for(let le=0;le0&&(Be+=X,Ie.push({first:he,second:G,count:X}),ue(he),ue(G))}}return re.sort((le,he)=>(p.get(le)??0)-(p.get(he)??0)),{count:Be,pairs:Ie,edgeSet:Ne,edges:re}},"crossingSnapshot"),b=s((ye,Be)=>{let Ie=new Set(Be.keys());if(Ie.size===0)return ye.count;let Ne=0;for(let ue of ye.pairs)(Ie.has(ue.first)||Ie.has(ue.second))&&(Ne+=ue.count);let re=0;for(let ue=0;ue{let Be=new Map;for(let re of ye.pairs){let ue=Be.get(re.first)??new Set;ue.add(re.second),Be.set(re.first,ue);let le=Be.get(re.second)??new Set;le.add(re.first),Be.set(re.second,le)}let Ie=[],Ne=new Set;for(let re of ye.edges){if(Ne.has(re))continue;let ue=[re],le=[];for(Ne.add(re);ue.length>0;){let he=ue.pop();le.push(he);for(let Te of Be.get(he)??[])Ne.has(Te)||(Ne.add(Te),ue.push(Te))}le.sort((he,Te)=>(p.get(he)??0)-(p.get(Te)??0)),le.length>1&&Ie.push(le)}return Ie},"crossingComponents"),w=s(ye=>[ye.start,ye.end].filter(Be=>!!Be),"endpointIdsFor"),C=s(ye=>{let Be=[];for(let Ie of T(ye)){let Ne=new Set(Ie),re=new Set(Ie.flatMap(le=>w(le))),ue=[...Ie];for(let le of f)Ne.has(le)||w(le).some(he=>re.has(he))&&ue.push(le);ue.sort((le,he)=>(p.get(le)??0)-(p.get(he)??0)),Be.push(ue)}return Be},"pairSearchGroups"),S=s((ye,Be,Ie)=>b(ye,new Map([[Be,Ie]])),"crossingCountWithSingleReplacement"),k=s(ye=>{let Be=new Map;for(let Ie of ye.pairs)Be.set(Ie.first,(Be.get(Ie.first)??0)+Ie.count),Be.set(Ie.second,(Be.get(Ie.second)??0)+Ie.count);return Be},"currentCrossingsByEdge"),R=s(ye=>ye.slice(1).reduce((Be,Ie,Ne)=>{let re=ye[Ne];return Be+Math.abs(Ie.x-re.x)+Math.abs(Ie.y-re.y)},0),"pathLength"),M=s((ye=new Map)=>f.reduce((Be,Ie)=>Be+yo(g(Ie,ye)),0),"totalBends"),P=s((ye=new Map)=>f.reduce((Be,Ie)=>Be+R(g(Ie,ye)),0),"totalLength"),N=s((ye,Be,Ie=new Map)=>{let Ne=Qr(Be);for(let re of f)if(re!==ye){for(let ue of Ne)for(let le of Qr(g(re,Ie)))if(hc(ue,le,.5)>=$a)return!0}return!1},"pathHasSegmentConflict"),_=s((ye,Be)=>{let Ie=[ye.start,ye.end].filter(Ne=>!!Ne);for(let Ne of Qr(Be))if(Qn(Ne.a,Ne.b,u,Ie,-2))return!0;return!1},"pathHitsNode"),A=s((ye,Be)=>{let Ie=kl(rn(Be));Qr(Ie).length===Ie.length-1&&ye.push(Ie)},"pushOrthogonalCandidate"),I=s(ye=>ye==="left"||ye==="right","sideIsHorizontal"),D=s((ye,Be,Ie)=>{switch(Be){case"left":return Math.min(ye.x,Ie.x)-20;case"right":return Math.max(ye.x,Ie.x)+20;case"top":return Math.min(ye.y,Ie.y)-20;case"bottom":return Math.max(ye.y,Ie.y)+20}},"localTrackForSameSide"),L=s((ye,Be,Ie,Ne)=>{let re=Ie==="left"||Ie==="top"?-1:1,ue=[D(Be,Ie,Ne),d[Ie]];for(let le of ue)for(let he=0;he<=2;he++)A(ye,Q3(Be,Ie,Ne,le+re*20*he))},"addSameSideCandidates"),B=s((ye,Be,Ie,Ne,re)=>{for(let ue of m(Ie))for(let le of m(re))A(ye,[Be,{x:ue,y:Be.y},{x:ue,y:le},{x:Ne.x,y:le},Ne])},"addHorizontalToVerticalCandidates"),O=s((ye,Be,Ie,Ne,re)=>{for(let ue of m(Ie))for(let le of m(re))A(ye,[Be,{x:Be.x,y:ue},{x:le,y:ue},{x:le,y:Ne.y},Ne])},"addVerticalToHorizontalCandidates"),$=s((ye,Be,Ie,Ne,re)=>{let ue=[...m("top"),...m("bottom")];for(let le of m(Ie))for(let he of m(re))for(let Te of ue)A(ye,[Be,{x:le,y:Be.y},{x:le,y:Te},{x:he,y:Te},{x:he,y:Ne.y},Ne])},"addHorizontalPairCandidates"),W=s((ye,Be,Ie,Ne,re)=>{let ue=[...m("left"),...m("right")];for(let le of m(Ie))for(let he of m(re))for(let Te of ue)A(ye,[Be,{x:Be.x,y:le},{x:Te,y:le},{x:Te,y:he},{x:Ne.x,y:he},Ne])},"addVerticalPairCandidates"),V=s(ye=>{let Be=new Set;return ye.map(Ie=>rn(Ie)).filter(Ie=>{let Ne=Ie.map(re=>`${re.x.toFixed(3)},${re.y.toFixed(3)}`).join("|");return Be.has(Ne)||Ie.length<2?!1:(Be.add(Ne),!0)})},"dedupeCandidatePaths"),z=s((ye,Be,Ie,Ne)=>{let re=[],ue=Z3(ye,Be,Ie,Ne,20,ir);ue&&A(re,ue),Be===Ne&&L(re,ye,Be,Ie);let le=I(Be),he=I(Ne);return le&&!he?B(re,ye,Be,Ie,Ne):!le&&he?O(re,ye,Be,Ie,Ne):le?$(re,ye,Be,Ie,Ne):W(re,ye,Be,Ie,Ne),V(re)},"buildCandidatesForSides"),H=s((ye,Be,Ie,Ne)=>{let re=[...m("left"),...m("right")],ue=[...m("top"),...m("bottom")];for(let le of h){let he=If(Ne,le),Te=le==="top"||le==="bottom"?m(le):ue;for(let oe of re){A(ye,[Be,Ie,{x:oe,y:Ie.y},{x:oe,y:he.y},he]);for(let G of Te)A(ye,[Be,Ie,{x:oe,y:Ie.y},{x:oe,y:G},{x:he.x,y:G},he])}}},"addVerticalDepartureOuterTrackCandidates"),K=s((ye,Be,Ie,Ne)=>{let re=[...m("left"),...m("right")],ue=[...m("top"),...m("bottom")];for(let le of h){let he=If(Ne,le),Te=le==="left"||le==="right"?m(le):re;for(let oe of ue){A(ye,[Be,Ie,{x:Ie.x,y:oe},{x:he.x,y:oe},he]);for(let G of Te)A(ye,[Be,Ie,{x:Ie.x,y:oe},{x:G,y:oe},{x:G,y:he.y},he])}}},"addHorizontalDepartureOuterTrackCandidates"),J=s(ye=>{let Be=ye.start,Ie=ye.end,Ne=Ie?l.get(Ie):void 0;if(!Be||!Ne)return[];let re=rn(ye.points??[]);if(re.length<4)return[];let ue=re[0],le=re[1],he=[];return Ei(ue,le,ir)?H(he,ue,le,Ne):ki(ue,le,ir)&&K(he,ue,le,Ne),he},"terminalPreservingOuterTrackCandidates"),ne=s(ye=>{let Be=ye.start,Ie=ye.end,Ne=Be?l.get(Be):void 0,re=Ie?l.get(Ie):void 0;if(!Ne||!re)return[];let ue=[];for(let le of h){let he=If(Ne,le);for(let Te of h)ue.push(...z(he,le,If(re,Te),Te))}return ue.push(...J(ye)),ue},"candidatePathsFor"),Z=s(()=>new Map(f.map(ye=>[ye,Qr(g(ye))])),"currentSegmentsByEdge"),pe=s((ye,Be,Ie)=>{let Ne=new Set;for(let re of f){if(re===ye)continue;let ue=Ie.get(re)??Qr(g(re));Be.some(le=>ue.some(he=>hc(le,he,.5)>=$a))&&Ne.add(re)}return Ne},"sharedTrackConflictsFor"),te=s((ye,Be,Ie,Ne)=>{let re=new Set;return ne(ye).map(le=>kl(rn(le))).filter(le=>{if(_(ye,le))return!1;let he=le.map(Te=>`${Te.x.toFixed(3)},${Te.y.toFixed(3)}`).join("|");return re.has(he)||le.length<2?!1:(re.add(he),!0)}).map(le=>{let he=Qr(le),Te=0;for(let oe of f)oe!==ye&&(Te+=y(he,Ie.get(oe)??Qr(g(oe))));return{candidate:le,candidateSegments:he,crossings:Be.count-(Ne.get(ye)??0)+Te,bends:yo(le,ir),totalBends:yo(le),length:R(le)}}).filter(({crossings:le})=>le<=Be.count).sort((le,he)=>le.crossings-he.crossings||le.bends-he.bends||le.length-he.length).slice(0,48).map(le=>({path:le.candidate,segments:le.candidateSegments,sharedTrackConflicts:pe(ye,le.candidateSegments,Ie),totalBends:le.totalBends,length:le.length}))},"pairCandidatesFor"),fe=s((ye,Be,Ie,Ne,re,ue)=>{let le=0;for(let Te of ye.pairs)(Te.first===Be||Te.second===Be||Te.first===Ne||Te.second===Ne)&&(le+=Te.count);let he=y(Ie.segments,re.segments);for(let Te of f){if(Te===Be||Te===Ne)continue;let oe=ue.get(Te)??Qr(g(Te));he+=y(Ie.segments,oe)+y(re.segments,oe)}return ye.count-le+he},"pairCrossingCount"),ce=s((ye,Be)=>{for(let Ie of ye.sharedTrackConflicts)if(Ie!==Be)return!1;return!0},"conflictsOnlyWith"),de=s((ye,Be)=>ye.segments.some(Ie=>Be.segments.some(Ne=>hc(Ie,Ne,.5)>=$a)),"candidatesShareTrack"),Ee=s((ye,Be,Ie,Ne)=>ce(Be,Ie.edge)&&ce(Ne,ye.edge)&&!de(Be,Ne),"pairCandidatesAreCompatible"),we=s((ye,Be,Ie,Ne,re)=>{let ue=fe(ye.current,Be.edge,Ie,Ne.edge,re,ye.baseSegments);if(!(ue>=ye.current.count))return{replacements:new Map([[Be.edge,Ie.path],[Ne.edge,re.path]]),crossings:ue,bends:ye.currentBends-(ye.baseBendsByEdge.get(Be.edge)??0)-(ye.baseBendsByEdge.get(Ne.edge)??0)+Ie.totalBends+re.totalBends,length:ye.currentLength-(ye.baseLengthByEdge.get(Be.edge)??0)-(ye.baseLengthByEdge.get(Ne.edge)??0)+Ie.length+re.length}},"scorePairReplacement"),Fe=s((ye,Be)=>ye.crossings{let re=Ne;for(let ue of Be.candidates)for(let le of Ie.candidates){if(!Ee(Be,ue,Ie,le))continue;let he=we(ye,Be,ue,Ie,le);he&&Fe(he,re)&&(re=he)}return re},"bestScoreForOptionPair"),He=s(ye=>{let Be=M(),Ie=P(),Ne=Z(),re=k(ye),ue=new Map(f.map(X=>[X,yo(g(X))])),le=new Map(f.map(X=>[X,R(g(X))])),he=new Map,Te=C(ye);for(let X of Te)for(let ee of X){if(he.has(ee))continue;let q=te(ee,ye,Ne,re);q.length>0&&he.set(ee,{edge:ee,candidates:q})}let oe={replacements:new Map,crossings:ye.count,bends:Be,length:Ie},G={current:ye,currentBends:Be,currentLength:Ie,baseBendsByEdge:ue,baseLengthByEdge:le,baseSegments:Ne};for(let X of Te){let ee=new Set(X.filter(ie=>ye.edgeSet.has(ie))),q=X.map(ie=>he.get(ie)).filter(ie=>!!ie);for(let ie=0;ie0?oe.replacements:void 0},"bestPairedReplacement");for(let ye=0;ye<4;ye++){let Be=x(),Ie=Be.count;if(Ie===0)return;let Ne,re,ue=Ie,le=Number.POSITIVE_INFINITY;for(let Te of Be.edges){let oe=yo(g(Te),ir);for(let G of ne(Te)){let X=_(Te,G),ee=!X&&N(Te,G),q=S(Be,Te,G),ie=yo(G,ir);X||ee||!(que||q===ue&&ie>=le||(Ne=Te,re=G,ue=q,le=ie)}}if(Ne&&re){Ne.points=re;continue}let he=He(Be);if(!he)return;for(let[Te,oe]of he)Te.points=oe}}var ir,$a,Qr,hB,y1e=F(()=>{"use strict";fc();ir=.001,$a=8,Qr=Df,hB=s((e,t)=>tn(e,t,ir)||an(e,t,ir),"orthogonallyAligned");s(h1e,"separateSharedRenderedTerminalLanes");s(d1e,"collapseRedundantRectangularDoglegs");s(dB,"liftObstacleHuggingSameSideRails");s(fB,"liftTopLaneTitleBandsAboveRails");s(pB,"shiftLeftLaneTitleBandsLeftOfRails");s(f1e,"swapDestinationTerminalTailsToReduceCrossings");s(p1e,"reassignCrossingExternalRailChannels");s(m1e,"shortcutRedundantOrthogonalJogs");s(g1e,"resolveRenderedOrthogonalCrossings")});function v1e(e,t){let{nodeInfoById:r,realNodeRects:n}=m1(t),i=["top","bottom","left","right"],a=20,o={top:Math.min(...n.map(y=>y.rect.top))-a,bottom:Math.max(...n.map(y=>y.rect.bottom))+a,left:Math.min(...n.map(y=>y.rect.left))-a,right:Math.max(...n.map(y=>y.rect.right))+a},l=s((y,v,x,b)=>{let T=[],w=Z3(y,v,x,b,a,Mf);return w&&T.push(w),v===b&&T.push(Q3(y,v,x,o[v])),T},"buildOrthogonalPathCandidates"),u=s((y,v)=>{for(let x=0;x{let b=0,T=Df(y,Mf),w=v.start,C=v.end;for(let S of e){if(S===v||S.isLayoutOnly)continue;let k=S.start,R=S.end;if(!x&&w&&C&&(k===w||k===C||R===w||R===C))continue;let M=S.points;if(!(!M||M.length<2))for(let P of T)for(let N of Df(M,Mf)){if(aB(P.a,P.b,N.a,N.b,Mf,Mf)){b++;continue}hc(P,N,Mf)>=zlt&&b++}}return b},"pathConflictCount"),d=4,f=s((y,v)=>{let x=Math.abs(y.y-v.rect.top),b=Math.abs(y.y-v.rect.bottom),T=Math.abs(y.x-v.rect.left),w=Math.abs(y.x-v.rect.right),C="top",S=x;return b{let b=p.get(y)??[];b.push({side:v,edgeId:x}),p.set(y,b)},"addFaceClaim");for(let y of e){if(y.isLayoutOnly)continue;let v=y.points??[];if(v.length<1)continue;let x=y.id??"",b=y.start,T=y.end;if(b){let w=r.get(b);w&&m(b,f(v[0],w),x)}if(T){let w=r.get(T);w&&m(T,f(v[v.length-1],w),x)}}let g=s((y,v,x)=>p.get(y)?.some(b=>b.edgeId!==x&&b.side===v)??!1,"faceIsClaimed");for(let y of e){if(y.isLayoutOnly)continue;let v=y.points;if(!v||v.length<2)continue;let x=yo(v,Mf);if(x0){let O=h(L,y,!0);if(O>P||O===P&&B>=N)continue;P=O,N=B,M=L;continue}h(L,y)>R||BI.edgeId!==S));let A=p.get(T);A&&p.set(T,A.filter(I=>I.edgeId!==S)),m(b,f(M[0],w),S),m(T,f(M[M.length-1],C),S)}}}var Mf,zlt,x1e=F(()=>{"use strict";fc();Mf=.001,zlt=8;s(v1e,"simplifyDetouredEdges")});function T1e(e,t){let r=t?0:e.length-1,n=t?1:-1,i=e[r],a=e[r+n];if(!i||!a)return;let o=a.x-i.x,l=a.y-i.y;if(!(Math.abs(o)+Math.abs(l)a&&K3(e,Vlt(a)))}function n5(e,t){let r=[];for(let g of e){if(g.isLayoutOnly)continue;let y=g.points;if(!(!y||y.length<2))for(let v=0;v{let v=iB(y,a);for(let{nodeId:x,rect:b}of n)if(x!==g&&K3(v,b))return!0;return!1},"labelOverlapsForeignNode"),h=s((g,y)=>{let v=iB(y,a);for(let x of r)if(x.edgeId!==g&&gT(x.p1,x.p2,v))return!0;return!1},"labelOverlapsForeignEdge"),d=s((g,y,v)=>u(g,v)||h(y,v),"labelOverlapsAnything"),f=[],p=s(g=>{for(let{id:y,rect:v}of i)if(Fye(v,g))return y},"findContainingLane"),m=s((g,y)=>f.some(v=>v.labelId!==g&&K3(y,v.rect)),"overlapsPlacedLabel");for(let g of e){if(g.isLayoutOnly)continue;let y=g.labelNodeId;if(!y)continue;let v=t.get(y);if(!v)continue;let x=g.points;if(!x||x.length<2)continue;let b=v.width??0,T=v.height??0;if(b<=0||T<=0)continue;let w=[];for(let V=0;V=vo&&J>=vo||w.push({idx:V,length:K+J,orientation:K>=vo?"horizontal":"vertical",midX:(z.x+H.x)/2,midY:(z.y+H.y)/2})}if(w.length===0)continue;let C=w.length>=3?w.filter(V=>V.idx>0&&V.idx0?C:w,k=b>=T?"horizontal":"vertical",R=s(V=>[...V].sort((z,H)=>{let K=z.orientation===k,J=H.orientation===k;if(K!==J)return K?-1:1;let ne=z.length>=(z.orientation==="horizontal"?b:T)+2,Z=H.length>=(H.orientation==="horizontal"?b:T)+2;return ne!==Z?ne?-1:1:H.length-z.length}),"rankSegments"),M=w[0],P=w[w.length-1],N=[.5,.25,.75,.05,.95,.15,.85,.1,.9],_=s((V,z)=>{let H=x[V.idx],K=x[V.idx+1];return{midX:H.x+(K.x-H.x)*z,midY:H.y+(K.y-H.y)*z}},"anchorAtT"),A=s((V,z,H)=>Math.min(H,Math.max(z,V)),"clamp"),I=s((V,z)=>V.midX>=z.left-vo&&V.midX<=z.right+vo&&V.midY>=z.top-vo&&V.midY<=z.bottom+vo,"pointInsideRectInclusive"),D=s(V=>{let z=p1(V.midX,V.midY,b,T),H=p(z);if(H)return{laneId:H,anchor:V,rect:z};let K=i.find(({rect:ce})=>I(V,ce));if(!K)return;let J=K.rect.left+b/2+o,ne=K.rect.right-b/2-o,Z=K.rect.top+T/2+o,pe=K.rect.bottom-T/2-o;if(J>ne||Z>pe)return;let te={midX:A(V.midX,J,ne),midY:A(V.midY,Z,pe)},fe=p1(te.midX,te.midY,b,T);return I(V,fe)?{laneId:K.id,anchor:te,rect:fe}:void 0},"placementForAnchor"),L=s((V,z,H)=>V.orientation==="horizontal"?Math.abs(z.midX-H.x):Math.abs(z.midY-H.y),"distanceAlongSegment"),B=s((V,z)=>{let K=(V.orientation==="horizontal"?b/2:T/2)+l;if(V===M){let J=x[V.idx];if(L(V,z,J)+vo{let z=R(V);for(let H of z)for(let K of N){let J=_(H,K);if(!B(H,J))continue;let ne=D(J);if(ne&&!C1e(ne.rect,x)&&!m(y,ne.rect)&&!d(y,g.id,ne.rect))return{laneId:ne.laneId,anchor:ne.anchor}}},"tryPool"),$=s((V,z,H=!1)=>{let K=R(V);for(let J of K){let ne={midX:J.midX,midY:J.midY};if(z&&!B(J,ne))continue;let Z=D(ne);if(Z&&!C1e(Z.rect,x)&&!m(y,Z.rect)&&!u(y,Z.rect)&&(H||!h(g.id,Z.rect)))return{laneId:Z.laneId,anchor:Z.anchor}}},"findLaneContainingFallback"),W=O(S)??(S.lengthH.labelId===y);z>=0?f[z]={labelId:y,rect:V}:f.push({labelId:y,rect:V})}}}var vo,b1e,r5,S1e=F(()=>{"use strict";fc();vo=.001,b1e=10,r5=7;s(T1e,"markerClearanceRectFor");s(Vlt,"normalizeRect");s(C1e,"labelOverlapsOwnMarker");s(n5,"anchorLabelsToPolyline")});function k1e(e,t){return e{let d=k1e(l,u),f=0,p=s(m=>{if(!m)return;let g=i.get(m);if(!g)return;let y=h==="x"?g.w/2:g.h/2;y>f&&(f=y)},"consider");p(o.labelNodeId);for(let m of e){if(m===o||m.isLayoutOnly)continue;let g=m.start,y=m.end;!g||!y||k1e(g,y)===d&&p(m.labelNodeId)}return f>0?f+qlt:0},"labelClearanceFor");for(let o of e){if(o.isLayoutOnly)continue;let l=o.points;if(!j3(l,mB))continue;let u=J3(o,r,mB);if(!u)continue;let{srcId:h,dstId:d,srcInfo:f,dstInfo:p,collinearX:m,collinearY:g}=u;if(m===g)continue;let y,v;if(m){let C=p.cy>f.cy;y={x:f.cx,y:C?f.rect.bottom:f.rect.top},v={x:p.cx,y:C?p.rect.top:p.rect.bottom}}else{let C=p.cx>f.cx;y={x:C?f.rect.right:f.rect.left,y:f.cy},v={x:C?p.rect.left:p.rect.right,y:p.cy}}if(Qn(y,v,n,[h,d],1))continue;let b=a(o,h,d,m?"x":"y"),T=b>w1e?b:w1e,w=[0,T,-T];for(let C of w){let S={...y},k={...v};if(m){if(S.x+=C,k.x+=C,S.x<=f.rect.left||S.x>=f.rect.right||k.x<=p.rect.left||k.x>=p.rect.right)continue}else if(S.y+=C,k.y+=C,S.y<=f.rect.top||S.y>=f.rect.bottom||k.y<=p.rect.top||k.y>=p.rect.bottom)continue;if(!Qn(S,k,n,[h,d],1)&&!yT(S,k,e,o,{epsilon:mB})){o.points=[S,k];break}}}}var mB,Wlt,w1e,qlt,A1e=F(()=>{"use strict";fc();mB=1e-6,Wlt=8,w1e=Wlt/2,qlt=3;s(k1e,"pairKey");s(E1e,"straightenCollinearSiblingDetours")});function gB(e,t){let{realNodeRects:h,labelNodeRects:d}=yu(t.values()),f=s((S,k)=>Df(k,.001).map(R=>({...R,edge:S,interior:R.index>=1&&R.index<=k.length-3})),"segmentsFor"),p=s(()=>{let S=[];for(let k of e){if(k.isLayoutOnly)continue;let R=k.points;!R||R.length<2||S.push(...f(k,rn(R)))}return S},"allSegments"),m=s((S,k)=>S.horizontal&&k.horizontal?xs(S.a.x,S.b.x,k.a.x,k.b.x)>=8&&Math.abs(S.a.y-k.a.y)<7:S.vertical&&k.vertical?xs(S.a.y,S.b.y,k.a.y,k.b.y)>=8&&Math.abs(S.a.x-k.a.x)<7:!1,"hasCrowdedParallelTrack"),g=s((S,k)=>{let R=S.start,M=S.end,P=f(S,k);if(P.length!==k.length-1)return!1;let N=[R,M].filter(A=>!!A),_=S.labelNodeId?[S.labelNodeId]:[];for(let A of P)if(Qn(A.a,A.b,h,N,-2)||Qn(A.a,A.b,d,_,-2))return!1;for(let A of e){if(A===S||A.isLayoutOnly)continue;let I=A.points;if(!(!I||I.length<2)){for(let D of P)for(let L of f(A,rn(I)))if(m(D,L)||dc(D.a,D.b,L.a,L.b,.001))return!1}}return!0},"candidateIsSafe"),y=s((S,k)=>{let R=rn(S.edge.points??[]);if(R.length<4||S.index>=R.length-1)return;let M=R.map(P=>({...P}));if(S.horizontal)M[S.index].y+=k,M[S.index+1].y+=k;else if(S.vertical)M[S.index].x+=k,M[S.index+1].x+=k;else return;return f(S.edge,M).length===M.length-1?M:void 0},"shiftedCandidate"),v=s((S,k)=>({x:S.x??(k.left+k.right)/2,y:S.y??(k.top+k.bottom)/2}),"nodeCenter"),x=s(S=>{let k=S.edge,R=rn(k.points??[]);if(R.length!==4||S.index!==1)return;let M=k.start?t.get(k.start):void 0,P=k.end?t.get(k.end):void 0,N=M?Ba(M):void 0,_=P?Ba(P):void 0,A=R.slice(S.index+2);if(!(!M||!P||!N||!_||A.length===0))return{sourceCenter:v(M,N),targetCenter:v(P,_),sourceRect:N,tail:A}},"sourceDetourContextFor"),b=s((S,k,R,M,P,N)=>{let _=M.y>=R.y,A=_?P.bottom:P.top,I=A+(_?20:-20);if(_&&S.b.y<=I+.001||!_&&S.b.y>=I-.001)return;let D=S.a.x+k;return rn([{x:R.x,y:A},{x:R.x,y:I},{x:D,y:I},{x:D,y:S.b.y},...N],.001)},"verticalSourceDetour"),T=s((S,k,R,M,P,N)=>{let _=M.x>=R.x,A=_?P.right:P.left,I=A+(_?20:-20);if(_&&S.b.x<=I+.001||!_&&S.b.x>=I-.001)return;let D=S.a.y+k;return rn([{x:A,y:R.y},{x:I,y:R.y},{x:I,y:D},{x:S.b.x,y:D},...N],.001)},"horizontalSourceDetour"),w=s((S,k)=>{let R=x(S);if(R){if(S.vertical)return b(S,k,R.sourceCenter,R.targetCenter,R.sourceRect,R.tail);if(S.horizontal)return T(S,k,R.sourceCenter,R.targetCenter,R.sourceRect,R.tail)}},"sourceDetourCandidate"),C=[-7,7,-14,14,-21,21];for(let S=0;S<12;S++){let k=p(),R=!1;for(let M=0;MI.interior);for(let I of A){for(let D of C){let L=y(I,D);if(L&&g(I.edge,L)){I.edge.points=L,R=!0;break}let B=w(I,D);if(B&&g(I.edge,B)){I.edge.points=B,R=!0;break}}if(R)break}}if(!R)return}}var R1e=F(()=>{"use strict";fc();s(gB,"nudgeSharedInteriorSubpaths")});function Hlt(e,t,r,n){let i=t.x-e.x,a=t.y-e.y,o=n.x-r.x,l=n.y-r.y,u=i*l-a*o;if(Math.abs(u)<1e-10)return!1;let h=r.x-e.x,d=r.y-e.y,f=(h*l-d*o)/u,p=(h*a-d*i)/u,m=.01;return f>m&&f<1-m&&p>m&&p<1-m}function yB(e){let t=e.nodes??[],r=e.edges??[],n=[];if(!r.length||!t.length)return n;let i=Gye(t),a=1,o=[];for(let u of r){if(u.isLayoutOnly)continue;let h=u.points;if(!h||h.length<2)continue;let d=u.start,f=u.end,p=u.labelNodeId,m=u.id??`${d}->${f}`;for(let g of i)if(!(g.nodeId===d||g.nodeId===f)&&!(p&&g.nodeId===p)){for(let y=0;y0){let u=n.filter(d=>d.type==="edge-node-overlap").length,h=n.filter(d=>d.type==="edge-edge-crossing").length;se.warn(`[SWIMLANE_VALIDATE] ${n.length} issue(s) detected: ${u} edge-node overlap(s), ${h} edge crossing(s)`);for(let d of n)se.warn(`[SWIMLANE_VALIDATE] ${d.type}: ${d.detail}`)}return n}var _1e=F(()=>{"use strict";St();fc();s(Hlt,"segmentsIntersect");s(yB,"validateSwimlanesLayout")});function L1e(e,t){let r=e.nodes??[],n=e.edges??[],i=r.filter(l=>!l.isGroup);if((t==="LR"||t==="RL")&&i.length>0&&!a1e(e,t)||t==="BT"&&i.length>0&&!i1e(e))return;for(let l of n){if(l.isLayoutOnly)continue;let u=l.points;!u||u.length<2||(l.points=kl(vT(u)))}v1e(n,r),E1e(n,r),o1e(n,r);let a=new Map;for(let l of r)a.set(String(l.id),l);n5(n,a),Qye(n,a),c1e(n,a),gB(n,a),h1e(n,a),d1e(n,a),dB(n,a),f1e(n,a);let o=s(()=>{g1e(n,a),p1e(n,a),m1e(n,a),n5(n,a),cB(n,a),dB(n,a),n5(n,a),cB(n,a)},"finalizeRenderedEdges");o(),gB(n,a),o(),fB(n,a),pB(n,a),fB(n,a),pB(n,a)}var D1e=F(()=>{"use strict";e1e();fc();s1e();l1e();u1e();y1e();x1e();S1e();A1e();R1e();_1e();s(L1e,"postProcessSwimlaneLayout")});function xo(e){let t=new Map(e.nodeById),r=new Set,n=[];for(let a of e.edges){if(!t.has(a.src)||!t.has(a.dst))continue;let o=`${a.id}:${a.src}->${a.dst}`;r.has(o)||(r.add(o),n.push(a))}return{nodes:[...t.keys()],edges:n,layout:e.layout,nodeById:t}}function i5(e,t){return e.edges.filter(r=>r.dst===t)}function Ult(e){let t=new Map;for(let r of e.nodes)t.set(r,[]);for(let r of e.edges)t.get(r.src).push(r.dst);return t}function vB(e){let t=Ult(e);for(let r of t.values())r.sort((n,i)=>n.localeCompare(i));return t}function xB(e){let t=new Map;for(let r of e.nodes)t.set(r,0);for(let r of e.edges)t.set(r.dst,(t.get(r.dst)??0)+1);return t}function bB(e){return[...e.entries()].filter(([,t])=>t===0).map(([t])=>t).sort((t,r)=>t.localeCompare(r))}function g1(e,t=()=>!0){let r=new Map,n=new Map;for(let i of e.nodes)r.set(i,[]),n.set(i,[]);for(let i of e.edges)t(i)&&(n.get(i.src).push(i.dst),r.get(i.dst).push(i.src));return{preds:r,succs:n}}function a5(e,t,r,n){let i=0;for(let o of e.nodes)n?.skipGroups&&e.nodeById.get(o)?.isGroup||(i=Math.max(i,r[o]??0));let a=Array.from({length:i+1},()=>[]);for(let o of t)n?.skipGroups&&e.nodeById.get(o)?.isGroup||a[Math.max(0,r[o]??0)].push(o);return a}function Nf(e){let t=xB(e),r=bB(t),n=[],i=vB(e);for(;r.length;){let a=r.shift();n.push(a);for(let o of i.get(a)??[])if(t.set(o,(t.get(o)??0)-1),(t.get(o)??0)===0){let l=0;for(;l{if(i-n<=1)return 0;let a=n+i>>1,o=r(n,a)+r(a,i),l=n,u=a,h=n;for(;l=i||l{"use strict";s(xo,"normalizeGraph");s(i5,"incoming");s(Ult,"buildSuccessorMap");s(vB,"buildSortedSuccessorMap");s(xB,"buildInDegreeMap");s(bB,"sortedZeroInDegreeNodes");s(g1,"buildPredecessorSuccessorMaps");s(a5,"buildLayersFromRanks");s(Nf,"topoSortIfAcyclic");s(Hm,"buildLayerIndex");s(s5,"countInversions")});function I1e(e){let t=xo(e),r=new Map;for(let d of t.nodes)r.set(d,[]);for(let d of t.edges)r.get(d.src).push(d);for(let d of r.values())d.sort((f,p)=>f.dst===p.dst?f.id.localeCompare(p.id):f.dst.localeCompare(p.dst));let n=Object.create(null);for(let d of t.nodes)n[d]=0;let i=[],a=s(d=>{n[d]=1;for(let f of r.get(d)??[]){let p=f.dst;n[p]===0?a(p):n[p]===1&&i.push(f)}n[d]=2},"dfs"),o=[...t.nodes].sort((d,f)=>d.localeCompare(f));for(let d of o)n[d]===0&&a(d);let l=new Set(i.map(d=>`${d.id}:${d.src}->${d.dst}`)),u=t.edges.map(d=>l.has(`${d.id}:${d.src}->${d.dst}`)?{id:d.id,src:d.dst,dst:d.src,weight:d.weight,ref:d.ref}:d);return{acyclic:{nodes:[...t.nodes],edges:u,layout:t.layout,nodeById:new Map(t.nodeById)},reversed:i}}var M1e=F(()=>{"use strict";vu();s(I1e,"removeCycles_DFS")});function Ylt(e){let t=new Map,r=s(n=>{if(t.has(n))return t.get(n);let i=e.nodeById.get(n);if(!i)return t.set(n,null),null;let a=i.parentId;if(!a)return t.set(n,null),null;let l=r(a)??a;return t.set(n,l),l},"resolve");for(let n of e.nodes)r(n);return t}function bs(e){let t=Ylt(e);return r=>t.get(r)??null}function o5(e){let t=[];for(let r of e.layout.nodes??[])r.isGroup&&!r.parentId&&t.push(r.id);return[...new Set(t)].reverse()}function l5(e,t){let r=o5(e);if(!t||t.length===0)return r;let n=new Set(r),i=new Set,a=[];for(let o of t)!n.has(o)||i.has(o)||(i.add(o),a.push(o));for(let o of r)i.has(o)||a.push(o);return a}var Nh=F(()=>{"use strict";s(Ylt,"buildTopLaneMap");s(bs,"createTopLaneResolver");s(o5,"buildTopLaneOrder");s(l5,"resolveTopLaneOrder")});var N1e,Um,TB,y1=F(()=>{"use strict";N1e={EPSILON:1e-6},Um={GRAVITY_ITERATIONS:8,MAX_CROSSING_OPTIMIZATION_PASSES:4,DEFAULT_COMPACT_SINGLE_INPUT:!0},TB={DEFAULT_LAYER_GAP:100,DEFAULT_NODE_GAP:40}});function P1e(e,t){let r=xo(e),n=t?.laneOf??(()=>null),i=t?.rankHint,{preds:a}=g1(r);for(let C of a.values())C.sort((S,k)=>S.localeCompare(k));let o=Nf(r)??[...r.nodes].sort((C,S)=>C.localeCompare(S)),l=new Map;for(let[C,S]of o.entries())l.set(S,C);let u=new Map,h=new Map;for(let C of r.nodes)h.set(C,[]);for(let C of o){let S=(a.get(C)??[]).filter(k=>u.has(k));if(S.length>0){let k=jlt(C,S,{laneOf:n,rankHint:i,topoIndex:l});u.set(C,k),h.get(k).push(C)}else u.has(C)||u.set(C,null)}for(let C of r.nodes)u.has(C)||u.set(C,null);let d=new Set;for(let C of r.nodes)(u.get(C)??null)===null&&d.add(C);let f=[...d].sort((C,S)=>{let k=l.get(C)??0,R=l.get(S)??0;return k===R?C.localeCompare(S):k-R}),p=Xlt(r),m=new Map;for(let[C,S]of p.entries())m.set(C,[...S].sort((k,R)=>k.localeCompare(R)));let g=Klt(m),y=Zlt(m),v=new Map;for(let C of r.nodes)v.set(C,[]);for(let C of y)for(let S of C.nodes){let k=v.get(S);k?k.push(C.id):v.set(S,[C.id])}let x=[],b=[],T=new Set,w=s(C=>{if(!T.has(C)){T.add(C),x.push(C);for(let S of h.get(C)??[])w(S);b.push(C)}},"walk");for(let C of f)w(C);for(let C of o)w(C);return{parent:u,children:h,roots:f,componentOf:g,blocks:y,nodeBlocks:v,adjacency:m,preorder:x,postorder:b,topologicalOrder:o}}function jlt(e,t,r){let n=r.laneOf(e);return[...t].sort((a,o)=>{let l=r.laneOf(a),u=r.laneOf(o),h=l!=null&&l===n,d=u!=null&&u===n;if(h!==d)return h?-1:1;let f=r.rankHint?.[a],p=r.rankHint?.[o];if(f!=null&&p!=null&&f!==p)return p-f;let m=r.topoIndex.get(a)??0,g=r.topoIndex.get(o)??0;return m!==g?m-g:a.localeCompare(o)})[0]}function Xlt(e){let t=new Map;for(let r of e.nodes)t.set(r,new Set);for(let r of e.edges)t.get(r.src).add(r.dst),t.get(r.dst).add(r.src);return t}function Klt(e){let t=new Map,r=0;for(let n of e.keys()){if(t.has(n))continue;let i=[n];for(;i.length>0;){let a=i.pop();if(!t.has(a)){t.set(a,r);for(let o of e.get(a)??[])t.has(o)||i.push(o)}}r++}return t}function Zlt(e){let t=new Map,r=new Map,n=[],i=[],a=0,o=s((l,u)=>{t.set(l,++a),r.set(l,a);for(let h of e.get(l)??[])h!==u&&(t.has(h)?(t.get(h)??0)<(t.get(l)??0)&&(n.push([l,h]),r.set(l,Math.min(r.get(l)??a,t.get(h)??a))):(n.push([l,h]),o(h,l),r.set(l,Math.min(r.get(l)??a,r.get(h)??a)),(r.get(h)??0)>=(t.get(l)??0)&&i.push(Qlt(l,h,n,i.length))))},"visit");for(let l of e.keys())t.has(l)||o(l,null);return i}function Qlt(e,t,r,n){let i=[],a=new Set;for(;r.length>0;){let o=r.pop();if(i.push(o),a.add(o[0]),a.add(o[1]),o[0]===e&&o[1]===t||o[0]===t&&o[1]===e)break}return{id:n,edges:i,nodes:[...a]}}var O1e=F(()=>{"use strict";vu();s(P1e,"buildDrivingTree");s(jlt,"chooseParent");s(Xlt,"buildAdjacency");s(Klt,"assignComponents");s(Zlt,"computeBlocks");s(Qlt,"popBlock")});function B1e(e,t,r){let n=[...e.nodes],i=new Map;for(let[b,T]of n.entries())i.set(T,b);let a=n.length,o=new Array(a).fill(-1),l=new Array(a).fill(0),u=[],h=new Set;for(let b of n){let T=r.parent.get(b)??null,w=i.get(b);w!=null&&T==null&&(o[w]=-1,l[w]=0,h.has(b)||(h.add(b),u.push(b)))}for(;u.length>0;){let b=u.shift(),T=i.get(b);if(T==null)continue;let w=r.children.get(b)??[];for(let C of w){if(h.has(C))continue;let S=i.get(C);S!=null&&(o[S]=T,l[S]=l[T]+1,h.add(C),u.push(C))}}for(let b of n){if(h.has(b))continue;let T=i.get(b);T!=null&&(o[T]=-1,l[T]=0,h.add(b))}let d=Math.max(1,Math.ceil(Math.log2(Math.max(1,a)))+1),f=Array.from({length:d},()=>new Array(a).fill(-1));for(let b=0;b{if(b===-1||T===-1)return-1;l[b]>C&1&&(b=f[C][b],b===-1))return-1;if(b===T)return b;for(let C=d-1;C>=0;C--){let S=f[C][b],k=f[C][T];S===-1||k===-1||S!==k&&(b=S,T=k)}return f[0][b]},"lcaIndex"),m=Array.from({length:a},()=>new Map);for(let b of e.edges){let T=b.src,w=b.dst,C=t[T],S=t[w];if(C==null||S==null||(C>S&&([T,w]=[w,T],[C,S]=[S,C]),C==null||S==null||C===S))continue;let k=i.get(T),R=i.get(w);if(k==null||R==null)continue;let M=p(k,R);if(M===-1)continue;let P=m[M];for(let N=C;N{if(T.size!==0)for(let[w,C]of T)b.set(w,(b.get(w)??0)+C)},"mergeInto"),v=new Set,x=s(b=>{let T=i.get(b);v.add(b);let w=T==null?void 0:m[T],C=w?new Map(w):new Map,S=r.children.get(b)??[];for(let k of S){let R=x(k),M=t[b];if(M!=null){let P=g.get(b);P||(P=new Map,g.set(b,P));let N=R.get(M)??0,_=t[k];_!=null&&_>M&&(N+=1),P.set(k,N)}y(C,R)}return C},"dfs");for(let b of r.roots)v.has(b)||x(b);for(let b of n)v.has(b)||x(b);return g}var $1e=F(()=>{"use strict";s(B1e,"computeSubtreeCrossCounts")});function F1e(e,t,r){let n=new Map,i=s(a=>{let o=r[a]??0,l=[...t.get(a)??[]];l.sort(CB(r));for(let u of l){i(u);let h=n.get(u);h!=null&&(o=Math.min(o,h))}n.set(a,o)},"annotate");for(let a of e)i(a);return n}function CB(e){return(t,r)=>{let n=e[t]??0,i=e[r]??0;return n===i?t.localeCompare(r):n-i}}function G1e(e,t,r,n){let i=0;for(let u of t){let h=r[u]??0;h>i&&(i=h)}let a=Array.from({length:i+1},()=>[]),o=new Set,l=s(u=>{if(o.has(u))return;o.add(u);let h=r[u]??0;a[h]||(a[h]=[]),a[h].push(u);for(let d of n(u))l(d)},"emit");for(let u of e)l(u);for(let u of t)if(!o.has(u)){let h=r[u]??0;a[h]||(a[h]=[]),a[h].push(u),o.add(u)}return a}function z1e(e){let t=[];for(let r of e){let n=new Set,i=[];for(let a of r)n.has(a)||(n.add(a),i.push(a));t.push(i)}return t}var V1e=F(()=>{"use strict";s(F1e,"annotateMinimumLayers");s(CB,"compareByRankThenId");s(G1e,"emitNodesInTreeOrder");s(z1e,"deduplicateLayers")});function Jlt(e,t,r,n){return i=>{let a=e.get(i)??[];if(a.length===0)return[];let o=t[i]??0,l=[],u=[],h=r.get(i);for(let d of a){let f=n.get(d)??o;f>o?l.push({child:d,min:f}):u.push(d)}return l.sort((d,f)=>d.min===f.min?d.child.localeCompare(f.child):d.min-f.min),u.sort((d,f)=>{let p=h?.get(d)??0,m=h?.get(f)??0;if(p!==m)return p-m;let g=n.get(d)??o,y=n.get(f)??o;return g!==y?g-y:d.localeCompare(f)}),[...l.map(d=>d.child),...u]}}function xT(e,t,r){let n=P1e(e,{rankHint:t,laneOf:r}),{children:i,roots:a}=n;for(let f of e.nodes)i.has(f)||i.set(f,[]);let o=B1e(e,t,n),l=[...a].sort(CB(t)),u=F1e(l,i,t),h=Jlt(i,t,o,u),d=G1e(l,e.nodes,t,h);return d=z1e(d),d}var SB=F(()=>{"use strict";O1e();$1e();V1e();s(Jlt,"createChildOrderer");s(xT,"buildMultitreeLayerOrder")});function ect(e,t,r){let n=new Set(e),i=new Set(t),a=Hm(t),o=[];for(let l of r)n.has(l.src)&&i.has(l.dst)&&o.push(a.get(l.dst));return s5(o)}function W1e(e,t,r){let n=[];for(let a of t){let o=r[a.src],l=r[a.dst];if(o==null||l==null||o===l)continue;let u=a.src,h=a.dst,d=o,f=l;o>l&&(u=a.dst,h=a.src,d=l,f=o);for(let p=d;p(r[p]??0)-(r[f]??0));for(let f of d){let p=r[f]??0;if(p===0)continue;let m=0;for(let x of n.get(f)??[])m=Math.max(m,(r[x]??0)+1);if(m>=p)continue;let g=p;r[f]=m;let y=xT(e,r,i),v=W1e(y,e.edges,r);v{"use strict";vu();y1();Nh();SB();s(ect,"countCrossingsBetweenAdjacent");s(W1e,"totalCrossings");s(q1e,"optimizeRanksByCrossings")});function U1e(e,t){let r=bs(e),n=[...e.nodes].sort((i,a)=>(t[i]??0)-(t[a]??0)||i.localeCompare(a));for(let i of n){let a=r(i);if(!a)continue;let o=e.edges.filter(y=>y.src===i);if(o.length===0)continue;let l=!1,u=0;for(let y of o){let v=r(y.dst);v==null||v===a?l=!0:u++}if(u===0||l)continue;let h=0,d=!1;for(let y of e.edges){if(y.dst!==i)continue;let v=r(y.src);v&&(v===a?d=!0:h++)}if(h>0||!d)continue;let f=t[i]??0,p=f+u,m=0;for(let y of e.edges)y.dst===i&&(m=Math.max(m,(t[y.src]??0)+1));let g=Math.max(f,m,p);g!==f&&(t[i]=g)}}var Y1e=F(()=>{"use strict";Nh();s(U1e,"adjustCrossLaneSources")});function j1e(e,t){let r=xo(e),n=Nf(r)??[...r.nodes].sort(),i=t?.compactSingleInput??!1,a=bs(r),o=Object.create(null);for(let u of n){let h=i5(r,u),d=t?.ignoreCrossLaneEdges?h.filter(f=>{let p=a(f.src),m=a(u);return!p||!m?!0:p===m}):h;if(d.length===0)o[u]=0;else if(i&&d.length===1){let f=d[0].src,p=a(f),m=a(u);p!==m?o[u]=o[f]??0:o[u]=(o[f]??0)+1}else{let f=-1/0;for(let p of d)f=Math.max(f,(o[p.src]??0)+1);o[u]=f===-1/0?0:f}}return(t?.optimizeRanksByCrossings??!1)&&(o=q1e(r,o)),t?.ignoreCrossLaneEdges&&U1e(r,o),{layers:xT(r,o,a),rankOf:o,dummy:new Set}}var X1e=F(()=>{"use strict";vu();Nh();H1e();Y1e();SB();s(j1e,"assignLayers_LongestPath")});function K1e(e,t){let r=xo(e),i={...j1e(r,{compactSingleInput:t?.compactSingleInput,ignoreCrossLaneEdges:t?.ignoreCrossLaneEdges,optimizeRanksByCrossings:t?.optimizeRanksByCrossings}).rankOf},a=bs(r),{preds:o,succs:l}=g1(r,g=>{if(t?.ignoreCrossLaneEdges){let y=a(g.src),v=a(g.dst);if(y&&v&&y!==v)return!1}return!0}),u=Nf(r)??[...r.nodes],h=[...u].reverse(),d=s((g,y)=>{let v=0;for(let T of o.get(g)??[])v=Math.max(v,(i[T]??0)+1);let x=Number.POSITIVE_INFINITY,b=l.get(g)??[];return b.length>0&&(x=Math.min(...b.map(T=>(i[T]??0)-1))),Number.isFinite(x)||(x=Math.max(v,y)),Math.min(Math.max(y,v),x)},"clampFeasible"),f=Um.GRAVITY_ITERATIONS,p=s(g=>{let y=!1;for(let v of g){let x=o.get(v)??[],b=l.get(v)??[];if(x.length===0&&b.length===0)continue;let T=x.length>0?x.reduce((k,R)=>k+(i[R]??0)+1,0)/x.length:i[v]??0,w=b.length>0?b.reduce((k,R)=>k+(i[R]??0)-1,0)/b.length:i[v]??0,C=Math.round((T+w)/2),S=d(v,C);S!==i[v]&&(i[v]=S,y=!0)}return y},"relaxOrder");for(let g=0;g0){let v=Math.min(...y.map(x=>(i[x]??0)-1));(i[g]??0)>v&&(i[g]=v)}}return{layers:a5(r,u,i),rankOf:i,dummy:new Set}}var Z1e=F(()=>{"use strict";vu();Nh();y1();X1e();s(K1e,"assignLayers_Gravity")});function tct(e){let t=xB(e),r=vB(e),n=bB(t),i=[];for(;n.length>0;){let a=[];for(let o of n){i.push(o);for(let l of r.get(o)??[])t.set(l,(t.get(l)??0)-1),(t.get(l)??0)===0&&a.push(l)}n=a.sort((o,l)=>o.localeCompare(l))}return i.length===e.nodes.length?i:null}function Q1e(e,t){let r=xo(e),n=t?.direction==="LR"?tct(r)??[...r.nodes].sort():Nf(r)??[...r.nodes].sort(),i=bs(r),a=s(d=>i(d)??d,"laneOf"),o=Object.create(null),l=new Map,u=s((d,f)=>t?.ignoreCrossLaneEdges??!0?a(d)===a(f)?1:0:1,"edgeWeight");for(let d of n){if(r.nodeById.get(d)?.isGroup)continue;let p=i5(r,d),m=0;if(p.length>0)for(let x of p){let b=x.src,T=o[b]??0;m=Math.max(m,T+u(b,d))}let g=a(d),y=l.get(g)??0,v=Math.max(m,y);o[d]=v,l.set(g,v+1)}return{layers:a5(r,n,o,{skipGroups:!0}),rankOf:o,dummy:new Set}}var J1e=F(()=>{"use strict";vu();Nh();s(tct,"topoSortByGenerationIfAcyclic");s(Q1e,"assignLayers_LaneAwareCompact")});function eve(e,t){let r=xo(t),{rankOf:n}=e,i=e.layers.map(m=>[...m]),a=new Set(e.dummy?[...e.dummy]:[]),o=0,l=new Map(r.nodeById),u=s(m=>{let g=`placeholder-${o++}`,y={id:g,isGroup:!1,isDummy:!0,width:0,height:0};for(l.set(g,y),a.add(g);i.length<=m;)i.push([]);return i[m].push(g),n[g]=m,g},"addDummyAt"),h=[...r.edges].sort((m,g)=>m.id===g.id?m.src===g.src?m.dst.localeCompare(g.dst):m.src.localeCompare(g.src):m.id.localeCompare(g.id)),d=[];for(let m of h){let g=n[m.src]??0,y=n[m.dst]??0;if(y-g<=1){d.push(m);continue}let v=m.src;for(let b=g+1,T=0;b!r.nodes.includes(m))],edges:d,layout:r.layout,nodeById:l};return{layering:{layers:i,rankOf:n,dummy:a},graphWithDummies:p}}var tve=F(()=>{"use strict";vu();s(eve,"makeProperLayering")});function rve(e){let t=e.length;if(t===0)return Number.POSITIVE_INFINITY;let r=[...e].sort((n,i)=>n-i);return t%2===1?r[(t-1)/2]:.5*(r[t/2-1]+r[t/2])}function nve(e){return e.length===0?Number.POSITIVE_INFINITY:e.reduce((r,n)=>r+n,0)/e.length}function rct(e,t,r,n){let i=new Map;for(let a of e)i.set(a,[]);for(let a of r)n==="down"?t.has(a.src)&&i.has(a.dst)&&i.get(a.dst).push(t.get(a.src)):t.has(a.dst)&&i.has(a.src)&&i.get(a.src).push(t.get(a.dst));return i}function nct(e,t,r){let n=r.get(e)??0,i=r.get(t)??0;return n!==i?n-i:e.localeCompare(t)}function ive(e,t,r){let n=new Set(e),i=new Set(t),a=Hm(e),o=Hm(t),l=[];for(let h of r)n.has(h.src)&&i.has(h.dst)&&l.push({u:a.get(h.src),v:o.get(h.dst)});l.sort((h,d)=>h.u===d.u?h.v-d.v:h.u-d.u);let u=l.map(h=>h.v);return s5(u)}function wB(e,t,r){return[...e].sort((n,i)=>{let a=rve(t.get(n)??[]),o=rve(t.get(i)??[]);return a===o?nct(n,i,r):isFinite(a)?isFinite(o)?a-o:-1:1})}function ave(e,t,r,n,i,a){let o=Hm(e),l=Hm(t),u=rct(t,o,r,n);if(!i||!a||a.length===0)return wB(t,u,l);let h=new Map;for(let p of t){let m=i(p),g=h.get(m)??[];g.push(p),h.set(m,g)}let d=[];for(let p of a){let m=h.get(p);if(!m||m.length===0)continue;let g=wB(m,u,l);d.push(...g)}let f=h.get(null);if(f&&f.length>0){let p=wB(f,u,l);for(let m of p){let g=nve(u.get(m)??[]),y=d.length;if(isFinite(g))for(let[v,x]of d.entries()){let b=nve(u.get(x)??[]);if(go.has(y.src)&&l.has(y.dst)),d=u?r.filter(y=>l.has(y.src)&&u.has(y.dst)):void 0,f=s(y=>{let v=ive(e,y,h);return d&&n&&(v+=ive(y,n,d)),v},"crossingScore"),p=i?new Map:null;if(i&&p)for(let y of t)p.set(y,i(y));let m=!0,g=f(a);for(;m;){m=!1;for(let y=0;y+1[...l]),i=t.edges,a=bs(t),o=l5(t,r?.laneOrder);for(let l=0;l<3;l++){for(let u=1;u=0;u--)n[u]=ave(n[u+1],n[u],i,"up",a,o),n[u]=sve(n[u+1],n[u],i,n[u-1],a)}return{layers:n}}var lve=F(()=>{"use strict";vu();Nh();s(rve,"median");s(nve,"barycenter");s(rct,"neighborPositionsFor");s(nct,"currentOrderTieBreak");s(ive,"countCrossingsBetweenAdjacent");s(wB,"sortByHeuristic");s(ave,"reorderLayer");s(sve,"transposeImprove");s(ove,"orderLayers")});function cve(e,t,r){let n=r?.layerGap??TB.DEFAULT_LAYER_GAP,i=r?.nodeGap??TB.DEFAULT_NODE_GAP,a=r?.laneGap??i*2,o=r?.direction??"TB",l=o==="LR"||o==="RL",u=e.layers,h=Object.create(null),d=Object.create(null),f=s(P=>t.nodeById.get(P),"getNode"),p=s(P=>f(P)?.width??0,"getWidth"),m=s(P=>f(P)?.height??0,"getHeight"),g=bs(t),y=l5(t,r?.laneOrder),v=u.map(P=>P.reduce((N,_)=>Math.max(N,m(_)),0)),x=[];if(l)for(let P=0;P+1Math.max(O,p($)),0),_=u[P+1].reduce((O,$)=>Math.max(O,p($)),0),A=v[P],I=v[P+1],D=A/2+I/2,L=(N+_)/2,B=Math.max(0,L-D-n);x.push(B)}let b=new Set;for(let P of u)for(let N of P)b.add(g(N));let T=b.has(null),w=y.filter(P=>b.has(P)),C=[...T?[null]:[],...w],S=Object.create(null);for(let P of w)S[P]=0;T&&(S.null=0);for(let P of u){let N=Object.create(null),_=[];for(let A of P){let I=g(A);I===null?_.push(A):(N[I]||=[]).push(A)}for(let[A,I]of Object.entries(N)){let D=I.reduce((L,B)=>L+p(B),0)+i*Math.max(0,I.length-1);S[A]=Math.max(S[A]??0,D)}if(T&&_.length){let A=_.reduce((I,D)=>I+p(D),0)+i*Math.max(0,_.length-1);S.null=Math.max(S.null??0,A)}}let k=new Map;{let P=C.map(A=>(A===null?S.null:S[A])??0),_=-(P.reduce((A,I)=>A+I,0)+a*Math.max(0,C.length-1))/2;for(let A=0;Ap(V)),$=O.reduce((V,z)=>V+z,0)+i*(L.length-1),W=B-$/2;for(let[V,z]of L.entries()){let H=O[V];h[z]=W+H/2,d[z]=R+_/2,W+=H+i}}}let I=x[P]??0;R+=_+n+I}let M=new Map;for(let P of t.edges){let N=P.ref.id;M.has(N)||M.set(N,[]),M.get(N).push(P)}for(let[,P]of M){if(P.length===0)continue;let N=P[0].ref,_=N.start,A=N.end;if(_==null||A==null)continue;let I=Math.round(((h[_]??0)+(h[A]??0))/2),D=new Set;for(let L of P)D.add(L.src),D.add(L.dst);for(let L of D){if(L===_||L===A)continue;t.nodeById.get(L)?.isDummy&&(h[L]=I)}}return{x:h,y:d}}var uve=F(()=>{"use strict";y1();Nh();s(cve,"assignCoordinates")});function ict(e){let t=2166136261;for(let r=0;r>>0}function act(e){let t=e>>>0;return()=>{t+=1831565813;let r=t;return r=Math.imul(r^r>>>15,r|1),r^=r+Math.imul(r^r>>>7,r|61),((r^r>>>14)>>>0)/4294967296}}function sct(e,t){let r=[...e],n=act(t);for(let i=r.length-1;i>0;i--){let a=Math.floor(n()*(i+1));[r[i],r[a]]=[r[a],r[i]]}return r}function oct(e,t){let r=0;for(let[n,i]of e.entries())r+=Math.abs(n-(t.get(i)??n));return r}function hve(e,t){let r=new Map;for(let[i,a]of e.entries())r.set(a,i);let n=0;for(let{a:i,b:a,weight:o}of t){let l=r.get(i),u=r.get(a);l==null||u==null||(n+=o*Math.abs(l-u))}return n}function lct(e){let t=o5(e);if(t.length<2)return[];let r=new Map(t.map((a,o)=>[a,o])),n=bs(e),i=new Map;for(let a of e.layout.edges??[]){if(a.isLayoutOnly)continue;let o=typeof a.start=="string"?a.start:void 0,l=typeof a.end=="string"?a.end:void 0;if(!o||!l||!e.nodeById.has(o)||!e.nodeById.has(l))continue;let u=n(o),h=n(l);if(!u||!h||u===h)continue;let d=r.get(u),f=r.get(h);if(d==null||f==null)continue;let[p,m]=d<=f?[u,h]:[h,u],g=`${p}\0${m}`,y=i.get(g);y?y.weight++:i.set(g,{a:p,b:m,weight:1})}return[...i.values()]}function dve(e,t,r){let n=[...e],i=hve(n,t),a=!0,o=0,l=Math.max(1,n.length);for(;a&&oi.a===a.a?i.b.localeCompare(a.b):i.a.localeCompare(a.a)).map(({a:i,b:a,weight:o})=>`${i}:${a}:${o}`).join("|");return ict(`${e.join("|")}#${n}#${r}`)}function fve(e,t={}){let r=o5(e);if(r.length<2)return r;let n=lct(e);if(n.length===0)return r;let i=new Map(r.map((l,u)=>[l,u])),a=dve(r,n,i),o=Math.max(0,t.restarts??kB);for(let l=0;l{"use strict";Nh();kB=8;s(ict,"hashString");s(act,"mulberry32");s(sct,"deterministicShuffle");s(oct,"sourceDistance");s(hve,"laneArrangementCost");s(lct,"buildWeightedLaneEdges");s(dve,"greedySwitch");s(cct,"isBetterCandidate");s(uct,"seedForRestart");s(fve,"optimizeTopLaneOrder")});function mve(e,t){let r=t?.ignoreCrossLaneEdges??!0,n=t?.optimizeRanksByCrossings??!0,i=xo(e),a=t?.automaticLaneOrdering?fve(i,{restarts:kB}):void 0,o=I1e(i),l=o.acyclic,u=r?Q1e(l,{compactSingleInput:t?.compactSingleInput??Um.DEFAULT_COMPACT_SINGLE_INPUT,ignoreCrossLaneEdges:!0,direction:t?.direction}):K1e(l,{compactSingleInput:t?.compactSingleInput??Um.DEFAULT_COMPACT_SINGLE_INPUT,ignoreCrossLaneEdges:!1,optimizeRanksByCrossings:n}),{layering:h,graphWithDummies:d}=eve(u,l),f=ove(h,d,{laneOrder:a}),p=cve(f,d,{layerGap:t?.layerGap,nodeGap:t?.nodeGap,direction:t?.direction,laneOrder:a});return{acyclic:l,reversed:o.reversed,layering:h,ordered:f,coordinates:p}}var gve=F(()=>{"use strict";vu();M1e();Z1e();J1e();tve();lve();uve();y1();pve();s(mve,"sugiyamaLayout")});function yve(e,t,r){let n=e.x??0,i=e.y??0,a=t.x-n,o=t.y-i,l=Math.abs(a),u=Math.abs(o);return lJr&&u*3>=l?o>0?"bottom":"top":l>Jr?a>0?"right":"left":r}function vve(e,t){return Math.abs(e.to-t.from)re.isGroup&&!re.parentId);for(let re of h){let ue={id:re.id},le=s(he=>{o.set(he.id,ue),r.filter(Te=>Te.parentId===he.id).forEach(le)},"assignLane");le(re)}let d=r.filter(re=>!re.isGroup&&!re.isEdgeLabel).map(re=>{let ue=re.width??10,le=re.height??10,he=re.x??0,Te=re.y??0,oe=hct;return{nodeId:re.id,minX:he-ue/2-oe,maxX:he+ue/2+oe,minY:Te-le/2-oe,maxY:Te+le/2+oe,visualXHalfExtent:u?le/2+oe:ue/2+oe}}),f=s((re,ue,le,he)=>{let Te=l.find(oe=>oe.orientation===re&&Math.abs(oe.coord-ue)<1);return Te||(Te={id:`pipe-${re}-${ue.toFixed(0)}`,orientation:re,coord:ue,spanMin:le,spanMax:he,tracks:[]},l.push(Te)),Te.spanMin=Math.min(Te.spanMin,le),Te.spanMax=Math.max(Te.spanMax,he),Te},"getOrAddPipe"),p=s((re,ue)=>{let le=re.width??10,he=re.height??10,Te=re.x??0,oe=re.y??0;switch(ue){case"top":return{x:Te,y:oe-he/2};case"bottom":return{x:Te,y:oe+he/2};case"left":return{x:Te-le/2,y:oe};case"right":return{x:Te+le/2,y:oe}}},"portForSide"),m=s((re,ue,le)=>p(re,yve(re,ue,le?"bottom":"top")),"getOrthogonalPort"),g=[],y=[],v=new Set,x=1e3,b=s((re,ue,le)=>{if(g.length===0)return 0;let he=Math.abs(ue.y-le.y)ee||q.from-Jr<=G&&q.to+Jr>=G&&(oe+=x)}else if(Te){let G=ue.x,X=Math.min(ue.y,le.y)-Jr,ee=Math.max(ue.y,le.y)+Jr;if(ee<=X)return 0;for(let q of g)q.edgeIndex===re||q.orientation!=="horizontal"||q.pipe.coordee||q.from-Jr<=G&&q.to+Jr>=G&&(oe+=x)}return oe},"crossingPenalty"),T=i.map((re,ue)=>{if(!re.start||!re.end)return{idx:ue,crossLane:0,dx:0,dy:0};let le=a.get(re.start),he=a.get(re.end),Te=o.get(re.start),oe=o.get(re.end),G=Te&&oe&&Te.id!==oe.id?1:0,X=le&&he?Math.abs((he.x??0)-(le.x??0)):0,ee=le&&he?Math.abs((he.y??0)-(le.y??0)):0;return{idx:ue,crossLane:G,dx:X,dy:ee}}).sort((re,ue)=>{if(re.crossLane!==ue.crossLane)return ue.crossLane-re.crossLane;let le=re.dx+re.dy,he=ue.dx+ue.dy;return Math.abs(le-he)>1?le-he:re.idx-ue.idx}).map(re=>re.idx),w=s((re,ue,le,he)=>{let Te=Math.min(re.x,ue.x),oe=Math.max(re.x,ue.x),G=Math.min(re.y,ue.y),X=Math.max(re.y,ue.y);return!!d.find(q=>le&&q.nodeId===le||he&&q.nodeId===he?!1:Math.abs(re.x-ue.x)>Jr?q.minYre.y&&q.maxX>Te&&q.minXre.x&&q.maxY>G&&q.minYyve(re,ue,"bottom"),"determineSide"),R=new Map;for(let[re,ue]of i.entries()){if(!ue.start||!ue.end||ue.start===ue.end||ue.points&&ue.points.length>0)continue;let le=a.get(ue.start),he=a.get(ue.end);if(!le||!he)continue;let Te=(he.x??0)-(le.x??0),oe=(he.y??0)-(le.y??0);R.set(re,{edgeIdx:re,srcId:ue.start,dstId:ue.end,srcSide:k(le,{x:he.x??0,y:he.y??0}),dstSide:k(he,{x:le.x??0,y:le.y??0}),absDx:Math.abs(Te),absDy:Math.abs(oe),dxSign:Math.sign(Te),dySign:Math.sign(oe)})}let M=s(re=>re.srcSide==="top"||re.srcSide==="bottom"?re.absDx===0?1/0:re.absDy/re.absDx:re.absDy===0?1/0:re.absDx/re.absDy,"preferenceStrength"),P=s(re=>re.srcSide==="top"||re.srcSide==="bottom"?re.dxSign>=0?"right":"left":re.dySign>=0?"bottom":"top","secondarySide"),N=new Map;for(let re of R.values()){let ue=`${re.srcId}:${re.srcSide}`;N.has(ue)||N.set(ue,[]),N.get(ue).push(re)}let _=new Map,A=s((re,ue)=>`${re}:${ue}`,"loadKey");for(let re of R.values())_.set(A(re.srcId,re.srcSide),(_.get(A(re.srcId,re.srcSide))??0)+1),_.set(A(re.dstId,re.dstSide),(_.get(A(re.dstId,re.dstSide))??0)+1);for(let re of N.values())if(!(re.length<2)){re.sort((ue,le)=>{let he=M(ue),Te=M(le);return Math.abs(he-Te)>1e-9?Te-he:ue.edgeIdx-le.edgeIdx});for(let ue=1;ue=Te||(_.set(A(le.srcId,le.srcSide),Te-1),_.set(A(le.srcId,he),oe+1),le.srcSide=he)}}let I=s(re=>{let ue=re?.shape;return ue==="question"||ue==="diamond"},"isDiamondNode"),D=new Map;for(let re of R.values())D.has(re.dstId)||D.set(re.dstId,new Set),D.get(re.dstId).add(re.dstSide);for(let re of R.values()){if(!I(a.get(re.srcId)))continue;let ue=D.get(re.srcId);if(!ue?.has(re.srcSide))continue;let le=P(re);if(ue.has(le)||(_.get(A(re.srcId,le))??0)>0)continue;let he=_.get(A(re.srcId,re.srcSide))??0;_.set(A(re.srcId,re.srcSide),Math.max(0,he-1)),_.set(A(re.srcId,le),1),re.srcSide=le}for(let re of R.values()){let{edgeIdx:ue,srcId:le,dstId:he,srcSide:Te,dstSide:oe}=re,G=a.get(le),X=a.get(he),ee=`${le}:${Te}:src`,q=Te==="top"||Te==="bottom"?X.x??0:X.y??0;C.has(ee)||C.set(ee,[]),C.get(ee).push({edgeIdx:ue,oppositeCoord:q});let ie=`${he}:${oe}:dst`,j=oe==="top"||oe==="bottom"?G.x??0:G.y??0;C.has(ie)||C.set(ie,[]),C.get(ie).push({edgeIdx:ue,oppositeCoord:j})}let L=new Map,B=8;for(let[re,ue]of C){if(ue.length<2)continue;ue.sort((Se,Ce)=>Se.oppositeCoord-Ce.oppositeCoord);let le=re.split(":"),he=le.slice(0,-2).join(":"),Te=le[le.length-2],oe=le[le.length-1],G=a.get(he);if(!G)continue;let ee=Te==="left"||Te==="right"?G.height??10:G.width??10,q=G.shape,j=q==="question"||q==="diamond"?ee*.3:ee,Y=Math.min(20,Math.max(B,j/(ue.length+1))),me=-(Y*(ue.length-1))/2;for(let[Se,Ce]of ue.entries()){let Me=me+Se*Y,_e=`${Ce.edgeIdx}:${oe}`;L.set(_e,Me)}}let O=s(re=>!!i[re]?.labelNodeId,"edgeHasLabelNode"),$=s((re,ue)=>re?(C.get(`${re}:${ue}:src`)??[]).some(({edgeIdx:le})=>O(le))||(C.get(`${re}:${ue}:dst`)??[]).some(({edgeIdx:le})=>O(le)):!1,"faceHasLabelNode"),W=s((re,ue,le)=>ue==="top"||ue==="bottom"?{x:re.x+le,y:re.y}:{x:re.x,y:re.y+le},"applyPortOffset"),V=s((re,ue,le)=>{let he=R.get(re),Te={x:le.x??0,y:le.y??0},oe={x:ue.x??0,y:ue.y??0},G=he?.srcSide??k(ue,Te),X=he?.dstSide??k(le,oe),ee=he?p(ue,he.srcSide):m(ue,Te,!0),q=he?p(le,he.dstSide):m(le,oe,!1),ie=L.get(`${re}:src`),j=L.get(`${re}:dst`);return ie!==void 0&&(ee=W(ee,G,ie)),j!==void 0&&(q=W(q,X,j)),{pSrcPort:ee,pDstPort:q,srcSide:G,dstSide:X}},"portsForEdge");for(let re of T){let ue=i[re];if(y[re]=[],!ue.start||!ue.end||ue.points&&ue.points.length>0||ue.start===ue.end)continue;let le=a.get(ue.start),he=a.get(ue.end);if(!le||!he)continue;let{pSrcPort:Te,pDstPort:oe,srcSide:G,dstSide:X}=V(re,le,he),ee={...Te},q={...oe},ie=G==="top"||G==="bottom",j=X==="top"||X==="bottom";if(ie){let et=Te.y>(le.y??0);ee.y=et?Te.y+El:Te.y-El}else{let et=Te.x>(le.x??0);ee.x=et?Te.x+El:Te.x-El}if(j){let et=oe.y>(he.y??0);q.y=et?oe.y+El:oe.y-El}else{let et=oe.x>(he.x??0);q.x=et?oe.x+El:oe.x-El}let U=s((et,pt)=>{for(let kt of d)if(!pt.includes(kt.nodeId)&&et.x>kt.minX&&et.xkt.minY&&et.y{if(tr){let zr=et.y>(pt.y??0);return{x:(kt.x??0)>=et.x?vr.maxX+Ym:vr.minX-Ym,y:zr?vr.maxY+v1:vr.minY-v1,leavesPositiveSide:zr}}let Ur=et.x>(pt.x??0),on=(kt.y??0)>=et.y;return{x:Ur?vr.maxX+Ym:vr.minX-Ym,y:on?vr.maxY+v1:vr.minY-v1,leavesPositiveSide:Ur}},"obstacleDetour"),Ae=[],me=[ue.start,ue.end],Se=U(ee,me);if(Se.inside&&Se.obstacle){let et=Se.obstacle;if(ie){let pt=Y(Te,le,he,et,!0);ee.x=pt.x,ee.y=pt.y;let kt=pt.leavesPositiveSide?Math.min(et.minY-2,Te.y+El):Math.max(et.maxY+2,Te.y-El);Ae=[{x:Te.x,y:kt},{x:pt.x,y:kt},{x:pt.x,y:pt.y}]}else{let pt=Y(Te,le,he,et,!1),kt=pt.leavesPositiveSide?Math.min(et.minX-2,Te.x+El):Math.max(et.maxX+2,Te.x-El);ee.x=pt.x,ee.y=pt.y,Ae=[{x:kt,y:Te.y},{x:kt,y:pt.y},{x:pt.x,y:pt.y}]}}let Ce=[],Me=U(q,me);if(Me.inside&&Me.obstacle){let et=Me.obstacle;if(j){let pt=Y(oe,he,le,et,!0);q.x=pt.x,q.y=pt.y,Ce=[{x:pt.x,y:pt.y},{x:oe.x,y:pt.y}]}else{let pt=Y(oe,he,le,et,!1);q.x=pt.x,q.y=pt.y,Ce=[{x:pt.x,y:pt.y},{x:pt.x,y:oe.y}]}}if(Ae.length===0&&Ce.length===0){let et=Ym,pt=Math.abs(ee.x-q.x)1||Ur>1,zr=S.get(ue.start??"")??0,Pt=S.get(ue.end??"")??0,Et=tr>1&&$(ue.start,G)||Ur>1&&$(ue.end,X),Ot=tr<=1||zr<=2,zt=Ur<=1||Pt<=2;if((pt||kt)&&!vr&&(!on||on&&!Et&&Ot&&zt)&&!w(Te,oe,ue.start,ue.end)){ue.points=[{...Te},{...ee},{...q},{...oe}],v.add(re);let Re=kt?"horizontal":"vertical",ln=kt?Te.y:Te.x,Ge=kt?Math.min(Te.x,oe.x):Math.min(Te.y,oe.y),_r=kt?Math.max(Te.x,oe.x):Math.max(Te.y,oe.y),Yi={id:`fast-path-${Re}-${ln.toFixed(0)}-${re}`,orientation:Re,coord:ln,spanMin:Ge,spanMax:_r,tracks:[]};g.push({edgeIndex:re,segmentIndex:0,orientation:Re,pipe:Yi,trackIndex:0,from:Ge,to:_r});continue}}let _e=f("vertical",ee.x,ee.y,ee.y);ee.x=_e.coord;let ze=f("vertical",q.x,q.y,q.y);q.x=ze.coord;let Le=Math.min(ee.x,q.x)-50,qe=Math.max(ee.x,q.x)+50,ve=Math.min(ee.y,q.y)-50,lt=Math.max(ee.y,q.y)+50;for(let et of d){let pt=Math.min(ee.x,q.x),kt=Math.max(ee.x,q.x),vr=Math.min(ee.y,q.y),tr=Math.max(ee.y,q.y);et.minXpt&&et.minYvr&&(Le=Math.min(Le,et.minX-c5),qe=Math.max(qe,et.maxX+c5),ve=Math.min(ve,et.minY-c5),lt=Math.max(lt,et.maxY+c5))}for(let et of d){if(et.maxXqe||et.maxYlt)continue;let pt=Ym;f("horizontal",et.minY-pt,Le,qe),f("horizontal",et.maxY+pt,Le,qe);let kt=v1;f("vertical",et.minX-kt,ve,lt),f("vertical",et.maxX+kt,ve,lt)}f("horizontal",ee.y,Le,qe),f("horizontal",q.y,Le,qe);let Oe=l.filter(et=>et.orientation==="horizontal"&&et.coord>=ve&&et.coord<=lt),mt=l.filter(et=>et.orientation==="vertical"&&et.coord>=Le&&et.coord<=qe),At=s((et,pt)=>`${et.toFixed(1)},${pt.toFixed(1)}`,"getKey"),Gt=At(ee.x,ee.y),Ct=At(q.x,q.y),Rt=new Map,gt=new Map,be=new Map,ct=new Set,xt=[];Rt.set(Gt,0),be.set(Gt,"n"),xt.push({key:Gt,f:Math.hypot(q.x-ee.x,q.y-ee.y),pt:ee}),ct.add(Gt);let Ke=[],De=s((et,pt)=>w(et,pt,ue.start,ue.end),"checkSegmentBlocked"),tt={x:q.x,y:ee.y},Ze=De(ee,tt),dt=De(tt,q),Ut=Ze||dt,$t={x:ee.x,y:q.y},Yt=De(ee,$t),Sr=De($t,q);if(Ut?Yt||Sr||(Math.abs(ee.x-q.x)0;){xt.sort((Pt,Et)=>Pt.f-Et.f);let et=xt.shift();if(ct.delete(et.key),et.key===Ct){let Pt=Ct,Et=q;for(Ke=[Et];gt.has(Pt);){let Ot=gt.get(Pt);Ke.unshift(Ot),Et=Ot,Pt=At(Ot.x,Ot.y)}break}let pt=et.pt.x,kt=et.pt.y,vr=mt.sort((Pt,Et)=>Pt.coord-Et.coord),tr=vr.findIndex(Pt=>Math.abs(Pt.coord-pt)<1),Ur=Oe.sort((Pt,Et)=>Pt.coord-Et.coord),on=Ur.findIndex(Pt=>Math.abs(Pt.coord-kt)<1),zr=[];tr>0&&zr.push({x:vr[tr-1].coord,y:kt}),tr>=0&&tr0&&zr.push({x:pt,y:Ur[on-1].coord}),on>=0&&on$e.nodeId===ue.start||$e.nodeId===ue.end?!1:Et!==Ot?$e.minYkt&&$e.maxX>Et&&$e.minXpt&&$e.maxY>zt&&$e.minY10&&Qu<-5||ll<-10&&Qu>5)&&(_r=Math.abs(Qu)*100),(Yi>10&&Cd<-5||Yi<-10&&Cd>5)&&(_r+=Math.abs(Cd)*50);let O0=0,bi=be.get(et.key)??"n",No=Math.abs(Cd)>Jr?"h":"v";bi!=="n"&&bi!==No&&(O0=50);let Dc=ln+Ge+_r+O0,cl=(Rt.get(et.key)??1/0)+Dc,Bt=Math.abs(q.x-Pt.x)+Math.abs(q.y-Pt.y);if(cl<(Rt.get(Re)??1/0))if(gt.set(Re,et.pt),Rt.set(Re,cl),be.set(Re,No),!ct.has(Re))xt.push({key:Re,f:cl+Bt,pt:Pt}),ct.add(Re);else{let $e=xt.findIndex(nt=>nt.key===Re);$e!==-1&&(xt[$e].f=cl+Bt)}}}if(Ke.length===0&&(Ke=[ee,{x:ee.x,y:q.y},q]),Ke.length>4){let et=Ke[0],pt=Ke[Ke.length-1],kt=Math.min(et.x,pt.x),vr=Math.max(et.x,pt.x),tr=Math.min(et.y,pt.y),Ur=Math.max(et.y,pt.y);for(let zt of Ke)kt=Math.min(kt,zt.x),vr=Math.max(vr,zt.x),tr=Math.min(tr,zt.y),Ur=Math.max(Ur,zt.y);let on=vr>Math.max(et.x,pt.x),zr=ktGe.minXwt&&Ge.minYcr);if(ln.length>0){let Ge=Math.max(et.x,pt.x);for(let _r of ln){let Yi=(_r.minX+_r.maxX)/2;if(_r.visualXHalfExtent===void 0||isNaN(_r.visualXHalfExtent))continue;let ll=Yi+_r.visualXHalfExtent+zt;Ge=Math.max(Ge,ll)}isNaN(Ge)||(vr=Ge)}}if(zr){let wt=d.filter(cr=>cr.minXMath.min(et.y,pt.y));if(wt.length>0){let cr=Math.min(et.x,pt.x);for(let Re of wt){let Ge=(Re.minX+Re.maxX)/2-Re.visualXHalfExtent-zt;cr=Math.min(cr,Ge)}kt=cr}}}let Pt=s(zt=>{let wt=pt.y>et.y,cr=d.filter(Ge=>{let _r=Math.min(et.x,pt.x)Ge.minX,Yi=Math.min(et.y,pt.y)Ge.minY;return _r&&Yi}),Re=cr;if(u&&cr.length>0){let Ge=cr.filter(_r=>_r.minXzt);Ge.length>0&&(Re=Ge)}if(Re.length===0)return pt.y;let ln=Ym;if(wt){let _r=Math.max(...Re.map(Yi=>Yi.maxY))+ln;if(_rYi.minY))-ln;if(_r>pt.y+Jr)return _r}return pt.y},"findBestReturnY"),Et=s(zt=>{let wt=Pt(zt),cr={x:zt,y:et.y},Re={x:zt,y:wt},ln={x:pt.x,y:wt},Ge=De(et,cr),_r=De(cr,Re),Yi=De(Re,ln),ll=wt!==pt.y?De(ln,pt):!1;return!Ge&&!_r&&!Yi&&!ll?Math.abs(wt-pt.y)=3){let et=Wt[Wt.length-1],pt=Wt[Wt.length-2],kt=Wt[Wt.length-3],vr=Math.abs(kt.y-pt.y)Math.abs(et.x-kt.x)&&Wt.splice(-2,1)}else if(tr){let Ur=Math.sign(pt.y-kt.y),on=Math.sign(et.y-kt.y);Ur!==0&&Ur===on&&Math.abs(pt.y-kt.y)>Math.abs(et.y-kt.y)&&Wt.splice(-2,1)}}let ar=[Wt[0]];for(let et=1;etpt.x,Ur=vr.x>kt.x;if(tr!==Ur){ar.push(kt);continue}continue}if(Math.abs(pt.x-kt.x)pt.y,Ur=vr.y>kt.y;if(tr!==Ur){ar.push(kt);continue}continue}ar.push(kt)}ar.push(Wt[Wt.length-1]);for(let et=0;etre.from{let Te=!he.segments.some(G=>(G.edgeIndex!==ue.edgeIndex||G.segmentIndex!==ue.segmentIndex)&&z(G,re)),oe=!le.segments.some(G=>(G.edgeIndex!==re.edgeIndex||G.segmentIndex!==re.segmentIndex)&&z(G,ue));return Te&&oe?(re.trackIndex=he.index,ue.trackIndex=le.index,le.segments=[...le.segments.filter(G=>G.edgeIndex!==re.edgeIndex||G.segmentIndex!==re.segmentIndex),{edgeIndex:ue.edgeIndex,segmentIndex:ue.segmentIndex,from:ue.from,to:ue.to}],he.segments=[...he.segments.filter(G=>G.edgeIndex!==ue.edgeIndex||G.segmentIndex!==ue.segmentIndex),{edgeIndex:re.edgeIndex,segmentIndex:re.segmentIndex,from:re.from,to:re.to}],!0):!1},"trySwapSegmentsAcrossTracks"),K=s(re=>{let ue=re.tracks.length;return re.tracks[ue]={index:ue,coord:re.coord,segments:[]},ue},"createNewTrack"),J=s((re,ue)=>{let le=re.pipe.tracks[re.trackIndex];le.segments=le.segments.filter(Te=>Te.edgeIndex!==re.edgeIndex||Te.segmentIndex!==re.segmentIndex),re.trackIndex=ue,re.pipe.tracks[ue].segments.push({edgeIndex:re.edgeIndex,segmentIndex:re.segmentIndex,from:re.from,to:re.to})},"moveSegmentToTrack"),ne=s((re,ue)=>{let le=y[re.edgeIndex];for(let he of le){let Te=g[he];Te.pipe===re.pipe&&J(Te,ue)}},"moveSegmentChainToTrack"),Z=s(re=>{let ue=y[re.edgeIndex],le=ue.indexOf(g.indexOf(re)),he=[];return le>0&&he.push(g[ue[le-1]]),le{if(re.orientation===ue.orientation)return!1;let le=re.orientation==="horizontal"?re:ue,he=re.orientation==="horizontal"?ue:re;return he.pipe.coord>le.from&&he.pipe.coordhe.from&&le.pipe.coord{for(let le of re.tracks)if(!le.segments.some(Te=>(Te.edgeIndex!==ue.edgeIndex||Te.segmentIndex!==ue.segmentIndex)&&z(Te,ue)))return le.index;return-1},"findAvailableTrack"),fe=s((re,ue)=>{if(re.trackIndex===ue.trackIndex)return z(re,ue);let le=Z(re),he=Z(ue);return le.some(Te=>he.some(oe=>pe(Te,oe)))},"segmentsConflict"),ce=s((re,ue,le)=>{if(H(re,ue,re.pipe.tracks[re.trackIndex],ue.pipe.tracks[ue.trackIndex]))return;let he=te(re.pipe,ue);le(ue,he!==-1?he:K(re.pipe))},"resolveTrackConflict"),de=s(re=>{let ue=0;for(let le=0;le{if(Ee.has(re))return Ee.get(re);let ue=y[re];if(ue.length===0){let X={dest:0,deviation:0,base:0,delta:0};return Ee.set(re,X),X}let he=g[ue[0]].pipe.coord,Te=he;for(let X=1;XMath.abs(ie-he)?q:ie;break}}let oe=Math.abs(Te-he),G={dest:Te,deviation:oe,base:he,delta:Te-he};return Ee.set(re,G),G},"getDestInfo"),Fe=s(()=>{let re=0,ue=new Map;for(let[he,Te]of i.entries())y[he].length!==0&&Te.start&&(ue.has(Te.start)||ue.set(Te.start,[]),ue.get(Te.start).push(he));let le=s(he=>{let Te=i[he];if(!Te.start||!Te.end)return 0;let oe=a.get(Te.start),G=a.get(Te.end);if(!oe||!G)return 0;let X=(G.x??0)-(oe.x??0),ee=(G.y??0)-(oe.y??0);return Math.abs(X)+Math.abs(ee)},"getEdgeDistance");for(let he of ue.values()){he.sort((oe,G)=>{let X=we(oe),ee=we(G);if(Math.abs(X.deviation-ee.deviation)>1)return X.deviation-ee.deviation;if(Math.abs(X.dest-ee.dest)>1)return X.dest-ee.dest;let q=le(oe),ie=le(G);if(Math.abs(q-ie)>1)return ie-q;let j=y[oe].length,U=y[G].length;if(j!==U)return j-U;if(j===1){let Y=y[oe][0],Ae=y[G][0];if(g[Y]&&g[Ae]){let me=g[Y],Se=g[Ae],Ce=Math.abs(me.to-me.from),Me=Math.abs(Se.to-Se.from);if(Math.abs(Ce-Me)>1)return Ce-Me}}return 0});let Te=he.map(oe=>g[y[oe][0]]);re+=de(Te)}return re},"fixSourceHandleCrossings"),Ye=s(()=>{let re=0,ue=new Map;for(let[le,he]of i.entries())y[le].length!==0&&he.end&&(ue.has(he.end)||ue.set(he.end,[]),ue.get(he.end).push(le));for(let le of ue.values()){le.sort((Te,oe)=>{let G=s(q=>{let ie=y[q];if(ie.length<2)return 0;let j=g[ie[ie.length-2]];return Math.abs(j.to-j.from)},"getDist"),X=G(Te),ee=G(oe);return Math.abs(X-ee)>.1?X-ee:Te-oe});let he=le.map(Te=>g[y[Te][y[Te].length-1]]);re+=de(he)}return re},"fixTargetHandleCrossings"),He=s(()=>{let re=0;for(let ue of l){let le=[];for(let he of ue.tracks)for(let Te of he.segments){let oe=y[Te.edgeIndex].find(G=>g[G].segmentIndex===Te.segmentIndex);oe!==void 0&&le.push(g[oe])}le.sort((he,Te)=>he.edgeIndex-Te.edgeIndex||he.segmentIndex-Te.segmentIndex);for(let he=0;he{he.segments.forEach(Te=>{ue.push({edgeIndex:Te.edgeIndex,segmentIndex:Te.segmentIndex,trackIndex:he.index,from:Te.from,to:Te.to})})}),ue.sort((he,Te)=>he.from-Te.from);let le=[];if(ue.length>0){let he=[ue[0]],Te=ue[0].to;for(let oe=1;oeTe.add(Y.trackIndex));let oe=new Map;he.forEach(Y=>{let Ae=we(Y.edgeIndex);oe.set(Y.trackIndex,(oe.get(Y.trackIndex)??0)+Ae.delta)});let G=[...Te].filter(Y=>(oe.get(Y)??0)<-1),X=[...Te].filter(Y=>(oe.get(Y)??0)>1),ee=[...Te].filter(Y=>Math.abs(oe.get(Y)??0)<=1);G.sort((Y,Ae)=>(oe.get(Ae)??0)-(oe.get(Y)??0)),X.sort((Y,Ae)=>(oe.get(Y)??0)-(oe.get(Ae)??0));let q=s((Y,Ae)=>{he.filter(me=>me.trackIndex===Y).forEach(me=>{let Se=v.has(me.edgeIndex)?re.coord:Ae;Ie.set(`${me.edgeIndex}-${me.segmentIndex}`,Se)})},"assignCoord"),ie=0;for(let Y of G)ie++,q(Y,re.coord-ie*EB);if(ee.length===0&&Te.size>0){let Y=[...Te].sort((Se,Ce)=>Math.abs(oe.get(Se)??0)-Math.abs(oe.get(Ce)??0))[0],Ae=G.indexOf(Y);Ae!==-1&&G.splice(Ae,1);let me=X.indexOf(Y);me!==-1&&X.splice(me,1),ee.push(Y)}let j=0;for(let Y of ee){if(j===0)q(Y,re.coord);else{let Ae=j%2===1?1:-1,me=Math.ceil(j/2);q(Y,re.coord+Ae*me*EB*.5)}j++}let U=0;for(let Y of X)U++,q(Y,re.coord+U*EB)}}for(let[re,ue]of i.entries()){let le=y[re]??[];if(le.length===0)continue;let he=[],Te=a.get(ue.start),oe=a.get(ue.end),{pSrcPort:G,pDstPort:X}=V(re,Te,oe),ee=le.map(j=>{let U=g[j],Y=Ie.get(`${U.edgeIndex}-${U.segmentIndex}`)??U.pipe.coord;return{orient:U.orientation,coord:Y,from:U.from,to:U.to}});he.push(G);for(let j=0;jJr&&he.push(x1(U,Ae)),Ce&&Se.orient===U.orient)if(Math.abs(U.coord-Se.coord)>Jr){let Me=U.orient==="vertical"?(Ae+Se.from)/2:vve(U,Se);he.push(x1(U,Me),x1(Se,Me))}else(j===0||j===ee.length-2)&&he.push(x1(U,vve(U,Se)));else if(Ce)he.push(x1(U,Se.coord));else{let Me=Math.abs(U.from-Ae)Jr||Math.abs(q.y-X.y)>Jr)&&he.push(X);let ie=[];he.length>0&&ie.push(he[0]);for(let j=1;jJr||Math.abs(U.y-Y.y)>Jr)&&ie.push(U)}ue.points=ie}for(let re of i){let ue=re.__originalEdge;ue&&re.points&&(ue.points=re.points)}e.edges=(e.edges??[]).filter(re=>!re.isLayoutOnly);let Ne=s((re,ue)=>{let le=ue.x??0,he=ue.y??0,Te=ue.width??0,oe=ue.height??0;if(Te<=0||oe<=0)return re;let G=le-Te/2,X=le+Te/2,ee=he-oe/2,q=he+oe/2;if(re.xX||re.yq)return re;let ie=re.x-G,j=X-re.x,U=re.y-ee,Y=q-re.y,Ae=Math.min(ie,j,U,Y);return Ae===ie?{x:G,y:re.y}:Ae===j?{x:X,y:re.y}:Ae===U?{x:re.x,y:ee}:{x:re.x,y:q}},"nodeBoundaryClamp");for(let re of e.edges){let ue=re.points;if(!ue||ue.length<2)continue;let le=re.start,he=re.end,Te=le?a.get(le):void 0,oe=he?a.get(he):void 0;Te&&(ue[0]=Ne(ue[0],Te)),oe&&(ue[ue.length-1]=Ne(ue[ue.length-1],oe))}return e}var Jr,hct,Ym,v1,c5,El,EB,bve=F(()=>{"use strict";y1();Jr=N1e.EPSILON,hct=8,Ym=15,v1=15,c5=25,El=20,EB=10;s(yve,"chooseOrthogonalSide");s(vve,"sharedLineEndpointCoord");s(x1,"pointOnLine");s(xve,"routeEdgesOrthogonal")});function dct(e){return e.direction??"TB"}function Tve(e){let t=Iye(e),r=e.config.flowchart?.nodeSpacing??40,n=e.config.flowchart?.rankSpacing??100,i=e.config.swimlane?.ignoreCrossLaneEdges??!0,a=e.config.swimlane?.optimizeRanksByCrossings??!0,o=e.config.swimlane?.automaticLaneOrdering??!1,l=dct(e),{ordered:u,coordinates:h}=mve(t,{nodeGap:r,layerGap:n,ignoreCrossLaneEdges:i,optimizeRanksByCrossings:a,automaticLaneOrdering:o,direction:l});Mye(t,u,h,{nodeGap:r,layerGap:n});for(let d of e.edges??[])delete d.points;xve(e,l);for(let d of e.edges??[])(!d.curve||d.curve==="basis")&&(d.curve="rounded");return L1e(e,l),yB(e),l}var Cve=F(()=>{"use strict";D1e();nB();gve();bve();s(dct,"getSwimlaneDirection");s(Tve,"runSwimlaneLayoutCore")});var Sve={};sr(Sve,{render:()=>pct});function fct(e){Dye(e);let t=Nye(e);e.nodes=t.nodes,e.edges=t.edges}var pct,wve=F(()=>{"use strict";H3();_ye();nB();Pye();Cve();s(fct,"prepareSwimlaneLayout");pct=f1({prepareLayout:fct,runLayoutCore:Tve,afterPaint:Rye})});function nF(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r=e.length?{done:!0}:{done:!1,value:e[n++]}},"n"),e:s(function(u){throw u},"e"),f:i}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var a,o=!0,l=!1;return{s:s(function(){r=r.call(e)},"s"),n:s(function(){var u=r.next();return o=u.done,u},"n"),e:s(function(u){l=!0,a=u},"e"),f:s(function(){try{o||r.return==null||r.return()}finally{if(l)throw a}},"f")}}function Jbe(e,t,r){return(t=e2e(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function vct(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function xct(e,t){var r=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(r!=null){var n,i,a,o,l=[],u=!0,h=!1;try{if(a=(r=r.call(e)).next,t===0){if(Object(r)!==r)return;u=!1}else for(;!(u=(n=a.call(r)).done)&&(l.push(n.value),l.length!==t);u=!0);}catch(d){h=!0,i=d}finally{try{if(!u&&r.return!=null&&(o=r.return(),Object(o)!==o))return}finally{if(h)throw i}}return l}}function bct(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Tct(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function aa(e,t){return mct(e)||xct(e,t)||SF(e,t)||bct()}function N5(e){return gct(e)||vct(e)||SF(e)||Tct()}function Cct(e,t){if(typeof e!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var n=r.call(e,t);if(typeof n!="object")return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}function e2e(e){var t=Cct(e,"string");return typeof t=="symbol"?t:t+""}function ya(e){"@babel/helpers - typeof";return ya=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ya(e)}function SF(e,t){if(e){if(typeof e=="string")return nF(e,t);var r={}.toString.call(e).slice(8,-1);return r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set"?Array.from(e):r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?nF(e,t):void 0}}function HT(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function UT(){if(Ave)return AB;Ave=1;function e(t){var r=typeof t;return t!=null&&(r=="object"||r=="function")}return s(e,"isObject"),AB=e,AB}function Hct(){if(Rve)return RB;Rve=1;var e=typeof u5=="object"&&u5&&u5.Object===Object&&u5;return RB=e,RB}function X5(){if(_ve)return _B;_ve=1;var e=Hct(),t=typeof self=="object"&&self&&self.Object===Object&&self,r=e||t||Function("return this")();return _B=r,_B}function Uct(){if(Lve)return LB;Lve=1;var e=X5(),t=s(function(){return e.Date.now()},"now");return LB=t,LB}function Yct(){if(Dve)return DB;Dve=1;var e=/\s/;function t(r){for(var n=r.length;n--&&e.test(r.charAt(n)););return n}return s(t,"trimmedEndIndex"),DB=t,DB}function jct(){if(Ive)return IB;Ive=1;var e=Yct(),t=/^\s+/;function r(n){return n&&n.slice(0,e(n)+1).replace(t,"")}return s(r,"baseTrim"),IB=r,IB}function EF(){if(Mve)return MB;Mve=1;var e=X5(),t=e.Symbol;return MB=t,MB}function Xct(){if(Nve)return NB;Nve=1;var e=EF(),t=Object.prototype,r=t.hasOwnProperty,n=t.toString,i=e?e.toStringTag:void 0;function a(o){var l=r.call(o,i),u=o[i];try{o[i]=void 0;var h=!0}catch{}var d=n.call(o);return h&&(l?o[i]=u:delete o[i]),d}return s(a,"getRawTag"),NB=a,NB}function Kct(){if(Pve)return PB;Pve=1;var e=Object.prototype,t=e.toString;function r(n){return t.call(n)}return s(r,"objectToString"),PB=r,PB}function c2e(){if(Ove)return OB;Ove=1;var e=EF(),t=Xct(),r=Kct(),n="[object Null]",i="[object Undefined]",a=e?e.toStringTag:void 0;function o(l){return l==null?l===void 0?i:n:a&&a in Object(l)?t(l):r(l)}return s(o,"baseGetTag"),OB=o,OB}function Zct(){if(Bve)return BB;Bve=1;function e(t){return t!=null&&typeof t=="object"}return s(e,"isObjectLike"),BB=e,BB}function YT(){if($ve)return $B;$ve=1;var e=c2e(),t=Zct(),r="[object Symbol]";function n(i){return typeof i=="symbol"||t(i)&&e(i)==r}return s(n,"isSymbol"),$B=n,$B}function Qct(){if(Fve)return FB;Fve=1;var e=jct(),t=UT(),r=YT(),n=NaN,i=/^[-+]0x[0-9a-f]+$/i,a=/^0b[01]+$/i,o=/^0o[0-7]+$/i,l=parseInt;function u(h){if(typeof h=="number")return h;if(r(h))return n;if(t(h)){var d=typeof h.valueOf=="function"?h.valueOf():h;h=t(d)?d+"":d}if(typeof h!="string")return h===0?h:+h;h=e(h);var f=a.test(h);return f||o.test(h)?l(h.slice(2),f?2:8):i.test(h)?n:+h}return s(u,"toNumber"),FB=u,FB}function Jct(){if(Gve)return GB;Gve=1;var e=UT(),t=Uct(),r=Qct(),n="Expected a function",i=Math.max,a=Math.min;function o(l,u,h){var d,f,p,m,g,y,v=0,x=!1,b=!1,T=!0;if(typeof l!="function")throw new TypeError(n);u=r(u)||0,e(h)&&(x=!!h.leading,b="maxWait"in h,p=b?i(r(h.maxWait)||0,u):p,T="trailing"in h?!!h.trailing:T);function w(A){var I=d,D=f;return d=f=void 0,v=A,m=l.apply(D,I),m}s(w,"invokeFunc");function C(A){return v=A,g=setTimeout(R,u),x?w(A):m}s(C,"leadingEdge");function S(A){var I=A-y,D=A-v,L=u-I;return b?a(L,p-D):L}s(S,"remainingWait");function k(A){var I=A-y,D=A-v;return y===void 0||I>=u||I<0||b&&D>=p}s(k,"shouldInvoke");function R(){var A=t();if(k(A))return M(A);g=setTimeout(R,S(A))}s(R,"timerExpired");function M(A){return g=void 0,T&&d?w(A):(d=f=void 0,m)}s(M,"trailingEdge");function P(){g!==void 0&&clearTimeout(g),v=0,d=y=f=g=void 0}s(P,"cancel");function N(){return g===void 0?m:M(t())}s(N,"flush");function _(){var A=t(),I=k(A);if(d=arguments,f=this,y=A,I){if(g===void 0)return C(y);if(b)return clearTimeout(g),g=setTimeout(R,u),w(y)}return g===void 0&&(g=setTimeout(R,u)),m}return s(_,"debounced"),_.cancel=P,_.flush=N,_}return s(o,"debounce"),GB=o,GB}function iut(e,t,r,n,i){var a=i*Math.PI/180,o=Math.cos(a)*(e-r)-Math.sin(a)*(t-n)+r,l=Math.sin(a)*(e-r)+Math.cos(a)*(t-n)+n;return{x:o,y:l}}function sut(e,t,r){if(r===0)return e;var n=(t.x1+t.x2)/2,i=(t.y1+t.y2)/2,a=t.w/t.h,o=1/a,l=iut(e.x,e.y,n,i,r),u=aut(l.x,l.y,n,i,a,o);return{x:u.x,y:u.y}}function yut(){return Hve||(Hve=1,(function(e,t){(function(){var r,n,i,a,o,l,u,h,d,f,p,m,g,y,v;i=Math.floor,f=Math.min,n=s(function(x,b){return xb?1:0},"defaultCmp"),d=s(function(x,b,T,w,C){var S;if(T==null&&(T=0),C==null&&(C=n),T<0)throw new Error("lo must be non-negative");for(w==null&&(w=x.length);TP;0<=P?M++:M--)R.push(M);return R}).apply(this).reverse(),k=[],w=0,C=S.length;wN;0<=N?++R:--R)_.push(o(x,T));return _},"nsmallest"),y=s(function(x,b,T,w){var C,S,k;for(w==null&&(w=n),C=x[T];T>b;){if(k=T-1>>1,S=x[k],w(C,S)<0){x[T]=S,T=k;continue}break}return x[T]=C},"_siftdown"),v=s(function(x,b,T){var w,C,S,k,R;for(T==null&&(T=n),C=x.length,R=b,S=x[b],w=2*b+1;w-1}return s(t,"listCacheHas"),m$=t,m$}function udt(){if(Ixe)return g$;Ixe=1;var e=tA();function t(r,n){var i=this.__data__,a=e(i,r);return a<0?(++this.size,i.push([r,n])):i[a][1]=n,this}return s(t,"listCacheSet"),g$=t,g$}function hdt(){if(Mxe)return y$;Mxe=1;var e=sdt(),t=odt(),r=ldt(),n=cdt(),i=udt();function a(o){var l=-1,u=o==null?0:o.length;for(this.clear();++l-1&&n%1==0&&n0;){var d=i.shift();t(d),a.add(d.id()),l&&n(i,a,d)}return e}function G2e(e,t,r){if(r.isParent())for(var n=r._private.children,i=0;i0&&arguments[0]!==void 0?arguments[0]:Cft,t=arguments.length>1?arguments[1]:void 0,r=0;r0?_=I:N=I;while(Math.abs(A)>o&&++D=a?b(P,D):L===0?D:w(P,N,N+h)}s(C,"getTForX");var S=!1;function k(){S=!0,(e!==t||r!==n)&&T()}s(k,"precompute");var R=s(function(N){return S||k(),e===t&&r===n?N:N===0?0:N===1?1:v(C(N),t,n)},"f");R.getControlPoints=function(){return[{x:e,y:t},{x:r,y:n}]};var M="generateBezier("+[e,t,r,n]+")";return R.toString=function(){return M},R}function Cbe(e,t,r,n,i){if(n===1||t===r)return r;var a=i(t,r,n);return e==null||((e.roundValue||e.color)&&(a=Math.round(a)),e.min!==void 0&&(a=Math.max(a,e.min)),e.max!==void 0&&(a=Math.min(a,e.max))),a}function Sbe(e,t){return e.pfValue!=null||e.value!=null?e.pfValue!=null&&(t==null||t.type.units!=="%")?e.pfValue:e.value:e}function C1(e,t,r,n,i){var a=i!=null?i.type:null;r<0?r=0:r>1&&(r=1);var o=Sbe(e,i),l=Sbe(t,i);if(Vt(o)&&Vt(l))return Cbe(a,o,l,r,n);if(Xn(o)&&Xn(l)){for(var u=[],h=0;h0?(m==="spring"&&g.push(o.duration),o.easingImpl=R5[m].apply(null,g)):o.easingImpl=R5[m]}var y=o.easingImpl,v;if(o.duration===0?v=1:v=(r-u)/o.duration,o.applying&&(v=o.progress),v<0?v=0:v>1&&(v=1),o.delay==null){var x=o.startPosition,b=o.position;if(b&&i&&!e.locked()){var T={};ST(x.x,b.x)&&(T.x=C1(x.x,b.x,v,y)),ST(x.y,b.y)&&(T.y=C1(x.y,b.y,v,y)),e.position(T)}var w=o.startPan,C=o.pan,S=a.pan,k=C!=null&&n;k&&(ST(w.x,C.x)&&(S.x=C1(w.x,C.x,v,y)),ST(w.y,C.y)&&(S.y=C1(w.y,C.y,v,y)),e.emit("pan"));var R=o.startZoom,M=o.zoom,P=M!=null&&n;P&&(ST(R,M)&&(a.zoom=OT(a.minZoom,C1(R,M,v,y),a.maxZoom)),e.emit("zoom")),(k||P)&&e.emit("viewport");var N=o.style;if(N&&N.length>0&&i){for(var _=0;_=0;k--){var R=S[k];R()}S.splice(0,S.length)},"callbacks"),b=m.length-1;b>=0;b--){var T=m[b],w=T._private;if(w.stopped){m.splice(b,1),w.hooked=!1,w.playing=!1,w.started=!1,x(w.frames);continue}!w.playing&&!w.applying||(w.playing&&w.applying&&(w.applying=!1),w.started||Oft(d,T,e),Pft(d,T,e,f),w.applying&&(w.applying=!1),x(w.frames),w.step!=null&&w.step(e),T.completed()&&(m.splice(b,1),w.hooked=!1,w.playing=!1,w.started=!1,x(w.completes)),y=!0)}return!f&&m.length===0&&g.length===0&&n.push(d),y}s(i,"stepOne");for(var a=!1,o=0;o0?t.notify("draw",r):t.notify("draw")),r.unmerge(n),t.emit("step")}function iTe(e){this.options=kr({},qft,Hft,e)}function aTe(e){this.options=kr({},Uft,e)}function sTe(e){this.options=kr({},Yft,e)}function cA(e){this.options=kr({},jft,e),this.options.layout=this;var t=this.options.eles.nodes(),r=this.options.eles.edges(),n=r.filter(function(i){var a=i.source().data("id"),o=i.target().data("id"),l=t.some(function(h){return h.data("id")===a}),u=t.some(function(h){return h.data("id")===o});return!l||!u});this.options.eles=this.options.eles.not(n)}function uTe(e){this.options=kr({},cpt,e)}function VF(e){this.options=kr({},upt,e)}function hTe(e){this.options=kr({},hpt,e)}function dTe(e){this.options=kr({},dpt,e)}function fTe(e){this.options=e,this.notifications=0}function gTe(e,t){t.radius===0?e.lineTo(t.cx,t.cy):e.arc(t.cx,t.cy,t.radius,t.startAngle,t.endAngle,t.counterClockwise)}function qF(e,t,r,n){var i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0;return n===0||t.radius===0?{cx:t.x,cy:t.y,radius:0,startX:t.x,startY:t.y,stopX:t.x,stopY:t.y,startAngle:void 0,endAngle:void 0,counterClockwise:void 0}:(mpt(e,t,r,n,i),{cx:pF,cy:mF,radius:Qm,startX:pTe,startY:mTe,stopX:gF,stopY:yF,startAngle:Tu.ang+Math.PI/2*eg,endAngle:Al.ang-Math.PI/2*eg,counterClockwise:D5})}function yTe(e){var t=[];if(e!=null){for(var r=0;r5&&arguments[5]!==void 0?arguments[5]:5,o=Math.min(a,n/2,i/2);e.beginPath(),e.moveTo(t+o,r),e.lineTo(t+n-o,r),e.quadraticCurveTo(t+n,r,t+n,r+o),e.lineTo(t+n,r+i-o),e.quadraticCurveTo(t+n,r+i,t+n-o,r+i),e.lineTo(t+o,r+i),e.quadraticCurveTo(t,r+i,t,r+i-o),e.lineTo(t,r+o),e.quadraticCurveTo(t,r,t+o,r),e.closePath()}function qbe(e,t,r){var n=e.createShader(t);if(e.shaderSource(n,r),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(n));return n}function smt(e,t,r){var n=qbe(e,e.VERTEX_SHADER,t),i=qbe(e,e.FRAGMENT_SHADER,r),a=e.createProgram();if(e.attachShader(a,n),e.attachShader(a,i),e.linkProgram(a),!e.getProgramParameter(a,e.LINK_STATUS))throw new Error("Could not initialize shaders");return a}function omt(e,t,r){r===void 0&&(r=t);var n=e.makeOffscreenCanvas(t,r),i=n.context=n.getContext("2d");return n.clear=function(){return i.clearRect(0,0,n.width,n.height)},n.clear(),n}function YF(e){var t=e.pixelRatio,r=e.cy.zoom(),n=e.cy.pan();return{zoom:r*t,pan:{x:n.x*t,y:n.y*t}}}function lmt(e){var t=e.pixelRatio,r=e.cy.zoom();return r*t}function cmt(e,t,r,n,i){var a=n*r+t.x,o=i*r+t.y;return o=Math.round(e.canvasHeight-o),[a,o]}function umt(e,t){return t.picking?!0:e.pstyle("background-fill").value!=="solid"||e.pstyle("background-image").strValue!=="none"?!1:e.pstyle("border-width").value===0||e.pstyle("border-opacity").value===0?!0:e.pstyle("border-style").value==="solid"}function hmt(e,t){if(e.length!==t.length)return!1;for(var r=0;r>0&255)/255,r[1]=(e>>8&255)/255,r[2]=(e>>16&255)/255,r[3]=(e>>24&255)/255,r}function dmt(e){return e[0]+(e[1]<<8)+(e[2]<<16)+(e[3]<<24)}function fmt(e,t){var r=e.createTexture();return r.buffer=function(n){e.bindTexture(e.TEXTURE_2D,r),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR_MIPMAP_NEAREST),e.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!0),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,n),e.generateMipmap(e.TEXTURE_2D),e.bindTexture(e.TEXTURE_2D,null)},r.deleteTexture=function(){e.deleteTexture(r)},r}function DTe(e,t){switch(t){case"float":return[1,e.FLOAT,4];case"vec2":return[2,e.FLOAT,4];case"vec3":return[3,e.FLOAT,4];case"vec4":return[4,e.FLOAT,4];case"int":return[1,e.INT,4];case"ivec2":return[2,e.INT,4]}}function ITe(e,t,r){switch(t){case e.FLOAT:return new Float32Array(r);case e.INT:return new Int32Array(r)}}function pmt(e,t,r,n,i,a){switch(t){case e.FLOAT:return new Float32Array(r.buffer,a*n,i);case e.INT:return new Int32Array(r.buffer,a*n,i)}}function mmt(e,t,r,n){var i=DTe(e,t),a=aa(i,2),o=a[0],l=a[1],u=ITe(e,l,n),h=e.createBuffer();return e.bindBuffer(e.ARRAY_BUFFER,h),e.bufferData(e.ARRAY_BUFFER,u,e.STATIC_DRAW),l===e.FLOAT?e.vertexAttribPointer(r,o,l,!1,0,0):l===e.INT&&e.vertexAttribIPointer(r,o,l,0,0),e.enableVertexAttribArray(r),e.bindBuffer(e.ARRAY_BUFFER,null),h}function bu(e,t,r,n){var i=DTe(e,r),a=aa(i,3),o=a[0],l=a[1],u=a[2],h=ITe(e,l,t*o),d=o*u,f=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,f),e.bufferData(e.ARRAY_BUFFER,t*d,e.DYNAMIC_DRAW),e.enableVertexAttribArray(n),l===e.FLOAT?e.vertexAttribPointer(n,o,l,!1,d,0):l===e.INT&&e.vertexAttribIPointer(n,o,l,d,0),e.vertexAttribDivisor(n,1),e.bindBuffer(e.ARRAY_BUFFER,null);for(var p=new Array(t),m=0;mETe?(Imt(e),t.call(e,a)):(Mmt(e),OTe(e,a,IT.SCREEN)))}}{var r=e.matchCanvasSize;e.matchCanvasSize=function(a){r.call(e,a),e.pickingFrameBuffer.setFramebufferAttachmentSizes(e.canvasWidth,e.canvasHeight),e.pickingFrameBuffer.needsDraw=!0}}e.findNearestElements=function(a,o,l,u){return Fmt(e,a,o)};{var n=e.invalidateCachedZSortedEles;e.invalidateCachedZSortedEles=function(){n.call(e),e.pickingFrameBuffer.needsDraw=!0}}{var i=e.notify;e.notify=function(a,o){i.call(e,a,o),a==="viewport"||a==="bounds"?e.pickingFrameBuffer.needsDraw=!0:a==="background"&&e.drawing.invalidate(o,{type:"node-body"})}}}function Imt(e){var t=e.data.contexts[e.WEBGL];t.clear(t.COLOR_BUFFER_BIT|t.DEPTH_BUFFER_BIT)}function Mmt(e){var t=s(function(n){n.save(),n.setTransform(1,0,0,1,0,0),n.clearRect(0,0,e.canvasWidth,e.canvasHeight),n.restore()},"clear");t(e.data.contexts[e.NODE]),t(e.data.contexts[e.DRAG])}function Nmt(e){var t=e.canvasWidth,r=e.canvasHeight,n=YF(e),i=n.pan,a=n.zoom,o=Z$();M5(o,o,[i.x,i.y]),xF(o,o,[a,a]);var l=Z$();xmt(l,t,r);var u=Z$();return vmt(u,l,o),u}function PTe(e,t){var r=e.canvasWidth,n=e.canvasHeight,i=YF(e),a=i.pan,o=i.zoom;t.setTransform(1,0,0,1,0,0),t.clearRect(0,0,r,n),t.translate(a.x,a.y),t.scale(o,o)}function Pmt(e,t){e.drawSelectionRectangle(t,function(r){return PTe(e,r)})}function Omt(e){var t=e.data.contexts[e.NODE];t.save(),PTe(e,t),t.strokeStyle="rgba(0, 0, 0, 0.3)",t.beginPath(),t.moveTo(-1e3,0),t.lineTo(1e3,0),t.stroke(),t.beginPath(),t.moveTo(0,-1e3),t.lineTo(0,1e3),t.stroke(),t.restore()}function Bmt(e){var t=s(function(i,a,o){for(var l=i.atlasManager.getAtlasCollection(a),u=e.data.contexts[e.NODE],h=l.atlases,d=0;d=0&&w.add(k)}return w}function Fmt(e,t,r){var n=$mt(e,t,r),i=e.getCachedZSortedEles(),a,o,l=To(n),u;try{for(l.s();!(u=l.n()).done;){var h=u.value,d=i[h];if(!a&&d.isNode()&&(a=d),!o&&d.isEdge()&&(o=d),a&&o)break}}catch(f){l.e(f)}finally{l.f()}return[a,o].filter(Boolean)}function rF(e,t,r){var n=e.drawing;t+=1,r.isNode()?(n.drawNode(r,t,"node-underlay"),n.drawNode(r,t,"node-body"),n.drawTexture(r,t,"label"),n.drawNode(r,t,"node-overlay")):(n.drawEdgeLine(r,t),n.drawEdgeArrow(r,t,"source"),n.drawEdgeArrow(r,t,"target"),n.drawTexture(r,t,"label"),n.drawTexture(r,t,"edge-source-label"),n.drawTexture(r,t,"edge-target-label"))}function OTe(e,t,r){var n;e.webglDebug&&(n=performance.now());var i=e.drawing,a=0;if(r.screen&&e.data.canvasNeedsRedraw[e.SELECT_BOX]&&Pmt(e,t),e.data.canvasNeedsRedraw[e.NODE]||r.picking){var o=e.data.contexts[e.WEBGL];r.screen?(o.clearColor(0,0,0,0),o.enable(o.BLEND),o.blendFunc(o.ONE,o.ONE_MINUS_SRC_ALPHA)):o.disable(o.BLEND),o.clear(o.COLOR_BUFFER_BIT|o.DEPTH_BUFFER_BIT),o.viewport(0,0,o.canvas.width,o.canvas.height);var l=Nmt(e),u=e.getCachedZSortedEles();if(a=u.length,i.startFrame(l,r),r.screen){for(var h=0;h{"use strict";s(nF,"_arrayLikeToArray");s(mct,"_arrayWithHoles");s(gct,"_arrayWithoutHoles");s(Xf,"_classCallCheck");s(yct,"_defineProperties");s(Kf,"_createClass");s(To,"_createForOfIteratorHelper");s(Jbe,"_defineProperty$1");s(vct,"_iterableToArray");s(xct,"_iterableToArrayLimit");s(bct,"_nonIterableRest");s(Tct,"_nonIterableSpread");s(aa,"_slicedToArray");s(N5,"_toConsumableArray");s(Cct,"_toPrimitive");s(e2e,"_toPropertyKey");s(ya,"_typeof");s(SF,"_unsupportedIterableToArray");ma=typeof window>"u"?null:window,kve=ma?ma.navigator:null;ma&&ma.document;Sct=ya(""),t2e=ya({}),wct=ya(function(){}),kct=typeof HTMLElement>"u"?"undefined":ya(HTMLElement),WT=s(function(t){return t&&t.instanceString&&Ai(t.instanceString)?t.instanceString():null},"instanceStr"),yr=s(function(t){return t!=null&&ya(t)==Sct},"string"),Ai=s(function(t){return t!=null&&ya(t)===wct},"fn"),Xn=s(function(t){return!Zo(t)&&(Array.isArray?Array.isArray(t):t!=null&&t instanceof Array)},"array"),mn=s(function(t){return t!=null&&ya(t)===t2e&&!Xn(t)&&t.constructor===Object},"plainObject"),Ect=s(function(t){return t!=null&&ya(t)===t2e},"object"),Vt=s(function(t){return t!=null&&ya(t)===ya(1)&&!isNaN(t)},"number"),Act=s(function(t){return Vt(t)&&Math.floor(t)===t},"integer"),P5=s(function(t){if(kct!=="undefined")return t!=null&&t instanceof HTMLElement},"htmlElement"),Zo=s(function(t){return qT(t)||r2e(t)},"elementOrCollection"),qT=s(function(t){return WT(t)==="collection"&&t._private.single},"element"),r2e=s(function(t){return WT(t)==="collection"&&!t._private.single},"collection"),wF=s(function(t){return WT(t)==="core"},"core"),n2e=s(function(t){return WT(t)==="stylesheet"},"stylesheet"),Rct=s(function(t){return WT(t)==="event"},"event"),Wf=s(function(t){return t==null?!0:!!(t===""||t.match(/^\s+$/))},"emptyString"),_ct=s(function(t){return typeof HTMLElement>"u"?!1:t instanceof HTMLElement},"domElement"),Lct=s(function(t){return mn(t)&&Vt(t.x1)&&Vt(t.x2)&&Vt(t.y1)&&Vt(t.y2)},"boundingBox"),Dct=s(function(t){return Ect(t)&&Ai(t.then)},"promise"),Ict=s(function(){return kve&&kve.userAgent.match(/msie|trident|edge/i)},"ms"),P1=s(function(t,r){r||(r=s(function(){if(arguments.length===1)return arguments[0];if(arguments.length===0)return"undefined";for(var a=[],o=0;or?1:0},"ascending"),Fct=s(function(t,r){return-1*a2e(t,r)},"descending"),kr=Object.assign!=null?Object.assign.bind(Object):function(e){for(var t=arguments,r=1;r1&&(v-=1),v<1/6?g+(y-g)*6*v:v<1/2?y:v<2/3?g+(y-g)*(2/3-v)*6:g}s(d,"hue2rgb");var f=new RegExp("^"+Pct+"$").exec(t);if(f){if(n=parseInt(f[1]),n<0?n=(360- -1*n%360)%360:n>360&&(n=n%360),n/=360,i=parseFloat(f[2]),i<0||i>100||(i=i/100,a=parseFloat(f[3]),a<0||a>100)||(a=a/100,o=f[4],o!==void 0&&(o=parseFloat(o),o<0||o>1)))return;if(i===0)l=u=h=Math.round(a*255);else{var p=a<.5?a*(1+i):a+i-a*i,m=2*a-p;l=Math.round(255*d(m,p,n+1/3)),u=Math.round(255*d(m,p,n)),h=Math.round(255*d(m,p,n-1/3))}r=[l,u,h,o]}return r},"hsl2tuple"),Vct=s(function(t){var r,n=new RegExp("^"+Mct+"$").exec(t);if(n){r=[];for(var i=[],a=1;a<=3;a++){var o=n[a];if(o[o.length-1]==="%"&&(i[a]=!0),o=parseFloat(o),i[a]&&(o=o/100*255),o<0||o>255)return;r.push(Math.floor(o))}var l=i[1]||i[2]||i[3],u=i[1]&&i[2]&&i[3];if(l&&!u)return;var h=n[4];if(h!==void 0){if(h=parseFloat(h),h<0||h>1)return;r.push(h)}}return r},"rgb2tuple"),Wct=s(function(t){return qct[t.toLowerCase()]},"colorname2tuple"),s2e=s(function(t){return(Xn(t)?t:null)||Wct(t)||Gct(t)||Vct(t)||zct(t)},"color2tuple"),qct={transparent:[0,0,0,0],aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],grey:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},o2e=s(function(t){for(var r=t.map,n=t.keys,i=n.length,a=0;a1&&arguments[1]!==void 0?arguments[1]:Jm,n=r,i;i=t.next(),!i.done;)n=n*h2e+i.value|0;return n},"hashIterableInts"),MT=s(function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:Jm;return r*h2e+t|0},"hashInt"),NT=s(function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:A1;return(r<<5)+r+t|0},"hashIntAlt"),rut=s(function(t,r){return t*2097152+r},"combineHashes"),Pf=s(function(t){return t[0]*2097152+t[1]},"combineHashesArray"),h5=s(function(t,r){return[MT(t[0],r[0]),NT(t[1],r[1])]},"hashArrays"),zve=s(function(t,r){var n={value:0,done:!1},i=0,a=t.length,o={next:s(function(){return i=0;i--)t[i]===r&&t.splice(i,1)},"removeFromArray"),_F=s(function(t){t.splice(0,t.length)},"clearArray"),dut=s(function(t,r){for(var n=0;n"u"?"undefined":ya(Set))!==put?Set:mut,K5=s(function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0;if(t===void 0||r===void 0||!wF(t)){xi("An element must have a core reference and parameters set");return}var i=r.group;if(i==null&&(r.data&&r.data.source!=null&&r.data.target!=null?i="edges":i="nodes"),i!=="nodes"&&i!=="edges"){xi("An element must be of type `nodes` or `edges`; you specified `"+i+"`");return}this.length=1,this[0]=this;var a=this._private={cy:t,single:!0,data:r.data||{},position:r.position||{x:0,y:0},autoWidth:void 0,autoHeight:void 0,autoPadding:void 0,compoundBoundsClean:!1,listeners:[],group:i,style:{},rstyle:{},styleCxts:[],styleKeys:{},removed:!0,selected:!!r.selected,selectable:r.selectable===void 0?!0:!!r.selectable,locked:!!r.locked,grabbed:!1,grabbable:r.grabbable===void 0?!0:!!r.grabbable,pannable:r.pannable===void 0?i==="edges":!!r.pannable,active:!1,classes:new G1,animation:{current:[],queue:[]},rscratch:{},scratch:r.scratch||{},edges:[],children:[],parent:r.parent&&r.parent.isNode()?r.parent:null,traversalCache:{},backgrounding:!1,bbCache:null,bbCacheShift:{x:0,y:0},bodyBounds:null,overlayBounds:null,labelBounds:{all:null,source:null,target:null,main:null},arrowBounds:{source:null,target:null,"mid-source":null,"mid-target":null}};if(a.position.x==null&&(a.position.x=0),a.position.y==null&&(a.position.y=0),r.renderedPosition){var o=r.renderedPosition,l=t.pan(),u=t.zoom();a.position={x:(o.x-l.x)/u,y:(o.y-l.y)/u}}var h=[];Xn(r.classes)?h=r.classes:yr(r.classes)&&(h=r.classes.split(/\s+/));for(var d=0,f=h.length;d0;){var S=b.pop(),k=v(S),R=S.id();if(p[R]=k,k!==1/0)for(var M=S.neighborhood().intersect(g),P=0;P0)for(O.unshift(B);f[W];){var V=f[W];O.unshift(V.edge),O.unshift(V.node),$=V.node,W=$.id()}return l.spawn(O)},"pathTo")}},"dijkstra")},Cut={kruskal:s(function(t){t=t||function(T){return 1};for(var r=this.byGroup(),n=r.nodes,i=r.edges,a=n.length,o=new Array(a),l=n,u=s(function(w){for(var C=0;C0;){if(C(),k++,w===d){for(var R=[],M=a,P=d,N=x[P];R.unshift(M),N!=null&&R.unshift(N),M=v[P],M!=null;)P=M.id(),N=x[P];return{found:!0,distance:f[w],path:this.spawn(R),steps:k}}m[w]=!0;for(var _=T._private.edges,A=0;A<_.length;A++){var I=_[A];if(this.hasElementWithId(I.id())&&!(l&&I.data("source")!==w)){var D=I.source(),L=I.target(),B=D.id()!==w?D:L,O=B.id();if(this.hasElementWithId(O)&&!m[O]){var $=f[w]+u(I);if(!S(O)){f[O]=$,p[O]=$+o(B),b(B,O),v[O]=T,x[O]=I;continue}$N&&(g[P]=N,b[P]=M,T[P]=C),!a){var _=M*d+R;!a&&g[_]>N&&(g[_]=N,b[_]=R,T[_]=C)}}}for(var A=0;A1&&arguments[1]!==void 0?arguments[1]:o,Ie=T(ye),Ne=[],re=Ie;;){if(re==null)return r.spawn();var ue=b(re),le=ue.edge,he=ue.pred;if(Ne.unshift(re[0]),re.same(Be)&&Ne.length>0)break;le!=null&&Ne.unshift(le),re=he}return u.spawn(Ne)},"pathTo"),S=0;S=0;d--){var f=h[d],p=f[1],m=f[2];(r[p]===l&&r[m]===u||r[p]===u&&r[m]===l)&&h.splice(d,1)}for(var g=0;gi;){var a=Math.floor(Math.random()*r.length);r=Lut(a,t,r),n--}return r},"contractUntil"),Dut={kargerStein:s(function(){var t=this,r=this.byGroup(),n=r.nodes,i=r.edges;i.unmergeBy(function(O){return O.isLoop()});var a=n.length,o=i.length,l=Math.ceil(Math.pow(Math.log(a)/Math.LN2,2)),u=Math.floor(a/_ut);if(a<2){xi("At least 2 nodes are required for Karger-Stein algorithm");return}for(var h=[],d=0;d1&&arguments[1]!==void 0?arguments[1]:0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:t.length,i=1/0,a=r;a1&&arguments[1]!==void 0?arguments[1]:0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:t.length,i=-1/0,a=r;a1&&arguments[1]!==void 0?arguments[1]:0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:t.length,i=0,a=0,o=r;o1&&arguments[1]!==void 0?arguments[1]:0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:t.length,i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!0,a=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,o=arguments.length>5&&arguments[5]!==void 0?arguments[5]:!0;i?t=t.slice(r,n):(n0&&t.splice(0,r));for(var l=0,u=t.length-1;u>=0;u--){var h=t[u];o?isFinite(h)||(t[u]=-1/0,l++):t.splice(u,1)}a&&t.sort(function(p,m){return p-m});var d=t.length,f=Math.floor(d/2);return d%2!==0?t[f+1+l]:(t[f-1+l]+t[f+l])/2},"median"),But=s(function(t){return Math.PI*t/180},"deg2rad"),d5=s(function(t,r){return Math.atan2(r,t)-Math.PI/2},"getAngleFromDisp"),LF=Math.log2||function(e){return Math.log(e)/Math.log(2)},DF=s(function(t){return t>0?1:t<0?-1:0},"signum"),ng=s(function(t,r){return Math.sqrt(Zm(t,r))},"dist"),Zm=s(function(t,r){var n=r.x-t.x,i=r.y-t.y;return n*n+i*i},"sqdist"),$ut=s(function(t){for(var r=t.length,n=0,i=0;i=t.x1&&t.y2>=t.y1)return{x1:t.x1,y1:t.y1,x2:t.x2,y2:t.y2,w:t.x2-t.x1,h:t.y2-t.y1};if(t.w!=null&&t.h!=null&&t.w>=0&&t.h>=0)return{x1:t.x1,y1:t.y1,x2:t.x1+t.w,y2:t.y1+t.h,w:t.w,h:t.h}}},"makeBoundingBox"),Gut=s(function(t){return{x1:t.x1,x2:t.x2,w:t.w,y1:t.y1,y2:t.y2,h:t.h}},"copyBoundingBox"),zut=s(function(t){t.x1=1/0,t.y1=1/0,t.x2=-1/0,t.y2=-1/0,t.w=0,t.h=0},"clearBoundingBox"),Vut=s(function(t,r){t.x1=Math.min(t.x1,r.x1),t.x2=Math.max(t.x2,r.x2),t.w=t.x2-t.x1,t.y1=Math.min(t.y1,r.y1),t.y2=Math.max(t.y2,r.y2),t.h=t.y2-t.y1},"updateBoundingBox"),x2e=s(function(t,r,n){t.x1=Math.min(t.x1,r),t.x2=Math.max(t.x2,r),t.w=t.x2-t.x1,t.y1=Math.min(t.y1,n),t.y2=Math.max(t.y2,n),t.h=t.y2-t.y1},"expandBoundingBoxByPoint"),w5=s(function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0;return t.x1-=r,t.x2+=r,t.y1-=r,t.y2+=r,t.w=t.x2-t.x1,t.h=t.y2-t.y1,t},"expandBoundingBox"),k5=s(function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:[0],n,i,a,o;if(r.length===1)n=i=a=o=r[0];else if(r.length===2)n=a=r[0],o=i=r[1];else if(r.length===4){var l=aa(r,4);n=l[0],i=l[1],a=l[2],o=l[3]}return t.x1-=o,t.x2+=i,t.y1-=n,t.y2+=a,t.w=t.x2-t.x1,t.h=t.y2-t.y1,t},"expandBoundingBoxSides"),Yve=s(function(t,r){t.x1=r.x1,t.y1=r.y1,t.x2=r.x2,t.y2=r.y2,t.w=t.x2-t.x1,t.h=t.y2-t.y1},"assignBoundingBox"),IF=s(function(t,r){return!(t.x1>r.x2||r.x1>t.x2||t.x2r.y2||r.y1>t.y2)},"boundingBoxesIntersect"),Ff=s(function(t,r,n){return t.x1<=r&&r<=t.x2&&t.y1<=n&&n<=t.y2},"inBoundingBox"),jve=s(function(t,r){return Ff(t,r.x,r.y)},"pointInBoundingBox"),b2e=s(function(t,r){return Ff(t,r.x1,r.y1)&&Ff(t,r.x2,r.y2)},"boundingBoxInBoundingBox"),Wut=(qB=Math.hypot)!==null&&qB!==void 0?qB:function(e,t){return Math.sqrt(e*e+t*t)};s(qut,"inflatePolygon");s(Hut,"miterBox");T2e=s(function(t,r,n,i,a,o,l){var u=arguments.length>7&&arguments[7]!==void 0?arguments[7]:"auto",h=u==="auto"?Hf(a,o):u,d=a/2,f=o/2;h=Math.min(h,d,f);var p=h!==d,m=h!==f,g;if(p){var y=n-d+h-l,v=i-f-l,x=n+d-h+l,b=v;if(g=Gf(t,r,n,i,y,v,x,b,!1),g.length>0)return g}if(m){var T=n+d+l,w=i-f+h-l,C=T,S=i+f-h+l;if(g=Gf(t,r,n,i,T,w,C,S,!1),g.length>0)return g}if(p){var k=n-d+h-l,R=i+f+l,M=n+d-h+l,P=R;if(g=Gf(t,r,n,i,k,R,M,P,!1),g.length>0)return g}if(m){var N=n-d-l,_=i-f+h-l,A=N,I=i+f-h+l;if(g=Gf(t,r,n,i,N,_,A,I,!1),g.length>0)return g}var D;{var L=n-d+h,B=i-f+h;if(D=AT(t,r,n,i,L,B,h+l),D.length>0&&D[0]<=L&&D[1]<=B)return[D[0],D[1]]}{var O=n+d-h,$=i-f+h;if(D=AT(t,r,n,i,O,$,h+l),D.length>0&&D[0]>=O&&D[1]<=$)return[D[0],D[1]]}{var W=n+d-h,V=i+f-h;if(D=AT(t,r,n,i,W,V,h+l),D.length>0&&D[0]>=W&&D[1]>=V)return[D[0],D[1]]}{var z=n-d+h,H=i+f-h;if(D=AT(t,r,n,i,z,H,h+l),D.length>0&&D[0]<=z&&D[1]>=H)return[D[0],D[1]]}return[]},"roundRectangleIntersectLine"),Uut=s(function(t,r,n,i,a,o,l){var u=l,h=Math.min(n,a),d=Math.max(n,a),f=Math.min(i,o),p=Math.max(i,o);return h-u<=t&&t<=d+u&&f-u<=r&&r<=p+u},"inLineVicinity"),Yut=s(function(t,r,n,i,a,o,l,u,h){var d={x1:Math.min(n,l,a)-h,x2:Math.max(n,l,a)+h,y1:Math.min(i,u,o)-h,y2:Math.max(i,u,o)+h};return!(td.x2||rd.y2)},"inBezierVicinity"),jut=s(function(t,r,n,i){n-=i;var a=r*r-4*t*n;if(a<0)return[];var o=Math.sqrt(a),l=2*t,u=(-r+o)/l,h=(-r-o)/l;return[u,h]},"solveQuadratic"),Xut=s(function(t,r,n,i,a){var o=1e-5;t===0&&(t=o),r/=t,n/=t,i/=t;var l,u,h,d,f,p,m,g;if(u=(3*n-r*r)/9,h=-(27*i)+r*(9*n-2*(r*r)),h/=54,l=u*u*u+h*h,a[1]=0,m=r/3,l>0){f=h+Math.sqrt(l),f=f<0?-Math.pow(-f,1/3):Math.pow(f,1/3),p=h-Math.sqrt(l),p=p<0?-Math.pow(-p,1/3):Math.pow(p,1/3),a[0]=-m+f+p,m+=(f+p)/2,a[4]=a[2]=-m,m=Math.sqrt(3)*(-p+f)/2,a[3]=m,a[5]=-m;return}if(a[5]=a[3]=0,l===0){g=h<0?-Math.pow(-h,1/3):Math.pow(h,1/3),a[0]=-m+2*g,a[4]=a[2]=-(g+m);return}u=-u,d=u*u*u,d=Math.acos(h/Math.sqrt(d)),g=2*Math.sqrt(u),a[0]=-m+g*Math.cos(d/3),a[2]=-m+g*Math.cos((d+2*Math.PI)/3),a[4]=-m+g*Math.cos((d+4*Math.PI)/3)},"solveCubic"),Kut=s(function(t,r,n,i,a,o,l,u){var h=1*n*n-4*n*a+2*n*l+4*a*a-4*a*l+l*l+i*i-4*i*o+2*i*u+4*o*o-4*o*u+u*u,d=9*n*a-3*n*n-3*n*l-6*a*a+3*a*l+9*i*o-3*i*i-3*i*u-6*o*o+3*o*u,f=3*n*n-6*n*a+n*l-n*t+2*a*a+2*a*t-l*t+3*i*i-6*i*o+i*u-i*r+2*o*o+2*o*r-u*r,p=1*n*a-n*n+n*t-a*t+i*o-i*i+i*r-o*r,m=[];Xut(h,d,f,p,m);for(var g=1e-7,y=[],v=0;v<6;v+=2)Math.abs(m[v+1])=0&&m[v]<=1&&y.push(m[v]);y.push(1),y.push(0);for(var x=-1,b,T,w,C=0;C=0?wh?(t-a)*(t-a)+(r-o)*(r-o):d-p},"sqdistToFiniteLine"),bo=s(function(t,r,n){for(var i,a,o,l,u,h=0,d=0;d=t&&t>=o||i<=t&&t<=o)u=(t-i)/(o-i)*(l-a)+a,u>r&&h++;else continue;return h%2!==0},"pointInsidePolygonPoints"),$h=s(function(t,r,n,i,a,o,l,u,h){var d=new Array(n.length),f;u[0]!=null?(f=Math.atan(u[1]/u[0]),u[0]<0?f=f+Math.PI/2:f=-f-Math.PI/2):f=u;for(var p=Math.cos(-f),m=Math.sin(-f),g=0;g0){var v=F5(d,-h);y=$5(v)}else y=d;return bo(t,r,y)},"pointInsidePolygon"),Qut=s(function(t,r,n,i,a,o,l,u){for(var h=new Array(n.length*2),d=0;d=0&&v<=1&&b.push(v),x>=0&&x<=1&&b.push(x),b.length===0)return[];var T=b[0]*u[0]+t,w=b[0]*u[1]+r;if(b.length>1){if(b[0]==b[1])return[T,w];var C=b[1]*u[0]+t,S=b[1]*u[1]+r;return[T,w,C,S]}else return[T,w]},"intersectLineCircle"),HB=s(function(t,r,n){return r<=t&&t<=n||n<=t&&t<=r?t:t<=r&&r<=n||n<=r&&r<=t?r:n},"midOfThree"),Gf=s(function(t,r,n,i,a,o,l,u,h){var d=t-a,f=n-t,p=l-a,m=r-o,g=i-r,y=u-o,v=p*m-y*d,x=f*m-g*d,b=y*f-p*g;if(b!==0){var T=v/b,w=x/b,C=.001,S=0-C,k=1+C;return S<=T&&T<=k&&S<=w&&w<=k?[t+T*f,r+T*g]:h?[t+T*f,r+T*g]:[]}else return v===0||x===0?HB(t,n,l)===l?[l,u]:HB(t,n,a)===a?[a,o]:HB(a,l,n)===n?[n,i]:[]:[]},"finiteLinesIntersect"),eht=s(function(t,r,n,i,a){var o=[],l=i/2,u=a/2,h=r,d=n;o.push({x:h+l*t[0],y:d+u*t[1]});for(var f=1;f0){var y=F5(f,-u);m=$5(y)}else m=f}else m=n;for(var v,x,b,T,w=0;w2){for(var g=[d[0],d[1]],y=Math.pow(g[0]-t,2)+Math.pow(g[1]-r,2),v=1;vd&&(d=w)},"set"),get:s(function(T){return h[T]},"get")},p=0;p0?D=I.edgesTo(A)[0]:D=A.edgesTo(I)[0];var L=i(D);A=A.id(),k[A]>k[N]+L&&(k[A]=k[N]+L,R.nodes.indexOf(A)<0?R.push(A):R.updateItem(A),S[A]=0,C[A]=[]),k[A]==k[N]+L&&(S[A]=S[A]+S[N],C[A].push(N))}else for(var B=0;B0;){for(var V=w.pop(),z=0;z0&&l.push(n[u]);l.length!==0&&a.push(i.collection(l))}return a},"assign"),mht=s(function(t,r){for(var n=0;n5&&arguments[5]!==void 0?arguments[5]:vht,l=i,u,h,d=0;d=2?bT(t,r,n,0,Jve,xht):bT(t,r,n,0,Qve)},"euclidean"),squaredEuclidean:s(function(t,r,n){return bT(t,r,n,0,Jve)},"squaredEuclidean"),manhattan:s(function(t,r,n){return bT(t,r,n,0,Qve)},"manhattan"),max:s(function(t,r,n){return bT(t,r,n,-1/0,bht)},"max")};O1["squared-euclidean"]=O1.squaredEuclidean;O1.squaredeuclidean=O1.squaredEuclidean;s(Q5,"clusteringDistance");Tht=ts({k:2,m:2,sensitivityThreshold:1e-4,distance:"euclidean",maxIterations:10,attributes:[],testMode:!1,testCentroids:null}),NF=s(function(t){return Tht(t)},"setOptions"),G5=s(function(t,r,n,i,a){var o=a!=="kMedoids",l=o?function(f){return n[f]}:function(f){return i[f](n)},u=s(function(p){return i[p](r)},"getQ"),h=n,d=r;return Q5(t,i.length,l,u,h,d)},"getDist"),YB=s(function(t,r,n){for(var i=n.length,a=new Array(i),o=new Array(i),l=new Array(r),u=null,h=0;hn)return!1}return!0},"haveMatricesConverged"),wht=s(function(t,r,n){for(var i=0;il&&(l=r[h][d],u=d);a[u].push(t[h])}for(var f=0;f=a.threshold||a.mode==="dendrogram"&&t.length===1)return!1;var g=r[o],y=r[i[o]],v;a.mode==="dendrogram"?v={left:g,right:y,key:g.key}:v={value:g.value.concat(y.value),key:g.key},t[g.index]=v,t.splice(y.index,1),r[g.key]=v;for(var x=0;xn[y.key][b.key]&&(u=n[y.key][b.key])):a.linkage==="max"?(u=n[g.key][b.key],n[g.key][b.key]0&&i.push(a);return i},"findExemplars"),axe=s(function(t,r,n){for(var i=[],a=0;al&&(o=h,l=r[a*t+h])}o>0&&i.push(o)}for(var d=0;dh&&(u=d,h=f)}n[a]=o[u]}return i=axe(t,r,n),i},"assign"),sxe=s(function(t){for(var r=this.cy(),n=this.nodes(),i=Oht(t),a={},o=0;o=N?(_=N,N=I,A=D):I>_&&(_=I);for(var L=0;L0?1:0;k[M%i.minIterations*l+z]=H,V+=H}if(V>0&&(M>=i.minIterations-1||M==i.maxIterations-1)){for(var K=0,J=0;J1||S>1)&&(l=!0),f[T]=[],b.outgoers().forEach(function(R){R.isEdge()&&f[T].push(R.id())})}else p[T]=[void 0,b.target().id()]}):o.forEach(function(b){var T=b.id();if(b.isNode()){var w=b.degree(!0);w%2&&(u?h?l=!0:h=T:u=T),f[T]=[],b.connectedEdges().forEach(function(C){return f[T].push(C.id())})}else p[T]=[b.source().id(),b.target().id()]});var m={found:!1,trail:void 0};if(l)return m;if(h&&u)if(a){if(d&&h!=d)return m;d=h}else{if(d&&h!=d&&u!=d)return m;d||(d=h)}else d||(d=o[0].id());var g=s(function(T){for(var w=T,C=[T],S,k,R;f[w].length;)S=f[w].shift(),k=p[S][0],R=p[S][1],w!=R?(f[R]=f[R].filter(function(M){return M!=S}),w=R):!a&&w!=k&&(f[k]=f[k].filter(function(M){return M!=S}),w=k),C.unshift(S),C.unshift(w);return C},"walk"),y=[],v=[];for(v=g(d);v.length!=1;)f[v[0]].length==0?(y.unshift(o.getElementById(v.shift())),y.unshift(o.getElementById(v.shift()))):v=g(v.shift()).concat(v);y.unshift(o.getElementById(v.shift()));for(var x in f)if(f[x].length)return m;return m.found=!0,m.trail=this.spawn(y,!0),m},"hierholzer")},p5=s(function(){var t=this,r={},n=0,i=0,a=[],o=[],l={},u=s(function(p,m){for(var g=o.length-1,y=[],v=t.spawn();o[g].x!=p||o[g].y!=m;)y.push(o.pop().edge),g--;y.push(o.pop().edge),y.forEach(function(x){var b=x.connectedNodes().intersection(t);v.merge(x),b.forEach(function(T){var w=T.id(),C=T.connectedEdges().intersection(t);v.merge(T),r[w].cutVertex?v.merge(C.filter(function(S){return S.isLoop()})):v.merge(C)})}),a.push(v)},"buildComponent"),h=s(function(p,m,g){p===g&&(i+=1),r[m]={id:n,low:n++,cutVertex:!1};var y=t.getElementById(m).connectedEdges().intersection(t);if(y.size()===0)a.push(t.spawn(t.getElementById(m)));else{var v,x,b,T;y.forEach(function(w){v=w.source().id(),x=w.target().id(),b=v===m?x:v,b!==g&&(T=w.id(),l[T]||(l[T]=!0,o.push({x:m,y:b,edge:w})),b in r?r[m].low=Math.min(r[m].low,r[b].id):(h(p,b,m),r[m].low=Math.min(r[m].low,r[b].low),r[m].id<=r[b].low&&(r[m].cutVertex=!0,u(m,b))))})}},"biconnectedSearch");t.forEach(function(f){if(f.isNode()){var p=f.id();p in r||(i=0,h(p,p),r[p].cutVertex=i>1)}});var d=Object.keys(r).filter(function(f){return r[f].cutVertex}).map(function(f){return t.getElementById(f)});return{cut:t.spawn(d),components:a}},"hopcroftTarjanBiconnected"),qht={hopcroftTarjanBiconnected:p5,htbc:p5,htb:p5,hopcroftTarjanBiconnectedComponents:p5},m5=s(function(){var t=this,r={},n=0,i=[],a=[],o=t.spawn(t),l=s(function(h){a.push(h),r[h]={index:n,low:n++,explored:!1};var d=t.getElementById(h).connectedEdges().intersection(t);if(d.forEach(function(y){var v=y.target().id();v!==h&&(v in r||l(v),r[v].explored||(r[h].low=Math.min(r[h].low,r[v].low)))}),r[h].index===r[h].low){for(var f=t.spawn();;){var p=a.pop();if(f.merge(t.getElementById(p)),r[p].low=r[h].index,r[p].explored=!0,p===h)break}var m=f.edgesWith(f),g=f.merge(m);i.push(g),o=o.difference(g)}},"stronglyConnectedSearch");return t.forEach(function(u){if(u.isNode()){var h=u.id();h in r||l(h)}}),{cut:o,components:i}},"tarjanStronglyConnected"),Hht={tarjanStronglyConnected:m5,tsc:m5,tscc:m5,tarjanStronglyConnectedComponents:m5},R2e={};[PT,Tut,Cut,wut,Eut,Rut,Dut,iht,I1,M1,sF,yht,Lht,Nht,zht,Wht,qht,Hht].forEach(function(e){kr(R2e,e)});_2e=0,L2e=1,D2e=2,yc=s(function(t){if(!(this instanceof yc))return new yc(t);this.id="Thenable/1.0.7",this.state=_2e,this.fulfillValue=void 0,this.rejectReason=void 0,this.onFulfilled=[],this.onRejected=[],this.proxy={then:this.then.bind(this)},typeof t=="function"&&t.call(this,this.fulfill.bind(this),this.reject.bind(this))},"api");yc.prototype={fulfill:s(function(t){return oxe(this,L2e,"fulfillValue",t)},"fulfill"),reject:s(function(t){return oxe(this,D2e,"rejectReason",t)},"reject"),then:s(function(t,r){var n=this,i=new yc;return n.onFulfilled.push(cxe(t,i,"fulfill")),n.onRejected.push(cxe(r,i,"reject")),I2e(n),i.proxy},"then")};oxe=s(function(t,r,n,i){return t.state===_2e&&(t.state=r,t[n]=i,I2e(t)),t},"deliver"),I2e=s(function(t){t.state===L2e?lxe(t,"onFulfilled",t.fulfillValue):t.state===D2e&&lxe(t,"onRejected",t.rejectReason)},"execute"),lxe=s(function(t,r,n){if(t[r].length!==0){var i=t[r];t[r]=[];var a=s(function(){for(var l=0;l0},"animatedImpl")},"animated"),clearQueue:s(function(){return s(function(){var r=this,n=r.length!==void 0,i=n?r:[r],a=this._private.cy||this;if(!a.styleEnabled())return this;for(var o=0;o0&&this.spawn(i).updateStyle().emit("class"),r},"classes"),addClass:s(function(t){return this.toggleClass(t,!0)},"addClass"),hasClass:s(function(t){var r=this[0];return r!=null&&r._private.classes.has(t)},"hasClass"),toggleClass:s(function(t,r){Xn(t)||(t=t.match(/\S+/g)||[]);for(var n=this,i=r===void 0,a=[],o=0,l=n.length;o0&&this.spawn(a).updateStyle().emit("class"),n},"toggleClass"),removeClass:s(function(t){return this.toggleClass(t,!1)},"removeClass"),flashClass:s(function(t,r){var n=this;if(r==null)r=250;else if(r===0)return n;return n.addClass(t),setTimeout(function(){n.removeClass(t)},r),n},"flashClass")};E5.className=E5.classNames=E5.classes;pn={metaChar:"[\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\]\\^\\`\\{\\|\\}\\~]",comparatorOp:"=|\\!=|>|>=|<|<=|\\$=|\\^=|\\*=",boolOp:"\\?|\\!|\\^",string:`"(?:\\\\"|[^"])*"|'(?:\\\\'|[^'])*'`,number:ga,meta:"degree|indegree|outdegree",separator:"\\s*,\\s*",descendant:"\\s+",child:"\\s+>\\s+",subject:"\\$",group:"node|edge|\\*",directedEdge:"\\s+->\\s+",undirectedEdge:"\\s+<->\\s+"};pn.variable="(?:[\\w-.]|(?:\\\\"+pn.metaChar+"))+";pn.className="(?:[\\w-]|(?:\\\\"+pn.metaChar+"))+";pn.value=pn.string+"|"+pn.number;pn.id=pn.variable;(function(){var e,t,r;for(e=pn.comparatorOp.split("|"),r=0;r=0)&&t!=="="&&(pn.comparatorOp+="|\\!"+t)})();qn=s(function(){return{checks:[]}},"newQuery"),nr={GROUP:0,COLLECTION:1,FILTER:2,DATA_COMPARE:3,DATA_EXIST:4,DATA_BOOL:5,META_COMPARE:6,STATE:7,ID:8,CLASS:9,UNDIRECTED_EDGE:10,DIRECTED_EDGE:11,NODE_SOURCE:12,NODE_TARGET:13,NODE_NEIGHBOR:14,CHILD:15,DESCENDANT:16,PARENT:17,ANCESTOR:18,COMPOUND_SPLIT:19,TRUE:20},uF=[{selector:":selected",matches:s(function(t){return t.selected()},"matches")},{selector:":unselected",matches:s(function(t){return!t.selected()},"matches")},{selector:":selectable",matches:s(function(t){return t.selectable()},"matches")},{selector:":unselectable",matches:s(function(t){return!t.selectable()},"matches")},{selector:":locked",matches:s(function(t){return t.locked()},"matches")},{selector:":unlocked",matches:s(function(t){return!t.locked()},"matches")},{selector:":visible",matches:s(function(t){return t.visible()},"matches")},{selector:":hidden",matches:s(function(t){return!t.visible()},"matches")},{selector:":transparent",matches:s(function(t){return t.transparent()},"matches")},{selector:":grabbed",matches:s(function(t){return t.grabbed()},"matches")},{selector:":free",matches:s(function(t){return!t.grabbed()},"matches")},{selector:":removed",matches:s(function(t){return t.removed()},"matches")},{selector:":inside",matches:s(function(t){return!t.removed()},"matches")},{selector:":grabbable",matches:s(function(t){return t.grabbable()},"matches")},{selector:":ungrabbable",matches:s(function(t){return!t.grabbable()},"matches")},{selector:":animated",matches:s(function(t){return t.animated()},"matches")},{selector:":unanimated",matches:s(function(t){return!t.animated()},"matches")},{selector:":parent",matches:s(function(t){return t.isParent()},"matches")},{selector:":childless",matches:s(function(t){return t.isChildless()},"matches")},{selector:":child",matches:s(function(t){return t.isChild()},"matches")},{selector:":orphan",matches:s(function(t){return t.isOrphan()},"matches")},{selector:":nonorphan",matches:s(function(t){return t.isChild()},"matches")},{selector:":compound",matches:s(function(t){return t.isNode()?t.isParent():t.source().isParent()||t.target().isParent()},"matches")},{selector:":loop",matches:s(function(t){return t.isLoop()},"matches")},{selector:":simple",matches:s(function(t){return t.isSimple()},"matches")},{selector:":active",matches:s(function(t){return t.active()},"matches")},{selector:":inactive",matches:s(function(t){return!t.active()},"matches")},{selector:":backgrounding",matches:s(function(t){return t.backgrounding()},"matches")},{selector:":nonbackgrounding",matches:s(function(t){return!t.backgrounding()},"matches")}].sort(function(e,t){return Fct(e.selector,t.selector)}),Vdt=(function(){for(var e={},t,r=0;r0&&d.edgeCount>0)return On("The selector `"+t+"` is invalid because it uses both a compound selector and an edge selector"),!1;if(d.edgeCount>1)return On("The selector `"+t+"` is invalid because it uses multiple edge selectors"),!1;d.edgeCount===1&&On("The selector `"+t+"` is deprecated. Edge selectors do not take effect on changes to source and target nodes after an edge is added, for performance reasons. Use a class or data selector on edges instead, updating the class or data of an edge when your app detects a change in source or target nodes.")}return!0},"parse"),jdt=s(function(){if(this.toStringCache!=null)return this.toStringCache;for(var t=s(function(d){return d??""},"clean"),r=s(function(d){return yr(d)?'"'+d+'"':t(d)},"cleanVal"),n=s(function(d){return" "+d+" "},"space"),i=s(function(d,f){var p=d.type,m=d.value;switch(p){case nr.GROUP:{var g=t(m);return g.substring(0,g.length-1)}case nr.DATA_COMPARE:{var y=d.field,v=d.operator;return"["+y+n(t(v))+r(m)+"]"}case nr.DATA_BOOL:{var x=d.operator,b=d.field;return"["+t(x)+b+"]"}case nr.DATA_EXIST:{var T=d.field;return"["+T+"]"}case nr.META_COMPARE:{var w=d.operator,C=d.field;return"[["+C+n(t(w))+r(m)+"]]"}case nr.STATE:return m;case nr.ID:return"#"+m;case nr.CLASS:return"."+m;case nr.PARENT:case nr.CHILD:return a(d.parent,f)+n(">")+a(d.child,f);case nr.ANCESTOR:case nr.DESCENDANT:return a(d.ancestor,f)+" "+a(d.descendant,f);case nr.COMPOUND_SPLIT:{var S=a(d.left,f),k=a(d.subject,f),R=a(d.right,f);return S+(S.length>0?" ":"")+k+R}case nr.TRUE:return""}},"checkToString"),a=s(function(d,f){return d.checks.reduce(function(p,m,g){return p+(f===d&&g===0?"$":"")+i(m,f)},"")},"queryToString"),o="",l=0;l1&&l=0&&(r=r.replace("!",""),f=!0),r.indexOf("@")>=0&&(r=r.replace("@",""),d=!0),(a||l||d)&&(u=!a&&!o?"":""+t,h=""+n),d&&(t=u=u.toLowerCase(),n=h=h.toLowerCase()),r){case"*=":i=u.indexOf(h)>=0;break;case"$=":i=u.indexOf(h,u.length-h.length)>=0;break;case"^=":i=u.indexOf(h)===0;break;case"=":i=t===n;break;case">":p=!0,i=t>n;break;case">=":p=!0,i=t>=n;break;case"<":p=!0,i=t1&&arguments[1]!==void 0?arguments[1]:!0;return $F(this,e,t,G2e)};s(z2e,"addParent");B1.forEachUp=function(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;return $F(this,e,t,z2e)};s(rft,"addParentAndChildren");B1.forEachUpAndDown=function(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;return $F(this,e,t,rft)};B1.ancestors=B1.parents;$T=V2e={data:Pn.data({field:"data",bindingEvent:"data",allowBinding:!0,allowSetting:!0,settingEvent:"data",settingTriggersEvent:!0,triggerFnName:"trigger",allowGetting:!0,immutableKeys:{id:!0,source:!0,target:!0,parent:!0},updateStyle:!0}),removeData:Pn.removeData({field:"data",event:"data",triggerFnName:"trigger",triggerEvent:!0,immutableKeys:{id:!0,source:!0,target:!0,parent:!0},updateStyle:!0}),scratch:Pn.data({field:"scratch",bindingEvent:"scratch",allowBinding:!0,allowSetting:!0,settingEvent:"scratch",settingTriggersEvent:!0,triggerFnName:"trigger",allowGetting:!0,updateStyle:!0}),removeScratch:Pn.removeData({field:"scratch",event:"scratch",triggerFnName:"trigger",triggerEvent:!0,updateStyle:!0}),rscratch:Pn.data({field:"rscratch",allowBinding:!1,allowSetting:!0,settingTriggersEvent:!1,allowGetting:!0}),removeRscratch:Pn.removeData({field:"rscratch",triggerEvent:!1}),id:s(function(){var t=this[0];if(t)return t._private.data.id},"id")};$T.attr=$T.data;$T.removeAttr=$T.removeData;nft=V2e,nA={};s(H$,"defineDegreeFunction");kr(nA,{degree:H$(function(e,t){return t.source().same(t.target())?2:1}),indegree:H$(function(e,t){return t.target().same(e)?1:0}),outdegree:H$(function(e,t){return t.source().same(e)?1:0})});s(T1,"defineDegreeBoundsFunction");kr(nA,{minDegree:T1("degree",function(e,t){return et}),minIndegree:T1("indegree",function(e,t){return et}),minOutdegree:T1("outdegree",function(e,t){return et})});kr(nA,{totalDegree:s(function(t){for(var r=0,n=this.nodes(),i=0;i0,p=f;f&&(d=d[0]);var m=p?d.position():{x:0,y:0};r!==void 0?h.position(t,r+m[t]):a!==void 0&&h.position({x:a.x+m.x,y:a.y+m.y})}else{var g=n.position(),y=l?n.parent():null,v=y&&y.length>0,x=v;v&&(y=y[0]);var b=x?y.position():{x:0,y:0};return a={x:g.x-b.x,y:g.y-b.y},t===void 0?a:a[t]}else if(!o)return;return this},"relativePosition")};gc.modelPosition=gc.point=gc.position;gc.modelPositions=gc.points=gc.positions;gc.renderedPoint=gc.renderedPosition;gc.relativePoint=gc.relativePosition;ift=W2e,$1=s(function(t){switch(t){case"left":case"right-inside":return"left";case"right":case"left-inside":return"right";default:return"center"}},"labelHalign"),F1=s(function(t){switch(t){case"top":case"bottom-inside":return"top";case"bottom":case"top-inside":return"bottom";default:return"center"}},"labelValign"),aft=s(function(t){switch(t){case"left":return"right";case"right":return"left";case"left-inside":return"left";case"right-inside":return"right";default:return"center"}},"labelJustification");N1=Zf={};Zf.renderedBoundingBox=function(e){var t=this.boundingBox(e),r=this.cy(),n=r.zoom(),i=r.pan(),a=t.x1*n+i.x,o=t.x2*n+i.x,l=t.y1*n+i.y,u=t.y2*n+i.y;return{x1:a,x2:o,y1:l,y2:u,w:o-a,h:u-l}};Zf.dirtyCompoundBoundsCache=function(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!1,t=this.cy();return!t.styleEnabled()||!t.hasCompoundNodes()?this:(this.forEachUp(function(r){if(r.isParent()){var n=r._private;n.compoundBoundsClean=!1,n.bbCache=null,e||r.emitAndNotify("bounds")}}),this)};Zf.updateCompoundBounds=function(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!1,t=this.cy();if(!t.styleEnabled()||!t.hasCompoundNodes())return this;if(!e&&t.batching())return this;function r(o){if(!o.isParent())return;var l=o._private,u=o.children(),h=o.pstyle("compound-sizing-wrt-labels").value==="include",d={width:{val:o.pstyle("min-width").pfValue,left:o.pstyle("min-width-bias-left"),right:o.pstyle("min-width-bias-right")},height:{val:o.pstyle("min-height").pfValue,top:o.pstyle("min-height-bias-top"),bottom:o.pstyle("min-height-bias-bottom")}},f=u.boundingBox({includeLabels:h,includeOverlays:!1,useCache:!1}),p=l.position;(f.w===0||f.h===0)&&(f={w:o.pstyle("width").pfValue,h:o.pstyle("height").pfValue},f.x1=p.x-f.w/2,f.x2=p.x+f.w/2,f.y1=p.y-f.h/2,f.y2=p.y+f.h/2);function m(M,P,N){var _=0,A=0,I=P+N;return M>0&&I>0&&(_=P/I*M,A=N/I*M),{biasDiff:_,biasComplementDiff:A}}s(m,"computeBiasValues");function g(M,P,N,_){if(N.units==="%")switch(_){case"width":return M>0?N.pfValue*M:0;case"height":return P>0?N.pfValue*P:0;case"average":return M>0&&P>0?N.pfValue*(M+P)/2:0;case"min":return M>0&&P>0?M>P?N.pfValue*P:N.pfValue*M:0;case"max":return M>0&&P>0?M>P?N.pfValue*M:N.pfValue*P:0;default:return 0}else return N.units==="px"?N.pfValue:0}s(g,"computePaddingValues");var y=d.width.left.value;d.width.left.units==="px"&&d.width.val>0&&(y=y*100/d.width.val);var v=d.width.right.value;d.width.right.units==="px"&&d.width.val>0&&(v=v*100/d.width.val);var x=d.height.top.value;d.height.top.units==="px"&&d.height.val>0&&(x=x*100/d.height.val);var b=d.height.bottom.value;d.height.bottom.units==="px"&&d.height.val>0&&(b=b*100/d.height.val);var T=m(d.width.val-f.w,y,v),w=T.biasDiff,C=T.biasComplementDiff,S=m(d.height.val-f.h,x,b),k=S.biasDiff,R=S.biasComplementDiff;l.autoPadding=g(f.w,f.h,o.pstyle("padding"),o.pstyle("padding-relative-to").value),l.autoWidth=Math.max(f.w,d.width.val),p.x=(-w+f.x1+f.x2+C)/2,l.autoHeight=Math.max(f.h,d.height.val),p.y=(-k+f.y1+f.y2+R)/2}s(r,"update");for(var n=0;nt.x2?i:t.x2,t.y1=nt.y2?a:t.y2,t.w=t.x2-t.x1,t.h=t.y2-t.y1)},"updateBounds"),Bf=s(function(t,r){return r==null?t:mc(t,r.x1,r.y1,r.x2,r.y2)},"updateBoundsFromBox"),TT=s(function(t,r,n){return Ws(t,r,n)},"prefixedProperty"),g5=s(function(t,r,n){if(!r.cy().headless()){var i=r._private,a=i.rstyle,o=a.arrowWidth/2,l=r.pstyle(n+"-arrow-shape").value,u,h;if(l!=="none"){n==="source"?(u=a.srcX,h=a.srcY):n==="target"?(u=a.tgtX,h=a.tgtY):(u=a.midX,h=a.midY);var d=i.arrowBounds=i.arrowBounds||{},f=d[n]=d[n]||{};f.x1=u-o,f.y1=h-o,f.x2=u+o,f.y2=h+o,f.w=f.x2-f.x1,f.h=f.y2-f.y1,w5(f,1),mc(t,f.x1,f.y1,f.x2,f.y2)}}},"updateBoundsFromArrow"),U$=s(function(t,r,n){if(!r.cy().headless()){var i;n?i=n+"-":i="";var a=r._private,o=a.rstyle,l=r.pstyle(i+"label").strValue;if(l){var u=r.pstyle("text-halign"),h=r.pstyle("text-valign"),d=TT(o,"labelWidth",n),f=TT(o,"labelHeight",n),p=TT(o,"labelX",n),m=TT(o,"labelY",n),g=r.pstyle(i+"text-margin-x").pfValue,y=r.pstyle(i+"text-margin-y").pfValue,v=r.isEdge(),x=r.pstyle(i+"text-rotation"),b=r.pstyle("text-outline-width").pfValue,T=r.pstyle("text-border-width").pfValue,w=T/2,C=r.pstyle("text-background-padding").pfValue,S=2,k=f,R=d,M=R/2,P=k/2,N,_,A,I;if(v)N=p-M,_=p+M,A=m-P,I=m+P;else{switch($1(u.value)){case"left":N=p-R,_=p;break;case"center":N=p-M,_=p+M;break;case"right":N=p,_=p+R;break}switch(F1(h.value)){case"top":A=m-k,I=m;break;case"center":A=m-P,I=m+P;break;case"bottom":A=m,I=m+k;break}}var D=g-Math.max(b,w)-C-S,L=g+Math.max(b,w)+C+S,B=y-Math.max(b,w)-C-S,O=y+Math.max(b,w)+C+S;N+=D,_+=L,A+=B,I+=O;var $=n||"main",W=a.labelBounds,V=W[$]=W[$]||{};V.x1=N,V.y1=A,V.x2=_,V.y2=I,V.w=_-N,V.h=I-A,V.leftPad=D,V.rightPad=L,V.topPad=B,V.botPad=O;var z=v&&x.strValue==="autorotate",H=x.pfValue!=null&&x.pfValue!==0;if(z||H){var K=z?TT(a.rstyle,"labelAngle",n):x.pfValue,J=Math.cos(K),ne=Math.sin(K),Z=(N+_)/2,pe=(A+I)/2;if(!v){switch($1(u.value)){case"left":Z=_;break;case"right":Z=N;break}switch(F1(h.value)){case"top":pe=I;break;case"bottom":pe=A;break}}var te=s(function(He,ye){return He=He-Z,ye=ye-pe,{x:He*J-ye*ne+Z,y:He*ne+ye*J+pe}},"rotate"),fe=te(N,A),ce=te(N,I),de=te(_,A),Ee=te(_,I);N=Math.min(fe.x,ce.x,de.x,Ee.x),_=Math.max(fe.x,ce.x,de.x,Ee.x),A=Math.min(fe.y,ce.y,de.y,Ee.y),I=Math.max(fe.y,ce.y,de.y,Ee.y)}var we=$+"Rot",Fe=W[we]=W[we]||{};Fe.x1=N,Fe.y1=A,Fe.x2=_,Fe.y2=I,Fe.w=_-N,Fe.h=I-A,mc(t,N,A,_,I),mc(a.labelBounds.all,N,A,_,I)}return t}},"updateBoundsFromLabel"),lbe=s(function(t,r){if(!r.cy().headless()){var n=r.pstyle("outline-opacity").value,i=r.pstyle("outline-width").value,a=r.pstyle("outline-offset").value,o=i+a;H2e(t,r,n,o,"outside",o/2)}},"updateBoundsFromOutline"),H2e=s(function(t,r,n,i,a,o){if(!(n===0||i<=0||a==="inside")){var l=r.cy(),u=l.renderer(),h=u.nodeShapes[u.getNodeShape(r)];if(h){var d=r.position(),f=d.x,p=d.y,m=r.width(),g=r.height();if(h.hasMiterBounds){a==="center"&&(i/=2);var y=h.miterBounds(f,p,m,g,i);Bf(t,y)}else o!=null&&o>0&&k5(t,[o,o,o,o])}}},"updateBoundsFromMiter"),sft=s(function(t,r){if(!r.cy().headless()){var n=r.pstyle("border-opacity").value,i=r.pstyle("border-width").pfValue,a=r.pstyle("border-position").value;H2e(t,r,n,i,a)}},"updateBoundsFromMiterBorder"),oft=s(function(t,r){var n=t._private.cy,i=n.styleEnabled(),a=n.headless(),o=qs(),l=t._private,u=t.isNode(),h=t.isEdge(),d,f,p,m,g,y,v=l.rstyle,x=u&&i?t.pstyle("bounds-expansion").pfValue:[0],b=s(function(Ye){return Ye.pstyle("display").value!=="none"},"isDisplayed"),T=!i||b(t)&&(!h||b(t.source())&&b(t.target()));if(T){var w=0,C=0;i&&r.includeOverlays&&(w=t.pstyle("overlay-opacity").value,w!==0&&(C=t.pstyle("overlay-padding").value));var S=0,k=0;i&&r.includeUnderlays&&(S=t.pstyle("underlay-opacity").value,S!==0&&(k=t.pstyle("underlay-padding").value));var R=Math.max(C,k),M=0,P=0;if(i&&(M=t.pstyle("width").pfValue,P=M/2),u&&r.includeNodes){var N=t.position();g=N.x,y=N.y;var _=t.outerWidth(),A=_/2,I=t.outerHeight(),D=I/2;d=g-A,f=g+A,p=y-D,m=y+D,mc(o,d,p,f,m),i&&lbe(o,t),i&&r.includeOutlines&&!a&&lbe(o,t),i&&sft(o,t)}else if(h&&r.includeEdges)if(i&&!a){var L=t.pstyle("curve-style").strValue;if(d=Math.min(v.srcX,v.midX,v.tgtX),f=Math.max(v.srcX,v.midX,v.tgtX),p=Math.min(v.srcY,v.midY,v.tgtY),m=Math.max(v.srcY,v.midY,v.tgtY),d-=P,f+=P,p-=P,m+=P,mc(o,d,p,f,m),L==="haystack"){var B=v.haystackPts;if(B&&B.length===2){if(d=B[0].x,p=B[0].y,f=B[1].x,m=B[1].y,d>f){var O=d;d=f,f=O}if(p>m){var $=p;p=m,m=$}mc(o,d-P,p-P,f+P,m+P)}}else if(L==="bezier"||L==="unbundled-bezier"||$f(L,"segments")||$f(L,"taxi")){var W;switch(L){case"bezier":case"unbundled-bezier":W=v.bezierPts;break;case"segments":case"taxi":case"round-segments":case"round-taxi":W=v.linePts;break}if(W!=null)for(var V=0;Vf){var Z=d;d=f,f=Z}if(p>m){var pe=p;p=m,m=pe}d-=P,f+=P,p-=P,m+=P,mc(o,d,p,f,m)}if(i&&r.includeEdges&&h&&(g5(o,t,"mid-source"),g5(o,t,"mid-target"),g5(o,t,"source"),g5(o,t,"target")),i){var te=t.pstyle("ghost").value==="yes";if(te){var fe=t.pstyle("ghost-offset-x").pfValue,ce=t.pstyle("ghost-offset-y").pfValue;mc(o,o.x1+fe,o.y1+ce,o.x2+fe,o.y2+ce)}}var de=l.bodyBounds=l.bodyBounds||{};Yve(de,o),k5(de,x),w5(de,1),i&&(d=o.x1,f=o.x2,p=o.y1,m=o.y2,mc(o,d-R,p-R,f+R,m+R));var Ee=l.overlayBounds=l.overlayBounds||{};Yve(Ee,o),k5(Ee,x),w5(Ee,1);var we=l.labelBounds=l.labelBounds||{};we.all!=null?zut(we.all):we.all=qs(),i&&r.includeLabels&&(r.includeMainLabels&&U$(o,t,null),h&&(r.includeSourceLabels&&U$(o,t,"source"),r.includeTargetLabels&&U$(o,t,"target")))}return o.x1=Rl(o.x1),o.y1=Rl(o.y1),o.x2=Rl(o.x2),o.y2=Rl(o.y2),o.w=Rl(o.x2-o.x1),o.h=Rl(o.y2-o.y1),o.w>0&&o.h>0&&T&&(k5(o,x),w5(o,1)),o},"boundingBoxImpl"),U2e=s(function(t){var r=0,n=s(function(o){return(o?1:0)<=0;l--)o(l);return this};jf.removeAllListeners=function(){return this.removeListener("*")};jf.emit=jf.trigger=function(e,t,r){var n=this.listeners,i=n.length;return this.emitting++,Xn(t)||(t=[t]),Sft(this,function(a,o){r!=null&&(n=[{event:o.event,type:o.type,namespace:o.namespace,callback:r}],i=n.length);for(var l=s(function(){var d=n[u];if(d.type===o.type&&(!d.namespace||d.namespace===o.namespace||d.namespace===Tft)&&a.eventMatches(a.context,d,o)){var f=[o];t!=null&&dut(f,t),a.beforeEmit(a.context,d,o),d.conf&&d.conf.one&&(a.listeners=a.listeners.filter(function(g){return g!==d}));var p=a.callbackContext(a.context,d,o),m=d.callback.apply(p,f);a.afterEmit(a.context,d,o),m===!1&&(o.stopPropagation(),o.preventDefault())}},"_loop2"),u=0;u1&&!o){var l=this.length-1,u=this[l],h=u._private.data.id;this[l]=void 0,this[t]=u,a.set(h,{ele:u,index:t})}return this.length--,this},"unmergeAt"),unmergeOne:s(function(t){t=t[0];var r=this._private,n=t._private.data.id,i=r.map,a=i.get(n);if(!a)return this;var o=a.index;return this.unmergeAt(o),this},"unmergeOne"),unmerge:s(function(t){var r=this._private.cy;if(!t)return this;if(t&&yr(t)){var n=t;t=r.mutableElements().filter(n)}for(var i=0;i=0;r--){var n=this[r];t(n)&&this.unmergeAt(r)}return this},"unmergeBy"),map:s(function(t,r){for(var n=[],i=this,a=0;an&&(n=u,i=l)}return{value:n,ele:i}},"max"),min:s(function(t,r){for(var n=1/0,i,a=this,o=0;o=0&&a"u"?"undefined":ya(Symbol))!=t&&ya(Symbol.iterator)!=t;r&&(z5[Symbol.iterator]=function(){var n=this,i={value:void 0,done:!1},a=0,o=this.length;return Jbe({next:s(function(){return a1&&arguments[1]!==void 0?arguments[1]:!0,n=this[0],i=n.cy();if(i.styleEnabled()&&n){n._private.styleDirty&&(n._private.styleDirty=!1,i.style().apply(n));var a=n._private.style[t];return a??(r?i.style().getDefaultProperty(t):null)}},"parsedStyle"),numericStyle:s(function(t){var r=this[0];if(r.cy().styleEnabled()&&r){var n=r.pstyle(t);return n.pfValue!==void 0?n.pfValue:n.value}},"numericStyle"),numericStyleUnits:s(function(t){var r=this[0];if(r.cy().styleEnabled()&&r)return r.pstyle(t).units},"numericStyleUnits"),renderedStyle:s(function(t){var r=this.cy();if(!r.styleEnabled())return this;var n=this[0];if(n)return r.style().getRenderedStyle(n,t)},"renderedStyle"),style:s(function(t,r){var n=this.cy();if(!n.styleEnabled())return this;var i=!1,a=n.style();if(mn(t)){var o=t;a.applyBypass(this,o,i),this.emitAndNotify("style")}else if(yr(t))if(r===void 0){var l=this[0];return l?a.getStylePropertyValue(l,t):void 0}else a.applyBypass(this,t,r,i),this.emitAndNotify("style");else if(t===void 0){var u=this[0];return u?a.getRawStyle(u):void 0}return this},"style"),removeStyle:s(function(t){var r=this.cy();if(!r.styleEnabled())return this;var n=!1,i=r.style(),a=this;if(t===void 0)for(var o=0;o0&&t.push(d[0]),t.push(l[0])}return this.spawn(t,!0).filter(e)},"neighborhood"),closedNeighborhood:s(function(t){return this.neighborhood().add(this).filter(t)},"closedNeighborhood"),openNeighborhood:s(function(t){return this.neighborhood(t)},"openNeighborhood")});Ts.neighbourhood=Ts.neighborhood;Ts.closedNeighbourhood=Ts.closedNeighborhood;Ts.openNeighbourhood=Ts.openNeighborhood;kr(Ts,{source:_l(s(function(t){var r=this[0],n;return r&&(n=r._private.source||r.cy().collection()),n&&t?n.filter(t):n},"sourceImpl"),"source"),target:_l(s(function(t){var r=this[0],n;return r&&(n=r._private.target||r.cy().collection()),n&&t?n.filter(t):n},"targetImpl"),"target"),sources:xbe({attr:"source"}),targets:xbe({attr:"target"})});s(xbe,"defineSourceFunction");kr(Ts,{edgesWith:_l(bbe(),"edgesWith"),edgesTo:_l(bbe({thisIsSrc:!0}),"edgesTo")});s(bbe,"defineEdgesWithFunction");kr(Ts,{connectedEdges:_l(function(e){for(var t=[],r=this,n=0;n0);return o},"components"),component:s(function(){var t=this[0];return t.cy().mutableElements().components(t)[0]},"component")});Ts.componentsOf=Ts.components;es=s(function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!1,i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!1;if(t===void 0){xi("A collection must have a reference to the core");return}var a=new Oh,o=!1;if(!r)r=[];else if(r.length>0&&mn(r[0])&&!qT(r[0])){o=!0;for(var l=[],u=new G1,h=0,d=r.length;h0&&arguments[0]!==void 0?arguments[0]:!0,t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,r=this,n=r.cy(),i=n._private,a=[],o=[],l,u=0,h=r.length;u0){for(var $=l.length===r.length?r:new es(n,l),W=0;W<$.length;W++){var V=$[W];V.isNode()||(V.parallelEdges().clearTraversalCache(),V.source().clearTraversalCache(),V.target().clearTraversalCache())}var z;i.hasCompoundNodes?z=n.collection().merge($).merge($.connectedNodes()).merge($.parent()):z=$,z.dirtyCompoundBoundsCache().dirtyBoundingBoxCache().updateStyle(e),e?$.emitAndNotify("add"):t&&$.emit("add")}return r};ei.removed=function(){var e=this[0];return e&&e._private.removed};ei.inside=function(){var e=this[0];return e&&!e._private.removed};ei.remove=function(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!0,t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,r=this,n=[],i={},a=r._private.cy;function o(I){for(var D=I._private.edges,L=0;L0&&(e?N.emitAndNotify("remove"):t&&N.emit("remove"));for(var _=0;_d&&Math.abs(g.v)>d;);return p?function(y){return u[y*(u.length-1)|0]}:h},"springRK4Factory")})(),Jn=s(function(t,r,n,i){var a=Mft(t,r,n,i);return function(o,l,u){return o+(l-o)*a(u)}},"cubicBezier"),R5={linear:s(function(t,r,n){return t+(r-t)*n},"linear"),ease:Jn(.25,.1,.25,1),"ease-in":Jn(.42,0,1,1),"ease-out":Jn(0,0,.58,1),"ease-in-out":Jn(.42,0,.58,1),"ease-in-sine":Jn(.47,0,.745,.715),"ease-out-sine":Jn(.39,.575,.565,1),"ease-in-out-sine":Jn(.445,.05,.55,.95),"ease-in-quad":Jn(.55,.085,.68,.53),"ease-out-quad":Jn(.25,.46,.45,.94),"ease-in-out-quad":Jn(.455,.03,.515,.955),"ease-in-cubic":Jn(.55,.055,.675,.19),"ease-out-cubic":Jn(.215,.61,.355,1),"ease-in-out-cubic":Jn(.645,.045,.355,1),"ease-in-quart":Jn(.895,.03,.685,.22),"ease-out-quart":Jn(.165,.84,.44,1),"ease-in-out-quart":Jn(.77,0,.175,1),"ease-in-quint":Jn(.755,.05,.855,.06),"ease-out-quint":Jn(.23,1,.32,1),"ease-in-out-quint":Jn(.86,0,.07,1),"ease-in-expo":Jn(.95,.05,.795,.035),"ease-out-expo":Jn(.19,1,.22,1),"ease-in-out-expo":Jn(1,0,0,1),"ease-in-circ":Jn(.6,.04,.98,.335),"ease-out-circ":Jn(.075,.82,.165,1),"ease-in-out-circ":Jn(.785,.135,.15,.86),spring:s(function(t,r,n){if(n===0)return R5.linear;var i=Nft(t,r,n);return function(a,o,l){return a+(o-a)*i(l)}},"spring"),"cubic-bezier":Jn};s(Cbe,"getEasedValue");s(Sbe,"getValue");s(C1,"ease");s(Pft,"step$1");s(ST,"valid");s(Oft,"startAnimation");s(wbe,"stepAll");Bft={animate:Pn.animate(),animation:Pn.animation(),animated:Pn.animated(),clearQueue:Pn.clearQueue(),delay:Pn.delay(),delayAnimation:Pn.delayAnimation(),stop:Pn.stop(),addToAnimationPool:s(function(t){var r=this;r.styleEnabled()&&r._private.aniEles.merge(t)},"addToAnimationPool"),stopAnimationLoop:s(function(){this._private.animationsRunning=!1},"stopAnimationLoop"),startAnimationLoop:s(function(){var t=this;if(t._private.animationsRunning=!0,!t.styleEnabled())return;function r(){t._private.animationsRunning&&O5(s(function(a){wbe(a,t),r()},"animationStep"))}s(r,"headlessStep");var n=t.renderer();n&&n.beforeRender?n.beforeRender(s(function(a,o){wbe(o,t)},"rendererAnimationStep"),n.beforeRenderPriorities.animations):r()},"startAnimationLoop")},$ft={qualifierCompare:s(function(t,r){return t==null||r==null?t==null&&r==null:t.sameText(r)},"qualifierCompare"),eventMatches:s(function(t,r,n){var i=r.qualifier;return i!=null?t!==n.target&&qT(n.target)&&i.matches(n.target):!0},"eventMatches"),addEventFields:s(function(t,r){r.cy=t,r.target=t},"addEventFields"),callbackContext:s(function(t,r,n){return r.qualifier!=null?n.target:t},"callbackContext")},x5=s(function(t){return yr(t)?new Uf(t):t},"argSelector"),nTe={createEmitter:s(function(){var t=this._private;return t.emitter||(t.emitter=new iA($ft,this)),this},"createEmitter"),emitter:s(function(){return this._private.emitter},"emitter"),on:s(function(t,r,n){return this.emitter().on(t,x5(r),n),this},"on"),removeListener:s(function(t,r,n){return this.emitter().removeListener(t,x5(r),n),this},"removeListener"),removeAllListeners:s(function(){return this.emitter().removeAllListeners(),this},"removeAllListeners"),one:s(function(t,r,n){return this.emitter().one(t,x5(r),n),this},"one"),once:s(function(t,r,n){return this.emitter().one(t,x5(r),n),this},"once"),emit:s(function(t,r){return this.emitter().emit(t,r),this},"emit"),emitAndNotify:s(function(t,r){return this.emit(t),this.notify(t,r),this},"emitAndNotify")};Pn.eventAliasesOn(nTe);dF={png:s(function(t){var r=this._private.renderer;return t=t||{},r.png(t)},"png"),jpg:s(function(t){var r=this._private.renderer;return t=t||{},t.bg=t.bg||"#fff",r.jpg(t)},"jpg")};dF.jpeg=dF.jpg;_5={layout:s(function(t){var r=this;if(t==null){xi("Layout options must be specified to make a layout");return}if(t.name==null){xi("A `name` must be specified to make a layout");return}var n=t.name,i=r.extension("layout",n);if(i==null){xi("No such layout `"+n+"` found. Did you forget to import it and `cytoscape.use()` it?");return}var a;yr(t.eles)?a=r.$(t.eles):a=t.eles!=null?t.eles:r.$();var o=new i(kr({},t,{cy:r,eles:a}));return o},"layout")};_5.createLayout=_5.makeLayout=_5.layout;Fft={notify:s(function(t,r){var n=this._private;if(this.batching()){n.batchNotifications=n.batchNotifications||{};var i=n.batchNotifications[t]=n.batchNotifications[t]||this.collection();r!=null&&i.merge(r);return}if(n.notificationsEnabled){var a=this.renderer();this.destroyed()||!a||a.notify(t,r)}},"notify"),notifications:s(function(t){var r=this._private;return t===void 0?r.notificationsEnabled:(r.notificationsEnabled=!!t,this)},"notifications"),noNotifications:s(function(t){this.notifications(!1),t(),this.notifications(!0)},"noNotifications"),batching:s(function(){return this._private.batchCount>0},"batching"),startBatch:s(function(){var t=this._private;return t.batchCount==null&&(t.batchCount=0),t.batchCount===0&&(t.batchStyleEles=this.collection(),t.batchNotifications={}),t.batchCount++,this},"startBatch"),endBatch:s(function(){var t=this._private;if(t.batchCount===0)return this;if(t.batchCount--,t.batchCount===0){t.batchStyleEles.updateStyle();var r=this.renderer();Object.keys(t.batchNotifications).forEach(function(n){var i=t.batchNotifications[n];i.empty()?r.notify(n):r.notify(n,i)})}return this},"endBatch"),batch:s(function(t){return this.startBatch(),t(),this.endBatch(),this},"batch"),batchData:s(function(t){var r=this;return this.batch(function(){for(var n=Object.keys(t),i=0;i0;)r.removeChild(r.childNodes[0]);t._private.renderer=null,t.mutableElements().forEach(function(n){var i=n._private;i.rscratch={},i.rstyle={},i.animation.current=[],i.animation.queue=[]})},"destroyRenderer"),onRender:s(function(t){return this.on("render",t)},"onRender"),offRender:s(function(t){return this.off("render",t)},"offRender")};fF.invalidateDimensions=fF.resize;L5={collection:s(function(t,r){return yr(t)?this.$(t):Zo(t)?t.collection():Xn(t)?(r||(r={}),new es(this,t,r.unique,r.removed)):new es(this)},"collection"),nodes:s(function(t){var r=this.$(function(n){return n.isNode()});return t?r.filter(t):r},"nodes"),edges:s(function(t){var r=this.$(function(n){return n.isEdge()});return t?r.filter(t):r},"edges"),$:s(function(t){var r=this._private.elements;return t?r.filter(t):r.spawnSelf()},"$"),mutableElements:s(function(){return this._private.elements},"mutableElements")};L5.elements=L5.filter=L5.$;za={},LT="t",zft="f";za.apply=function(e){for(var t=this,r=t._private,n=r.cy,i=n.collection(),a=0;a0;if(p||f&&m){var g=void 0;p&&m||p?g=h.properties:m&&(g=h.mappedProperties);for(var y=0;y1&&(w=1),l.color){var S=n.valueMin[0],k=n.valueMax[0],R=n.valueMin[1],M=n.valueMax[1],P=n.valueMin[2],N=n.valueMax[2],_=n.valueMin[3]==null?1:n.valueMin[3],A=n.valueMax[3]==null?1:n.valueMax[3],I=[Math.round(S+(k-S)*w),Math.round(R+(M-R)*w),Math.round(P+(N-P)*w),Math.round(_+(A-_)*w)];a={bypass:n.bypass,name:n.name,value:I,strValue:"rgb("+I[0]+", "+I[1]+", "+I[2]+")"}}else if(l.number){var D=n.valueMin+(n.valueMax-n.valueMin)*w;a=this.parse(n.name,D,n.bypass,p)}else return!1;if(!a)return y(),!1;a.mapping=n,n=a;break}case o.data:{for(var L=n.field.split("."),B=f.data,O=0;O0&&a>0){for(var l={},u=!1,h=0;h0?e.delayAnimation(o).play().promise().then(T):T()}).then(function(){return e.animation({style:l,duration:a,easing:e.pstyle("transition-timing-function").value,queue:!1}).play().promise()}).then(function(){r.removeBypasses(e,i),e.emitAndNotify("style"),n.transitioning=!1})}else n.transitioning&&(this.removeBypasses(e,i),e.emitAndNotify("style"),n.transitioning=!1)};za.checkTrigger=function(e,t,r,n,i,a){var o=this.properties[t],l=i(o);e.removed()||l!=null&&l(r,n,e)&&a(o)};za.checkZOrderTrigger=function(e,t,r,n){var i=this;this.checkTrigger(e,t,r,n,function(a){return a.triggersZOrder},function(){i._private.cy.notify("zorder",e)})};za.checkBoundsTrigger=function(e,t,r,n){this.checkTrigger(e,t,r,n,function(i){return i.triggersBounds},function(i){e.dirtyCompoundBoundsCache(),e.dirtyBoundingBoxCache()})};za.checkConnectedEdgesBoundsTrigger=function(e,t,r,n){this.checkTrigger(e,t,r,n,function(i){return i.triggersBoundsOfConnectedEdges},function(i){e.connectedEdges().forEach(function(a){a.dirtyBoundingBoxCache()})})};za.checkParallelEdgesBoundsTrigger=function(e,t,r,n){this.checkTrigger(e,t,r,n,function(i){return i.triggersBoundsOfParallelEdges},function(i){e.parallelEdges().forEach(function(a){a.dirtyBoundingBoxCache()})})};za.checkTriggers=function(e,t,r,n){e.dirtyStyleCache(),this.checkZOrderTrigger(e,t,r,n),this.checkBoundsTrigger(e,t,r,n),this.checkConnectedEdgesBoundsTrigger(e,t,r,n),this.checkParallelEdgesBoundsTrigger(e,t,r,n)};ZT={};ZT.applyBypass=function(e,t,r,n){var i=this,a=[],o=!0;if(t==="*"||t==="**"){if(r!==void 0)for(var l=0;li.length?n=n.substr(i.length):n=""}s(l,"removeSelAndBlockFromRemaining");function u(){a.length>o.length?a=a.substr(o.length):a=""}for(s(u,"removePropAndValFromRem");;){var h=n.match(/^\s*$/);if(h)break;var d=n.match(/^\s*((?:.|\s)+?)\s*\{((?:.|\s)+?)\}/);if(!d){On("Halting stylesheet parsing: String stylesheet contains more to parse but no selector and block found in: "+n);break}i=d[0];var f=d[1];if(f!=="core"){var p=new Uf(f);if(p.invalid){On("Skipping parsing of block: Invalid selector found in string stylesheet: "+f),l();continue}}var m=d[2],g=!1;a=m;for(var y=[];;){var v=a.match(/^\s*$/);if(v)break;var x=a.match(/^\s*(.+?)\s*:\s*(.+?)(?:\s*;|\s*$)/);if(!x){On("Skipping parsing of block: Invalid formatting of style property and value definitions found in:"+m),g=!0;break}o=x[0];var b=x[1],T=x[2],w=t.properties[b];if(!w){On("Skipping property: Invalid property name in: "+o),u();continue}var C=r.parse(b,T);if(!C){On("Skipping property: Invalid property definition in: "+o),u();continue}y.push({name:b,val:T}),u()}if(g){l();break}r.selector(f);for(var S=0;S=7&&t[0]==="d"&&(d=new RegExp(l.data.regex).exec(t))){if(r)return!1;var p=l.data;return{name:e,value:d,strValue:""+t,mapped:p,field:d[1],bypass:r}}else if(t.length>=10&&t[0]==="m"&&(f=new RegExp(l.mapData.regex).exec(t))){if(r||h.multiple)return!1;var m=l.mapData;if(!(h.color||h.number))return!1;var g=this.parse(e,f[4]);if(!g||g.mapped)return!1;var y=this.parse(e,f[5]);if(!y||y.mapped)return!1;if(g.pfValue===y.pfValue||g.strValue===y.strValue)return On("`"+e+": "+t+"` is not a valid mapper because the output range is zero; converting to `"+e+": "+g.strValue+"`"),this.parse(e,g.strValue);if(h.color){var v=g.value,x=y.value,b=v[0]===x[0]&&v[1]===x[1]&&v[2]===x[2]&&(v[3]===x[3]||(v[3]==null||v[3]===1)&&(x[3]==null||x[3]===1));if(b)return!1}return{name:e,value:f,strValue:""+t,mapped:m,field:f[1],fieldMin:parseFloat(f[2]),fieldMax:parseFloat(f[3]),valueMin:g.value,valueMax:y.value,bypass:r}}}if(h.multiple&&n!=="multiple"){var T;if(u?T=t.split(/\s+/):Xn(t)?T=t:T=[t],h.evenMultiple&&T.length%2!==0)return null;for(var w=[],C=[],S=[],k="",R=!1,M=0;M0?" ":"")+P.strValue}return h.validate&&!h.validate(w,C)?null:h.singleEnum&&R?w.length===1&&yr(w[0])?{name:e,value:w[0],strValue:w[0],bypass:r}:null:{name:e,value:w,pfValue:S,strValue:k,bypass:r,units:C}}var N=s(function(){for(var te=0;teh.max||h.strictMax&&t===h.max))return null;var L={name:e,value:t,strValue:""+t+(_||""),units:_,bypass:r};return h.unitless||_!=="px"&&_!=="em"?L.pfValue=t:L.pfValue=_==="px"||!_?t:this.getEmSizeInPixels()*t,(_==="ms"||_==="s")&&(L.pfValue=_==="ms"?t:1e3*t),(_==="deg"||_==="rad")&&(L.pfValue=_==="rad"?t:But(t)),_==="%"&&(L.pfValue=t/100),L}else if(h.propList){var B=[],O=""+t;if(O!=="none"){for(var $=O.split(/\s*,\s*|\s+/),W=0;W<$.length;W++){var V=$[W].trim();i.properties[V]?B.push(V):On("`"+V+"` is not a valid property name")}if(B.length===0)return null}return{name:e,value:B,strValue:B.length===0?"none":B.join(" "),bypass:r}}else if(h.color){var z=s2e(t);return z?{name:e,value:z,pfValue:z,strValue:"rgb("+z[0]+","+z[1]+","+z[2]+")",bypass:r}:null}else if(h.regex||h.regexes){if(h.enums){var H=N();if(H)return H}for(var K=h.regexes?h.regexes:[h.regex],J=0;J0&&l>0&&!isNaN(n.w)&&!isNaN(n.h)&&n.w>0&&n.h>0){u=Math.min((o-2*r)/n.w,(l-2*r)/n.h),u=u>this._private.maxZoom?this._private.maxZoom:u,u=u=n.minZoom&&(n.maxZoom=r),this},"zoomRange"),minZoom:s(function(t){return t===void 0?this._private.minZoom:this.zoomRange({min:t})},"minZoom"),maxZoom:s(function(t){return t===void 0?this._private.maxZoom:this.zoomRange({max:t})},"maxZoom"),getZoomedViewport:s(function(t){var r=this._private,n=r.pan,i=r.zoom,a,o,l=!1;if(r.zoomingEnabled||(l=!0),Vt(t)?o=t:mn(t)&&(o=t.level,t.position!=null?a=Z5(t.position,i,n):t.renderedPosition!=null&&(a=t.renderedPosition),a!=null&&!r.panningEnabled&&(l=!0)),o=o>r.maxZoom?r.maxZoom:o,o=or.maxZoom||!r.zoomingEnabled?o=!0:(r.zoom=u,a.push("zoom"))}if(i&&(!o||!t.cancelOnFailedZoom)&&r.panningEnabled){var h=t.pan;Vt(h.x)&&(r.pan.x=h.x,l=!1),Vt(h.y)&&(r.pan.y=h.y,l=!1),l||a.push("pan")}return a.length>0&&(a.push("viewport"),this.emit(a.join(" ")),this.notify("viewport")),this},"viewport"),center:s(function(t){var r=this.getCenterPan(t);return r&&(this._private.pan=r,this.emit("pan viewport"),this.notify("viewport")),this},"center"),getCenterPan:s(function(t,r){if(this._private.panningEnabled){if(yr(t)){var n=t;t=this.mutableElements().filter(n)}else Zo(t)||(t=this.mutableElements());if(t.length!==0){var i=t.boundingBox(),a=this.width(),o=this.height();r=r===void 0?this._private.zoom:r;var l={x:(a-r*(i.x1+i.x2))/2,y:(o-r*(i.y1+i.y2))/2};return l}}},"getCenterPan"),reset:s(function(){return!this._private.panningEnabled||!this._private.zoomingEnabled?this:(this.viewport({pan:{x:0,y:0},zoom:1}),this)},"reset"),invalidateSize:s(function(){this._private.sizeCache=null},"invalidateSize"),size:s(function(){var t=this._private,r=t.container,n=this;return t.sizeCache=t.sizeCache||(r?(function(){var i=n.window().getComputedStyle(r),a=s(function(l){return parseFloat(i.getPropertyValue(l))},"val");return{width:r.clientWidth-a("padding-left")-a("padding-right"),height:r.clientHeight-a("padding-top")-a("padding-bottom")}})():{width:1,height:1})},"size"),width:s(function(){return this.size().width},"width"),height:s(function(){return this.size().height},"height"),extent:s(function(){var t=this._private.pan,r=this._private.zoom,n=this.renderedExtent(),i={x1:(n.x1-t.x)/r,x2:(n.x2-t.x)/r,y1:(n.y1-t.y)/r,y2:(n.y2-t.y)/r};return i.w=i.x2-i.x1,i.h=i.y2-i.y1,i},"extent"),renderedExtent:s(function(){var t=this.width(),r=this.height();return{x1:0,y1:0,x2:t,y2:r,w:t,h:r}},"renderedExtent"),multiClickDebounceTime:s(function(t){if(t)this._private.multiClickDebounceTime=t;else return this._private.multiClickDebounceTime;return this},"multiClickDebounceTime")};ag.centre=ag.center;ag.autolockNodes=ag.autolock;ag.autoungrabifyNodes=ag.autoungrabify;GT={data:Pn.data({field:"data",bindingEvent:"data",allowBinding:!0,allowSetting:!0,settingEvent:"data",settingTriggersEvent:!0,triggerFnName:"trigger",allowGetting:!0,updateStyle:!0}),removeData:Pn.removeData({field:"data",event:"data",triggerFnName:"trigger",triggerEvent:!0,updateStyle:!0}),scratch:Pn.data({field:"scratch",bindingEvent:"scratch",allowBinding:!0,allowSetting:!0,settingEvent:"scratch",settingTriggersEvent:!0,triggerFnName:"trigger",allowGetting:!0,updateStyle:!0}),removeScratch:Pn.removeData({field:"scratch",event:"scratch",triggerFnName:"trigger",triggerEvent:!0,updateStyle:!0})};GT.attr=GT.data;GT.removeAttr=GT.removeData;zT=s(function(t){var r=this;t=kr({},t);var n=t.container;n&&!P5(n)&&P5(n[0])&&(n=n[0]);var i=n?n._cyreg:null;i=i||{},i&&i.cy&&(i.cy.destroy(),i={});var a=i.readies=i.readies||[];n&&(n._cyreg=i),i.cy=r;var o=ma!==void 0&&n!==void 0&&!t.headless,l=t;l.layout=kr({name:o?"grid":"null"},l.layout),l.renderer=kr({name:o?"canvas":"null"},l.renderer);var u=s(function(g,y,v){return y!==void 0?y:v!==void 0?v:g},"defVal"),h=this._private={container:n,ready:!1,options:l,elements:new es(this),listeners:[],aniEles:new es(this),data:l.data||{},scratch:{},layout:null,renderer:null,destroyed:!1,notificationsEnabled:!0,minZoom:1e-50,maxZoom:1e50,zoomingEnabled:u(!0,l.zoomingEnabled),userZoomingEnabled:u(!0,l.userZoomingEnabled),panningEnabled:u(!0,l.panningEnabled),userPanningEnabled:u(!0,l.userPanningEnabled),boxSelectionEnabled:u(!0,l.boxSelectionEnabled),autolock:u(!1,l.autolock,l.autolockNodes),autoungrabify:u(!1,l.autoungrabify,l.autoungrabifyNodes),autounselectify:u(!1,l.autounselectify),styleEnabled:l.styleEnabled===void 0?o:l.styleEnabled,zoom:Vt(l.zoom)?l.zoom:1,pan:{x:mn(l.pan)&&Vt(l.pan.x)?l.pan.x:0,y:mn(l.pan)&&Vt(l.pan.y)?l.pan.y:0},animation:{current:[],queue:[]},hasCompoundNodes:!1,multiClickDebounceTime:u(250,l.multiClickDebounceTime)};this.createEmitter(),this.selectionType(l.selectionType),this.zoomRange({min:l.minZoom,max:l.maxZoom});var d=s(function(g,y){var v=g.some(Dct);if(v)return z1.all(g).then(y);y(g)},"loadExtData");h.styleEnabled&&r.setStyle([]);var f=kr({},l,l.renderer);r.initRenderer(f);var p=s(function(g,y,v){r.notifications(!1);var x=r.mutableElements();x.length>0&&x.remove(),g!=null&&(mn(g)||Xn(g))&&r.add(g),r.one("layoutready",function(T){r.notifications(!0),r.emit(T),r.one("load",y),r.emitAndNotify("load")}).one("layoutstop",function(){r.one("done",v),r.emit("done")});var b=kr({},r._private.options.layout);b.eles=r.elements(),r.layout(b).run()},"setElesAndLayout");d([l.style,l.elements],function(m){var g=m[0],y=m[1];h.styleEnabled&&r.style().append(g),p(y,function(){r.startAnimationLoop(),h.ready=!0,Ai(l.ready)&&r.on("ready",l.ready);for(var v=0;v0,l=!!e.boundingBox,u=qs(l?e.boundingBox:structuredClone(t.extent())),h;if(Zo(e.roots))h=e.roots;else if(Xn(e.roots)){for(var d=[],f=0;f0;){var I=A(),D=M(I,N);if(D)I.outgoers().filter(function(Be){return Be.isNode()&&r.has(Be)}).forEach(_);else if(D===null){On("Detected double maximal shift for node `"+I.id()+"`. Bailing maximal adjustment due to cycle. Use `options.maximal: true` only on DAGs.");break}}}var L=0;if(e.avoidOverlap)for(var B=0;B0&&x[0].length<=3?le/2:0),Te=2*Math.PI/x[re].length*ue;return re===0&&x[0].length===1&&(he=1),{x:de.x+he*Math.cos(Te),y:de.y+he*Math.sin(Te)}}else{var oe=x[re].length,G=Math.max(oe===1?0:l?(u.w-e.padding*2-Ee.w)/((e.grid?Fe:oe)-1):(u.w-e.padding*2-Ee.w)/((e.grid?Fe:oe)+1),L),X={x:de.x+(ue+1-(oe+1)/2)*G,y:de.y+(re+1-(J+1)/2)*we};return X}},"getPositionTopBottom"),He={downward:0,leftward:90,upward:180,rightward:-90};Object.keys(He).indexOf(e.direction)===-1&&xi("Invalid direction '".concat(e.direction,"' specified for breadthfirst layout. Valid values are: ").concat(Object.keys(He).join(", ")));var ye=s(function(Ie){return sut(Ye(Ie),u,He[e.direction])},"getPosition");return r.nodes().layoutPositions(this,e,ye),this};Uft={fit:!0,padding:30,boundingBox:void 0,avoidOverlap:!0,nodeDimensionsIncludeLabels:!1,spacingFactor:void 0,radius:void 0,startAngle:3/2*Math.PI,sweep:void 0,clockwise:!0,sort:void 0,animate:!1,animationDuration:500,animationEasing:void 0,animateFilter:s(function(t,r){return!0},"animateFilter"),ready:void 0,stop:void 0,transform:s(function(t,r){return r},"transform")};s(aTe,"CircleLayout");aTe.prototype.run=function(){var e=this.options,t=e,r=e.cy,n=t.eles,i=t.counterclockwise!==void 0?!t.counterclockwise:t.clockwise,a=n.nodes().not(":parent");t.sort&&(a=a.sort(t.sort));for(var o=qs(t.boundingBox?t.boundingBox:{x1:0,y1:0,w:r.width(),h:r.height()}),l={x:o.x1+o.w/2,y:o.y1+o.h/2},u=t.sweep===void 0?2*Math.PI-2*Math.PI/a.length:t.sweep,h=u/Math.max(1,a.length-1),d,f=0,p=0;p1&&t.avoidOverlap){f*=1.75;var x=Math.cos(h)-Math.cos(0),b=Math.sin(h)-Math.sin(0),T=Math.sqrt(f*f/(x*x+b*b));d=Math.max(T,d)}var w=s(function(S,k){var R=t.startAngle+k*h*(i?1:-1),M=d*Math.cos(R),P=d*Math.sin(R),N={x:l.x+M,y:l.y+P};return N},"getPos");return n.nodes().layoutPositions(this,t,w),this};Yft={fit:!0,padding:30,startAngle:3/2*Math.PI,sweep:void 0,clockwise:!0,equidistant:!1,minNodeSpacing:10,boundingBox:void 0,avoidOverlap:!0,nodeDimensionsIncludeLabels:!1,height:void 0,width:void 0,spacingFactor:void 0,concentric:s(function(t){return t.degree()},"concentric"),levelWidth:s(function(t){return t.maxDegree()/4},"levelWidth"),animate:!1,animationDuration:500,animationEasing:void 0,animateFilter:s(function(t,r){return!0},"animateFilter"),ready:void 0,stop:void 0,transform:s(function(t,r){return r},"transform")};s(sTe,"ConcentricLayout");sTe.prototype.run=function(){for(var e=this.options,t=e,r=t.counterclockwise!==void 0?!t.counterclockwise:t.clockwise,n=e.cy,i=t.eles,a=i.nodes().not(":parent"),o=qs(t.boundingBox?t.boundingBox:{x1:0,y1:0,w:n.width(),h:n.height()}),l={x:o.x1+o.w/2,y:o.y1+o.h/2},u=[],h=0,d=0;d0){var C=Math.abs(b[0].value-w.value);C>=v&&(b=[],x.push(b))}b.push(w)}var S=h+t.minNodeSpacing;if(!t.avoidOverlap){var k=x.length>0&&x[0].length>1,R=Math.min(o.w,o.h)/2-S,M=R/(x.length+k?1:0);S=Math.min(S,M)}for(var P=0,N=0;N1&&t.avoidOverlap){var D=Math.cos(I)-Math.cos(0),L=Math.sin(I)-Math.sin(0),B=Math.sqrt(S*S/(D*D+L*L));P=Math.max(B,P)}_.r=P,P+=S}if(t.equidistant){for(var O=0,$=0,W=0;W=e.numIter||(ept(n,e),n.temperature=n.temperature*e.coolingFactor,n.temperature=e.animationThreshold&&a(),O5(d)}},"frame");d()}else{for(;h;)h=o(u),u++;Abe(n,e),l()}return this};cA.prototype.stop=function(){return this.stopped=!0,this.thread&&this.thread.stop(),this.emit("layoutstop"),this};cA.prototype.destroy=function(){return this.thread&&this.thread.stop(),this};Xft=s(function(t,r,n){for(var i=n.eles.edges(),a=n.eles.nodes(),o=qs(n.boundingBox?n.boundingBox:{x1:0,y1:0,w:t.width(),h:t.height()}),l={isCompound:t.hasCompoundNodes(),layoutNodes:[],idToIndex:{},nodeSize:a.size(),graphSet:[],indexToGraph:[],layoutEdges:[],edgeSize:i.size(),temperature:n.initialTemp,clientWidth:o.w,clientHeight:o.h,boundingBox:o},u=n.eles.components(),h={},d=0;d0){l.graphSet.push(R);for(var d=0;di.count?0:i.graph},"findLCA"),oTe=s(function(t,r,n,i){var a=i.graphSet[n];if(-10)var f=i.nodeOverlap*d,p=Math.sqrt(l*l+u*u),m=f*l/p,g=f*u/p;else var y=W5(t,l,u),v=W5(r,-1*l,-1*u),x=v.x-y.x,b=v.y-y.y,T=x*x+b*b,p=Math.sqrt(T),f=(t.nodeRepulsion+r.nodeRepulsion)/T,m=f*x/p,g=f*b/p;t.isLocked||(t.offsetX-=m,t.offsetY-=g),r.isLocked||(r.offsetX+=m,r.offsetY+=g)}},"nodeRepulsion"),npt=s(function(t,r,n,i){if(n>0)var a=t.maxX-r.minX;else var a=r.maxX-t.minX;if(i>0)var o=t.maxY-r.minY;else var o=r.maxY-t.minY;return a>=0&&o>=0?Math.sqrt(a*a+o*o):0},"nodesOverlap"),W5=s(function(t,r,n){var i=t.positionX,a=t.positionY,o=t.height||1,l=t.width||1,u=n/r,h=o/l,d={};return r===0&&0n?(d.x=i,d.y=a+o/2,d):0r&&-1*h<=u&&u<=h?(d.x=i-l/2,d.y=a-l*n/2/r,d):0=h)?(d.x=i+o*r/2/n,d.y=a+o/2,d):(0>n&&(u<=-1*h||u>=h)&&(d.x=i-o*r/2/n,d.y=a-o/2),d)},"findClippingPoint"),ipt=s(function(t,r){for(var n=0;nn){var v=r.gravity*m/y,x=r.gravity*g/y;p.offsetX+=v,p.offsetY+=x}}}}},"calculateGravityForces"),spt=s(function(t,r){var n=[],i=0,a=-1;for(n.push.apply(n,t.graphSet[0]),a+=t.graphSet[0].length;i<=a;){var o=n[i++],l=t.idToIndex[o],u=t.layoutNodes[l],h=u.children;if(0n)var a={x:n*t/i,y:n*r/i};else var a={x:t,y:r};return a},"limitForce"),cTe=s(function(t,r){var n=t.parentId;if(n!=null){var i=r.layoutNodes[r.idToIndex[n]],a=!1;if((i.maxX==null||t.maxX+i.padRight>i.maxX)&&(i.maxX=t.maxX+i.padRight,a=!0),(i.minX==null||t.minX-i.padLefti.maxY)&&(i.maxY=t.maxY+i.padBottom,a=!0),(i.minY==null||t.minY-i.padTopx&&(g+=v+r.componentSpacing,m=0,y=0,v=0)}}},"separateComponents"),cpt={fit:!0,padding:30,boundingBox:void 0,avoidOverlap:!0,avoidOverlapPadding:10,nodeDimensionsIncludeLabels:!1,spacingFactor:void 0,condense:!1,rows:void 0,cols:void 0,position:s(function(t){},"position"),sort:void 0,animate:!1,animationDuration:500,animationEasing:void 0,animateFilter:s(function(t,r){return!0},"animateFilter"),ready:void 0,stop:void 0,transform:s(function(t,r){return r},"transform")};s(uTe,"GridLayout");uTe.prototype.run=function(){var e=this.options,t=e,r=e.cy,n=t.eles,i=n.nodes().not(":parent");t.sort&&(i=i.sort(t.sort));var a=qs(t.boundingBox?t.boundingBox:{x1:0,y1:0,w:r.width(),h:r.height()});if(a.h===0||a.w===0)n.nodes().layoutPositions(this,t,function(H){return{x:a.x1,y:a.y1}});else{var o=i.size(),l=Math.sqrt(o*a.h/a.w),u=Math.round(l),h=Math.round(a.w/a.h*l),d=s(function(K){if(K==null)return Math.min(u,h);var J=Math.min(u,h);J==u?u=K:h=K},"small"),f=s(function(K){if(K==null)return Math.max(u,h);var J=Math.max(u,h);J==u?u=K:h=K},"large"),p=t.rows,m=t.cols!=null?t.cols:t.columns;if(p!=null&&m!=null)u=p,h=m;else if(p!=null&&m==null)u=p,h=Math.ceil(o/u);else if(p==null&&m!=null)h=m,u=Math.ceil(o/h);else if(h*u>o){var g=d(),y=f();(g-1)*y>=o?d(g-1):(y-1)*g>=o&&f(y-1)}else for(;h*u=o?f(x+1):d(v+1)}var b=a.w/h,T=a.h/u;if(t.condense&&(b=0,T=0),t.avoidOverlap)for(var w=0;w=h&&(D=0,I++)},"moveToNextCell"),B={},O=0;O(D=Zut(e,t,L[B],L[B+1],L[B+2],L[B+3])))return v(k,D),!0}else if(M.edgeType==="bezier"||M.edgeType==="multibezier"||M.edgeType==="self"||M.edgeType==="compound"){for(var L=M.allpts,B=0;B+5(D=Kut(e,t,L[B],L[B+1],L[B+2],L[B+3],L[B+4],L[B+5])))return v(k,D),!0}for(var O=O||R.source,$=$||R.target,W=i.getArrowWidth(P,N),V=[{name:"source",x:M.arrowStartX,y:M.arrowStartY,angle:M.srcArrowAngle},{name:"target",x:M.arrowEndX,y:M.arrowEndY,angle:M.tgtArrowAngle},{name:"mid-source",x:M.midX,y:M.midY,angle:M.midsrcArrowAngle},{name:"mid-target",x:M.midX,y:M.midY,angle:M.midtgtArrowAngle}],B=0;B0&&(x(O),x($))}s(b,"checkEdge");function T(k,R,M){return Ws(k,R,M)}s(T,"preprop");function w(k,R){var M=k._private,P=p,N;R?N=R+"-":N="",k.boundingBox();var _=M.labelBounds[R||"main"],A=k.pstyle(N+"label").value,I=k.pstyle("text-events").strValue==="yes";if(!(!I||!A)){var D=T(M.rscratch,"labelX",R),L=T(M.rscratch,"labelY",R),B=T(M.rscratch,"labelAngle",R),O=k.pstyle(N+"text-margin-x").pfValue,$=k.pstyle(N+"text-margin-y").pfValue,W=_.x1-P-O,V=_.x2+P-O,z=_.y1-P-$,H=_.y2+P-$;if(B){var K=Math.cos(B),J=Math.sin(B),ne=s(function(Ee,we){return Ee=Ee-D,we=we-L,{x:Ee*K-we*J+D,y:Ee*J+we*K+L}},"rotate"),Z=ne(W,z),pe=ne(W,H),te=ne(V,z),fe=ne(V,H),ce=[Z.x+O,Z.y+$,te.x+O,te.y+$,fe.x+O,fe.y+$,pe.x+O,pe.y+$];if(bo(e,t,ce))return v(k),!0}else if(Ff(_,e,t))return v(k),!0}}s(w,"checkLabel");for(var C=o.length-1;C>=0;C--){var S=o[C];S.isNode()?x(S)||w(S):b(S)||w(S)||w(S,"source")||w(S,"target")}return l};og.getAllInBox=function(e,t,r,n){var i=this.getCachedZSortedEles().interactive,a=this.cy.zoom(),o=2/a,l=[],u=Math.min(e,r),h=Math.max(e,r),d=Math.min(t,n),f=Math.max(t,n);e=u,r=h,t=d,n=f;var p=qs({x1:e,y1:t,x2:r,y2:n}),m=[{x:p.x1,y:p.y1},{x:p.x2,y:p.y1},{x:p.x2,y:p.y2},{x:p.x1,y:p.y2}],g=[[m[0],m[1]],[m[1],m[2]],[m[2],m[3]],[m[3],m[0]]];function y(Ee,we,Fe){return Ws(Ee,we,Fe)}s(y,"preprop");function v(Ee,we){var Fe=Ee._private,Ye=o,He="";Ee.boundingBox();var ye=Fe.labelBounds.main;if(!ye)return null;var Be=y(Fe.rscratch,"labelX",we),Ie=y(Fe.rscratch,"labelY",we),Ne=y(Fe.rscratch,"labelAngle",we),re=Ee.pstyle(He+"text-margin-x").pfValue,ue=Ee.pstyle(He+"text-margin-y").pfValue,le=ye.x1-Ye-re,he=ye.x2+Ye-re,Te=ye.y1-Ye-ue,oe=ye.y2+Ye-ue;if(Ne){var G=Math.cos(Ne),X=Math.sin(Ne),ee=s(function(ie,j){return ie=ie-Be,j=j-Ie,{x:ie*G-j*X+Be,y:ie*X+j*G+Ie}},"rotate");return[ee(le,Te),ee(he,Te),ee(he,oe),ee(le,oe)]}else return[{x:le,y:Te},{x:he,y:Te},{x:he,y:oe},{x:le,y:oe}]}s(v,"getRotatedLabelBox");function x(Ee,we,Fe,Ye){function He(ye,Be,Ie){return(Ie.y-ye.y)*(Be.x-ye.x)>(Be.y-ye.y)*(Ie.x-ye.x)}return s(He,"ccw"),He(Ee,Fe,Ye)!==He(we,Fe,Ye)&&He(Ee,we,Fe)!==He(Ee,we,Ye)}s(x,"doLinesIntersect");for(var b=0;b0?-(Math.PI-t.ang):Math.PI+t.ang},"invertVec"),mpt=s(function(t,r,n,i,a){if(t!==Ibe?Mbe(r,t,Tu):ppt(Al,Tu),Mbe(r,n,Al),Lbe=Tu.nx*Al.ny-Tu.ny*Al.nx,Dbe=Tu.nx*Al.nx-Tu.ny*-Al.ny,Ph=Math.asin(Math.max(-1,Math.min(1,Lbe))),Math.abs(Ph)<1e-6){pF=r.x,mF=r.y,Qm=w1=0;return}eg=1,D5=!1,Dbe<0?Ph<0?Ph=Math.PI+Ph:(Ph=Math.PI-Ph,eg=-1,D5=!0):Ph>0&&(eg=-1,D5=!0),r.radius!==void 0?w1=r.radius:w1=i,jm=Ph/2,b5=Math.min(Tu.len/2,Al.len/2),a?(xu=Math.abs(Math.cos(jm)*w1/Math.sin(jm)),xu>b5?(xu=b5,Qm=Math.abs(xu*Math.sin(jm)/Math.cos(jm))):Qm=w1):(xu=Math.min(b5,w1),Qm=Math.abs(xu*Math.sin(jm)/Math.cos(jm))),gF=r.x+Al.nx*xu,yF=r.y+Al.ny*xu,pF=gF-Al.ny*Qm*eg,mF=yF+Al.nx*Qm*eg,pTe=r.x+Tu.nx*xu,mTe=r.y+Tu.ny*xu,Ibe=r},"calcCornerArc");s(gTe,"drawPreparedRoundCorner");s(qF,"getRoundCorner");VT=.01,gpt=Math.sqrt(2*VT),Ss={};Ss.findMidptPtsEtc=function(e,t){var r=t.posPts,n=t.intersectionPts,i=t.vectorNormInverse,a,o=e.pstyle("source-endpoint"),l=e.pstyle("target-endpoint"),u=o.units!=null&&l.units!=null,h=s(function(C,S,k,R){var M=R-S,P=k-C,N=Math.sqrt(P*P+M*M);return{x:-M/N,y:P/N}},"recalcVectorNormInverse"),d=e.pstyle("edge-distances").value;switch(d){case"node-position":a=r;break;case"intersection":a=n;break;case"endpoints":{if(u){var f=this.manualEndptToPx(e.source()[0],o),p=aa(f,2),m=p[0],g=p[1],y=this.manualEndptToPx(e.target()[0],l),v=aa(y,2),x=v[0],b=v[1],T={x1:m,y1:g,x2:x,y2:b};i=h(m,g,x,b),a=T}else On("Edge ".concat(e.id()," has edge-distances:endpoints specified without manual endpoints specified via source-endpoint and target-endpoint. Falling back on edge-distances:intersection (default).")),a=n;break}}return{midptPts:a,vectorNormInverse:i}};Ss.findHaystackPoints=function(e){for(var t=0;t0?Math.max(j-U,0):Math.min(j+U,0)},"subDWH"),A=_(P,R),I=_(N,M),D=!1;b===h?x=Math.abs(A)>Math.abs(I)?i:n:b===u||b===l?(x=n,D=!0):(b===a||b===o)&&(x=i,D=!0);var L=x===n,B=L?I:A,O=L?N:P,$=DF(O),W=!1;!(D&&(w||S))&&(b===l&&O<0||b===u&&O>0||b===a&&O>0||b===o&&O<0)&&($*=-1,B=$*Math.abs(B),W=!0);var V;if(w){var z=C<0?1+C:C;V=z*B}else{var H=C<0?B:0;V=H+C*$}var K=s(function(j){return Math.abs(j)=Math.abs(B)},"getIsTooClose"),J=K(V),ne=K(Math.abs(B)-Math.abs(V)),Z=J||ne;if(Z&&!W)if(L){var pe=Math.abs(O)<=p/2,te=Math.abs(P)<=m/2;if(pe){var fe=(d.x1+d.x2)/2,ce=d.y1,de=d.y2;r.segpts=[fe,ce,fe,de]}else if(te){var Ee=(d.y1+d.y2)/2,we=d.x1,Fe=d.x2;r.segpts=[we,Ee,Fe,Ee]}else r.segpts=[d.x1,d.y2]}else{var Ye=Math.abs(O)<=f/2,He=Math.abs(N)<=g/2;if(Ye){var ye=(d.y1+d.y2)/2,Be=d.x1,Ie=d.x2;r.segpts=[Be,ye,Ie,ye]}else if(He){var Ne=(d.x1+d.x2)/2,re=d.y1,ue=d.y2;r.segpts=[Ne,re,Ne,ue]}else r.segpts=[d.x2,d.y1]}else if(L){var le=d.y1+V+(v?p/2*$:0),he=d.x1,Te=d.x2;r.segpts=[he,le,Te,le]}else{var oe=d.x1+V+(v?f/2*$:0),G=d.y1,X=d.y2;r.segpts=[oe,G,oe,X]}if(r.isRound){var ee=e.pstyle("taxi-radius").value,q=e.pstyle("radius-type").value[0]==="arc-radius";r.radii=new Array(r.segpts.length/2).fill(ee),r.isArcRadius=new Array(r.segpts.length/2).fill(q)}};Ss.tryToCorrectInvalidPoints=function(e,t){var r=e._private.rscratch;if(r.edgeType==="bezier"){var n=t.srcPos,i=t.tgtPos,a=t.srcW,o=t.srcH,l=t.tgtW,u=t.tgtH,h=t.srcShape,d=t.tgtShape,f=t.srcCornerRadius,p=t.tgtCornerRadius,m=t.srcRs,g=t.tgtRs,y=!Vt(r.startX)||!Vt(r.startY),v=!Vt(r.arrowStartX)||!Vt(r.arrowStartY),x=!Vt(r.endX)||!Vt(r.endY),b=!Vt(r.arrowEndX)||!Vt(r.arrowEndY),T=3,w=this.getArrowWidth(e.pstyle("width").pfValue,e.pstyle("arrow-scale").value)*this.arrowShapeWidth,C=T*w,S=ng({x:r.ctrlpts[0],y:r.ctrlpts[1]},{x:r.startX,y:r.startY}),k=SO.poolIndex()){var $=B;B=O,O=$}var W=A.srcPos=B.position(),V=A.tgtPos=O.position(),z=A.srcW=B.outerWidth(),H=A.srcH=B.outerHeight(),K=A.tgtW=O.outerWidth(),J=A.tgtH=O.outerHeight(),ne=A.srcShape=r.nodeShapes[t.getNodeShape(B)],Z=A.tgtShape=r.nodeShapes[t.getNodeShape(O)],pe=A.srcCornerRadius=B.pstyle("corner-radius").value==="auto"?"auto":B.pstyle("corner-radius").pfValue,te=A.tgtCornerRadius=O.pstyle("corner-radius").value==="auto"?"auto":O.pstyle("corner-radius").pfValue,fe=A.tgtRs=O._private.rscratch,ce=A.srcRs=B._private.rscratch;A.dirCounts={north:0,west:0,south:0,east:0,northwest:0,southwest:0,northeast:0,southeast:0};for(var de=0;de=gpt||(Te=Math.sqrt(Math.max(he*he,VT)+Math.max(le*le,VT)));var oe=A.vector={x:he,y:le},G=A.vectorNorm={x:oe.x/Te,y:oe.y/Te},X={x:-G.y,y:G.x};A.nodesOverlap=!Vt(Te)||Z.checkPoint(ye[0],ye[1],0,K,J,V.x,V.y,te,fe)||ne.checkPoint(Ie[0],Ie[1],0,z,H,W.x,W.y,pe,ce),A.vectorNormInverse=X,I={nodesOverlap:A.nodesOverlap,dirCounts:A.dirCounts,calculatedIntersection:!0,hasBezier:A.hasBezier,hasUnbundled:A.hasUnbundled,eles:A.eles,srcPos:V,srcRs:fe,tgtPos:W,tgtRs:ce,srcW:K,srcH:J,tgtW:z,tgtH:H,srcIntn:Ne,tgtIntn:Be,srcShape:Z,tgtShape:ne,posPts:{x1:ue.x2,y1:ue.y2,x2:ue.x1,y2:ue.y1},intersectionPts:{x1:re.x2,y1:re.y2,x2:re.x1,y2:re.y1},vector:{x:-oe.x,y:-oe.y},vectorNorm:{x:-G.x,y:-G.y},vectorNormInverse:{x:-X.x,y:-X.y}}}var ee=He?I:A;we.nodesOverlap=ee.nodesOverlap,we.srcIntn=ee.srcIntn,we.tgtIntn=ee.tgtIntn,we.isRound=Fe.startsWith("round"),i&&(B.isParent()||B.isChild()||O.isParent()||O.isChild())&&(B.parents().anySame(O)||O.parents().anySame(B)||B.same(O)&&B.isParent())?t.findCompoundLoopPoints(Ee,ee,de,Ye):B===O?t.findLoopPoints(Ee,ee,de,Ye):Fe.endsWith("segments")?t.findSegmentsPoints(Ee,ee):Fe.endsWith("taxi")?t.findTaxiPoints(Ee,ee):Fe==="straight"||!Ye&&A.eles.length%2===1&&de===Math.floor(A.eles.length/2)?t.findStraightEdgePoints(Ee):t.findBezierPoints(Ee,ee,de,Ye,He),t.findEndpoints(Ee),t.tryToCorrectInvalidPoints(Ee,ee),t.checkForInvalidEdgeWarning(Ee),t.storeAllpts(Ee),t.storeEdgeProjections(Ee),t.calculateArrowAngles(Ee),t.recalculateEdgeLabelProjections(Ee),t.calculateLabelAngles(Ee)}},"_loop"),k=0;k0){var ye=h,Be=Zm(ye,R1(o)),Ie=Zm(ye,R1(He)),Ne=Be;if(Ie2){var re=Zm(ye,{x:He[2],y:He[3]});re0){var Y=d,Ae=Zm(Y,R1(o)),me=Zm(Y,R1(U)),Se=Ae;if(me2){var Ce=Zm(Y,{x:U[2],y:U[3]});Ce=g||k){v={cp:w,segment:S};break}}if(v)break}var R=v.cp,M=v.segment,P=(g-x)/M.length,N=M.t1-M.t0,_=m?M.t0+N*P:M.t1-N*P;_=OT(0,_,1),t=D1(R.p0,R.p1,R.p2,_),p=vpt(R.p0,R.p1,R.p2,_);break}case"straight":case"segments":case"haystack":{for(var A=0,I,D,L,B,O=n.allpts.length,$=0;$+3=g));$+=2);var W=g-D,V=W/I;V=OT(0,V,1),t=Fut(L,B,V),p=xTe(L,B);break}}o("labelX",f,t.x),o("labelY",f,t.y),o("labelAutoAngle",f,p)}},"calculateEndProjection");h("source"),h("target"),this.applyLabelDimensions(e)}};ku.applyLabelDimensions=function(e){this.applyPrefixedLabelDimensions(e),e.isEdge()&&(this.applyPrefixedLabelDimensions(e,"source"),this.applyPrefixedLabelDimensions(e,"target"))};ku.applyPrefixedLabelDimensions=function(e,t){var r=e._private,n=this.getLabelText(e,t),i=rg(n,e._private.labelDimsKey);if(Ws(r.rscratch,"prefixedLabelDimsKey",t)!==i){Cu(r.rscratch,"prefixedLabelDimsKey",t,i);var a=this.calculateLabelDimensions(e,n),o=e.pstyle("line-height").pfValue,l=e.pstyle("font-size").pfValue,u=e.pstyle("text-wrap").strValue,h=Ws(r.rscratch,"labelWrapCachedLines",t)||[],d=u!=="wrap"?1:Math.max(h.length,1),f=l*o,p=a.width,m=a.height+(d-1)*(o-1)*l;Cu(r.rstyle,"labelWidth",t,p),Cu(r.rscratch,"labelWidth",t,p),Cu(r.rstyle,"labelHeight",t,m),Cu(r.rscratch,"labelHeight",t,m),Cu(r.rscratch,"labelLineHeight",t,f),Cu(r.rscratch,"labelActualDescent",t,a.labelActualDescent)}};ku.getLabelText=function(e,t){var r=e._private,n=t?t+"-":"",i=e.pstyle(n+"label").strValue,a=e.pstyle("text-transform").value,o=s(function(H,K){return K?(Cu(r.rscratch,H,t,K),K):Ws(r.rscratch,H,t)},"rscratch");if(!i)return"";a=="none"||(a=="uppercase"?i=i.toUpperCase():a=="lowercase"&&(i=i.toLowerCase()));var l=e.pstyle("text-wrap").value;if(l==="wrap"){var u=o("labelKey");if(u!=null&&o("labelWrapKey")===u)return o("labelWrapCachedText");for(var h="\u200B",d=i.split(` +`),f=e.pstyle("text-max-width").pfValue,p=e.pstyle("text-overflow-wrap").value,m=p==="anywhere",g=[],y=/[\s\u200b]+|$/g,v=0;vf){var C=x.matchAll(y),S="",k=0,R=To(C),M;try{for(R.s();!(M=R.n()).done;){var P=M.value,N=P[0],_=x.substring(k,P.index);k=P.index+N.length;var A=S.length===0?_:S+_+N,I=this.calculateLabelDimensions(e,A),D=I.width;D<=f?S+=_+N:(S&&g.push(S),S=_+N)}}catch(z){R.e(z)}finally{R.f()}S.match(/^[\s\u200b]+$/)||g.push(S)}else g.push(x)}o("labelWrapCachedLines",g),i=o("labelWrapCachedText",g.join(` +`)),o("labelWrapKey",u)}else if(l==="ellipsis"){var L=e.pstyle("text-max-width").pfValue,B="",O="\u2026",$=!1;if(this.calculateLabelDimensions(e,i).widthL)break;B+=i[W],W===i.length-1&&($=!0)}return $||(B+=O),B}return i};ku.getLabelJustification=function(e){var t=e.pstyle("text-justification").strValue,r=e.pstyle("text-halign").strValue;return t==="auto"?e.isNode()?aft(r):"center":t};ku.calculateLabelDimensions=function(e,t){var r=this,n=r.cy.window(),i=n.document,a=0,o=e.pstyle("font-style").strValue,l=e.pstyle("font-size").pfValue,u=e.pstyle("font-family").strValue,h=e.pstyle("font-weight").strValue,d=e.pstyle("text-metrics").strValue||"font",f=this.labelCalcCanvas,p=this.labelCalcCanvasContext;if(!f){f=this.labelCalcCanvas=i.createElement("canvas"),p=this.labelCalcCanvasContext=f.getContext("2d");var m=f.style;m.position="absolute",m.left="-9999px",m.top="-9999px",m.zIndex="-1",m.visibility="hidden",m.pointerEvents="none"}p.font="".concat(o," ").concat(h," ").concat(l,"px ").concat(u);for(var g=0,y=0,v=t.split(` +`),x=v.length,b=0,T=0,w=0;w1&&arguments[1]!==void 0?arguments[1]:!0;if(t.merge(o),l)for(var u=0;u=e.desktopTapThreshold2}var Rt=a(q);lt&&(e.hoverData.tapholdCancelled=!0);var gt=s(function(){var Yt=e.hoverData.dragDelta=e.hoverData.dragDelta||[];Yt.length===0?(Yt.push(qe[0]),Yt.push(qe[1])):(Yt[0]+=qe[0],Yt[1]+=qe[1])},"updateDragDelta");j=!0,i(_e,["mousemove","vmousemove","tapdrag"],q,{x:me[0],y:me[1]});var be=s(function(Yt){return{originalEvent:q,type:Yt,position:{x:me[0],y:me[1]}}},"makeEvent"),ct=s(function(){e.data.bgActivePosistion=void 0,e.hoverData.selecting||U.emit(be("boxstart")),Me[4]=1,e.hoverData.selecting=!0,e.redrawHint("select",!0),e.redraw()},"goIntoBoxMode");if(e.hoverData.which===3){if(lt){var xt=be("cxtdrag");Le?Le.emit(xt):U.emit(xt),e.hoverData.cxtDragged=!0,(!e.hoverData.cxtOver||_e!==e.hoverData.cxtOver)&&(e.hoverData.cxtOver&&e.hoverData.cxtOver.emit(be("cxtdragout")),e.hoverData.cxtOver=_e,_e&&_e.emit(be("cxtdragover")))}}else if(e.hoverData.dragging){if(j=!0,U.panningEnabled()&&U.userPanningEnabled()){var Ke;if(e.hoverData.justStartedPan){var De=e.hoverData.mdownPos;Ke={x:(me[0]-De[0])*Y,y:(me[1]-De[1])*Y},e.hoverData.justStartedPan=!1}else Ke={x:qe[0]*Y,y:qe[1]*Y};U.panBy(Ke),U.emit(be("dragpan")),e.hoverData.dragged=!0}me=e.projectIntoViewport(q.clientX,q.clientY)}else if(Me[4]==1&&(Le==null||Le.pannable())){if(lt){if(!e.hoverData.dragging&&U.boxSelectionEnabled()&&(Rt||!U.panningEnabled()||!U.userPanningEnabled()))ct();else if(!e.hoverData.selecting&&U.panningEnabled()&&U.userPanningEnabled()){var tt=o(Le,e.hoverData.downs);tt&&(e.hoverData.dragging=!0,e.hoverData.justStartedPan=!0,Me[4]=0,e.data.bgActivePosistion=R1(Se),e.redrawHint("select",!0),e.redraw())}Le&&Le.pannable()&&Le.active()&&Le.unactivate()}}else{if(Le&&Le.pannable()&&Le.active()&&Le.unactivate(),(!Le||!Le.grabbed())&&_e!=ze&&(ze&&i(ze,["mouseout","tapdragout"],q,{x:me[0],y:me[1]}),_e&&i(_e,["mouseover","tapdragover"],q,{x:me[0],y:me[1]}),e.hoverData.last=_e),Le)if(lt){if(U.boxSelectionEnabled()&&Rt)Le&&Le.grabbed()&&(y(ve),Le.emit(be("freeon")),ve.emit(be("free")),e.dragData.didDrag&&(Le.emit(be("dragfreeon")),ve.emit(be("dragfree")))),ct();else if(Le&&Le.grabbed()&&e.nodeIsDraggable(Le)){var Ze=!e.dragData.didDrag;Ze&&e.redrawHint("eles",!0),e.dragData.didDrag=!0,e.hoverData.draggingEles||m(ve,{inDragLayer:!0});var dt={x:0,y:0};if(Vt(qe[0])&&Vt(qe[1])&&(dt.x+=qe[0],dt.y+=qe[1],Ze)){var Ut=e.hoverData.dragDelta;Ut&&Vt(Ut[0])&&Vt(Ut[1])&&(dt.x+=Ut[0],dt.y+=Ut[1])}e.hoverData.draggingEles=!0,ve.silentShift(dt).emit(be("position")).emit(be("drag")),e.redrawHint("drag",!0),e.redraw()}}else gt();j=!0}if(Me[2]=me[0],Me[3]=me[1],j)return q.stopPropagation&&q.stopPropagation(),q.preventDefault&&q.preventDefault(),!1}},"mousemoveHandler"),!1);var P,N,_;e.registerBinding(t,"mouseup",s(function(q){if(!(e.hoverData.which===1&&q.which!==1&&e.hoverData.capture)){var ie=e.hoverData.capture;if(ie){e.hoverData.capture=!1;var j=e.cy,U=e.projectIntoViewport(q.clientX,q.clientY),Y=e.selection,Ae=e.findNearestElement(U[0],U[1],!0,!1),me=e.dragData.possibleDragElements,Se=e.hoverData.down,Ce=a(q);e.data.bgActivePosistion&&(e.redrawHint("select",!0),e.redraw()),e.hoverData.tapholdCancelled=!0,e.data.bgActivePosistion=void 0,Se&&Se.unactivate();var Me=s(function(Oe){return{originalEvent:q,type:Oe,position:{x:U[0],y:U[1]}}},"makeEvent");if(e.hoverData.which===3){var _e=Me("cxttapend");if(Se?Se.emit(_e):j.emit(_e),!e.hoverData.cxtDragged){var ze=Me("cxttap");Se?Se.emit(ze):j.emit(ze)}e.hoverData.cxtDragged=!1,e.hoverData.which=null}else if(e.hoverData.which===1){if(i(Ae,["mouseup","tapend","vmouseup"],q,{x:U[0],y:U[1]}),!e.dragData.didDrag&&!e.hoverData.dragged&&!e.hoverData.selecting&&!e.hoverData.isOverThresholdDrag&&(i(Se,["click","tap","vclick"],q,{x:U[0],y:U[1]}),N=!1,q.timeStamp-_<=j.multiClickDebounceTime()?(P&&clearTimeout(P),N=!0,_=null,i(Se,["dblclick","dbltap","vdblclick"],q,{x:U[0],y:U[1]})):(P=setTimeout(function(){N||i(Se,["oneclick","onetap","voneclick"],q,{x:U[0],y:U[1]})},j.multiClickDebounceTime()),_=q.timeStamp)),Se==null&&!e.dragData.didDrag&&!e.hoverData.selecting&&!e.hoverData.dragged&&!a(q)&&(j.$(r).unselect(["tapunselect"]),me.length>0&&e.redrawHint("eles",!0),e.dragData.possibleDragElements=me=j.collection()),Ae==Se&&!e.dragData.didDrag&&!e.hoverData.selecting&&Ae!=null&&Ae._private.selectable&&(e.hoverData.dragging||(j.selectionType()==="additive"||Ce?Ae.selected()?Ae.unselect(["tapunselect"]):Ae.select(["tapselect"]):Ce||(j.$(r).unmerge(Ae).unselect(["tapunselect"]),Ae.select(["tapselect"]))),e.redrawHint("eles",!0)),e.hoverData.selecting){var Le=j.collection(e.getAllInBox(Y[0],Y[1],Y[2],Y[3]));e.redrawHint("select",!0),Le.length>0&&e.redrawHint("eles",!0),j.emit(Me("boxend"));var qe=s(function(Oe){return Oe.selectable()&&!Oe.selected()},"eleWouldBeSelected");j.selectionType()==="additive"||Ce||j.$(r).unmerge(Le).unselect(),Le.emit(Me("box")).stdFilter(qe).select().emit(Me("boxselect")),e.redraw()}if(e.hoverData.dragging&&(e.hoverData.dragging=!1,e.redrawHint("select",!0),e.redrawHint("eles",!0),e.redraw()),!Y[4]){e.redrawHint("drag",!0),e.redrawHint("eles",!0);var ve=Se&&Se.grabbed();y(me),ve&&(Se.emit(Me("freeon")),me.emit(Me("free")),e.dragData.didDrag&&(Se.emit(Me("dragfreeon")),me.emit(Me("dragfree"))))}}Y[4]=0,e.hoverData.down=null,e.hoverData.cxtStarted=!1,e.hoverData.draggingEles=!1,e.hoverData.selecting=!1,e.hoverData.isOverThresholdDrag=!1,e.dragData.didDrag=!1,e.hoverData.dragged=!1,e.hoverData.dragDelta=[],e.hoverData.mdownPos=null,e.hoverData.mdownGPos=null,e.hoverData.which=null}}},"mouseupHandler"),!1);var A=[],I=4,D,L=1e5,B=s(function(q,ie){for(var j=0;j=I){var U=A;if(D=B(U,5),!D){var Y=Math.abs(U[0]);D=O(U)&&Y>5}if(D)for(var Ae=0;Ae5&&(j=DF(j)*5),ze=j/-250,D&&(ze/=L,ze*=3),ze=ze*e.wheelSensitivity;var Le=q.deltaMode===1;Le&&(ze*=33);var qe=me.zoom()*Math.pow(10,ze);q.type==="gesturechange"&&(qe=e.gestureStartZoom*q.scale),me.zoom({level:qe,renderedPosition:{x:_e[0],y:_e[1]}}),me.emit({type:q.type==="gesturechange"?"pinchzoom":"scrollzoom",originalEvent:q,position:{x:Me[0],y:Me[1]}})}}}},"wheelHandler");e.registerBinding(e.container,"wheel",$,!0),e.registerBinding(t,"scroll",s(function(q){e.scrollingPage=!0,clearTimeout(e.scrollingPageTimeout),e.scrollingPageTimeout=setTimeout(function(){e.scrollingPage=!1},250)},"scrollHandler"),!0),e.registerBinding(e.container,"gesturestart",s(function(q){e.gestureStartZoom=e.cy.zoom(),e.hasTouchStarted||q.preventDefault()},"gestureStartHandler"),!0),e.registerBinding(e.container,"gesturechange",function(ee){e.hasTouchStarted||$(ee)},!0),e.registerBinding(e.container,"mouseout",s(function(q){var ie=e.projectIntoViewport(q.clientX,q.clientY);e.cy.emit({originalEvent:q,type:"mouseout",position:{x:ie[0],y:ie[1]}})},"mouseOutHandler"),!1),e.registerBinding(e.container,"mouseover",s(function(q){var ie=e.projectIntoViewport(q.clientX,q.clientY);e.cy.emit({originalEvent:q,type:"mouseover",position:{x:ie[0],y:ie[1]}})},"mouseOverHandler"),!1);var W,V,z,H,K,J,ne,Z,pe,te,fe,ce,de,Ee=s(function(q,ie,j,U){return Math.sqrt((j-q)*(j-q)+(U-ie)*(U-ie))},"distance"),we=s(function(q,ie,j,U){return(j-q)*(j-q)+(U-ie)*(U-ie)},"distanceSq"),Fe;e.registerBinding(e.container,"touchstart",Fe=s(function(q){if(e.hasTouchStarted=!0,!!R(q)){x(),e.touchData.capture=!0,e.data.bgActivePosistion=void 0;var ie=e.cy,j=e.touchData.now,U=e.touchData.earlier;if(q.touches[0]){var Y=e.projectIntoViewport(q.touches[0].clientX,q.touches[0].clientY);j[0]=Y[0],j[1]=Y[1]}if(q.touches[1]){var Y=e.projectIntoViewport(q.touches[1].clientX,q.touches[1].clientY);j[2]=Y[0],j[3]=Y[1]}if(q.touches[2]){var Y=e.projectIntoViewport(q.touches[2].clientX,q.touches[2].clientY);j[4]=Y[0],j[5]=Y[1]}var Ae=s(function(Rt){return{originalEvent:q,type:Rt,position:{x:j[0],y:j[1]}}},"makeEvent");if(q.touches[1]){e.touchData.singleTouchMoved=!0,y(e.dragData.touchDragEles);var me=e.findContainerClientCoords();pe=me[0],te=me[1],fe=me[2],ce=me[3],W=q.touches[0].clientX-pe,V=q.touches[0].clientY-te,z=q.touches[1].clientX-pe,H=q.touches[1].clientY-te,de=0<=W&&W<=fe&&0<=z&&z<=fe&&0<=V&&V<=ce&&0<=H&&H<=ce;var Se=ie.pan(),Ce=ie.zoom();K=Ee(W,V,z,H),J=we(W,V,z,H),ne=[(W+z)/2,(V+H)/2],Z=[(ne[0]-Se.x)/Ce,(ne[1]-Se.y)/Ce];var Me=200,_e=Me*Me;if(J<_e&&!q.touches[2]){var ze=e.findNearestElement(j[0],j[1],!0,!0),Le=e.findNearestElement(j[2],j[3],!0,!0);ze&&ze.isNode()?(ze.activate().emit(Ae("cxttapstart")),e.touchData.start=ze):Le&&Le.isNode()?(Le.activate().emit(Ae("cxttapstart")),e.touchData.start=Le):ie.emit(Ae("cxttapstart")),e.touchData.start&&(e.touchData.start._private.grabbed=!1),e.touchData.cxt=!0,e.touchData.cxtDragged=!1,e.data.bgActivePosistion=void 0,e.redraw();return}}if(q.touches[2])ie.boxSelectionEnabled()&&q.preventDefault();else if(!q.touches[1]){if(q.touches[0]){var qe=e.findNearestElements(j[0],j[1],!0,!0),ve=qe[0];if(ve!=null&&(ve.activate(),e.touchData.start=ve,e.touchData.starts=qe,e.nodeIsGrabbable(ve))){var lt=e.dragData.touchDragEles=ie.collection(),Oe=null;e.redrawHint("eles",!0),e.redrawHint("drag",!0),ve.selected()?(Oe=ie.$(function(Ct){return Ct.selected()&&e.nodeIsGrabbable(Ct)}),m(Oe,{addToList:lt})):g(ve,{addToList:lt}),h(ve),ve.emit(Ae("grabon")),Oe?Oe.forEach(function(Ct){Ct.emit(Ae("grab"))}):ve.emit(Ae("grab"))}i(ve,["touchstart","tapstart","vmousedown"],q,{x:j[0],y:j[1]}),ve==null&&(e.data.bgActivePosistion={x:Y[0],y:Y[1]},e.redrawHint("select",!0),e.redraw()),e.touchData.singleTouchMoved=!1,e.touchData.singleTouchStartTime=+new Date,clearTimeout(e.touchData.tapholdTimeout),e.touchData.tapholdTimeout=setTimeout(function(){e.touchData.singleTouchMoved===!1&&!e.pinching&&!e.touchData.selecting&&i(e.touchData.start,["taphold"],q,{x:j[0],y:j[1]})},e.tapholdDuration)}}if(q.touches.length>=1){for(var mt=e.touchData.startPosition=[null,null,null,null,null,null],At=0;At=e.touchTapThreshold2}if(ie&&e.touchData.cxt){q.preventDefault();var At=q.touches[0].clientX-pe,Gt=q.touches[0].clientY-te,Ct=q.touches[1].clientX-pe,Rt=q.touches[1].clientY-te,gt=we(At,Gt,Ct,Rt),be=gt/J,ct=150,xt=ct*ct,Ke=1.5,De=Ke*Ke;if(be>=De||gt>=xt){e.touchData.cxt=!1,e.data.bgActivePosistion=void 0,e.redrawHint("select",!0);var tt=Ce("cxttapend");e.touchData.start?(e.touchData.start.unactivate().emit(tt),e.touchData.start=null):U.emit(tt)}}if(ie&&e.touchData.cxt){var tt=Ce("cxtdrag");e.data.bgActivePosistion=void 0,e.redrawHint("select",!0),e.touchData.start?e.touchData.start.emit(tt):U.emit(tt),e.touchData.start&&(e.touchData.start._private.grabbed=!1),e.touchData.cxtDragged=!0;var Ze=e.findNearestElement(Y[0],Y[1],!0,!0);(!e.touchData.cxtOver||Ze!==e.touchData.cxtOver)&&(e.touchData.cxtOver&&e.touchData.cxtOver.emit(Ce("cxtdragout")),e.touchData.cxtOver=Ze,Ze&&Ze.emit(Ce("cxtdragover")))}else if(ie&&q.touches[2]&&U.boxSelectionEnabled())q.preventDefault(),e.data.bgActivePosistion=void 0,this.lastThreeTouch=+new Date,e.touchData.selecting||U.emit(Ce("boxstart")),e.touchData.selecting=!0,e.touchData.didSelect=!0,j[4]=1,!j||j.length===0||j[0]===void 0?(j[0]=(Y[0]+Y[2]+Y[4])/3,j[1]=(Y[1]+Y[3]+Y[5])/3,j[2]=(Y[0]+Y[2]+Y[4])/3+1,j[3]=(Y[1]+Y[3]+Y[5])/3+1):(j[2]=(Y[0]+Y[2]+Y[4])/3,j[3]=(Y[1]+Y[3]+Y[5])/3),e.redrawHint("select",!0),e.redraw();else if(ie&&q.touches[1]&&!e.touchData.didSelect&&U.zoomingEnabled()&&U.panningEnabled()&&U.userZoomingEnabled()&&U.userPanningEnabled()){q.preventDefault(),e.data.bgActivePosistion=void 0,e.redrawHint("select",!0);var dt=e.dragData.touchDragEles;if(dt){e.redrawHint("drag",!0);for(var Ut=0;Ut0&&!e.hoverData.draggingEles&&!e.swipePanning&&e.data.bgActivePosistion!=null&&(e.data.bgActivePosistion=void 0,e.redrawHint("select",!0),e.redraw())}},"touchmoveHandler"),!1);var He;e.registerBinding(t,"touchcancel",He=s(function(q){var ie=e.touchData.start;e.touchData.capture=!1,ie&&ie.unactivate()},"touchcancelHandler"));var ye,Be,Ie,Ne;if(e.registerBinding(t,"touchend",ye=s(function(q){var ie=e.touchData.start,j=e.touchData.capture;if(j)q.touches.length===0&&(e.touchData.capture=!1),q.preventDefault();else return;var U=e.selection;e.swipePanning=!1,e.hoverData.draggingEles=!1;var Y=e.cy,Ae=Y.zoom(),me=e.touchData.now,Se=e.touchData.earlier;if(q.touches[0]){var Ce=e.projectIntoViewport(q.touches[0].clientX,q.touches[0].clientY);me[0]=Ce[0],me[1]=Ce[1]}if(q.touches[1]){var Ce=e.projectIntoViewport(q.touches[1].clientX,q.touches[1].clientY);me[2]=Ce[0],me[3]=Ce[1]}if(q.touches[2]){var Ce=e.projectIntoViewport(q.touches[2].clientX,q.touches[2].clientY);me[4]=Ce[0],me[5]=Ce[1]}var Me=s(function(xt){return{originalEvent:q,type:xt,position:{x:me[0],y:me[1]}}},"makeEvent");ie&&ie.unactivate();var _e;if(e.touchData.cxt){if(_e=Me("cxttapend"),ie?ie.emit(_e):Y.emit(_e),!e.touchData.cxtDragged){var ze=Me("cxttap");ie?ie.emit(ze):Y.emit(ze)}e.touchData.start&&(e.touchData.start._private.grabbed=!1),e.touchData.cxt=!1,e.touchData.start=null,e.redraw();return}if(!q.touches[2]&&Y.boxSelectionEnabled()&&e.touchData.selecting){e.touchData.selecting=!1;var Le=Y.collection(e.getAllInBox(U[0],U[1],U[2],U[3]));U[0]=void 0,U[1]=void 0,U[2]=void 0,U[3]=void 0,U[4]=0,e.redrawHint("select",!0),Y.emit(Me("boxend"));var qe=s(function(xt){return xt.selectable()&&!xt.selected()},"eleWouldBeSelected");Le.emit(Me("box")).stdFilter(qe).select().emit(Me("boxselect")),Le.nonempty()&&e.redrawHint("eles",!0),e.redraw()}if(ie?.unactivate(),q.touches[2])e.data.bgActivePosistion=void 0,e.redrawHint("select",!0);else if(!q.touches[1]){if(!q.touches[0]){if(!q.touches[0]){e.data.bgActivePosistion=void 0,e.redrawHint("select",!0);var ve=e.dragData.touchDragEles;if(ie!=null){var lt=ie._private.grabbed;y(ve),e.redrawHint("drag",!0),e.redrawHint("eles",!0),lt&&(ie.emit(Me("freeon")),ve.emit(Me("free")),e.dragData.didDrag&&(ie.emit(Me("dragfreeon")),ve.emit(Me("dragfree")))),i(ie,["touchend","tapend","vmouseup","tapdragout"],q,{x:me[0],y:me[1]}),ie.unactivate(),e.touchData.start=null}else{var Oe=e.findNearestElement(me[0],me[1],!0,!0);i(Oe,["touchend","tapend","vmouseup","tapdragout"],q,{x:me[0],y:me[1]})}var mt=e.touchData.startPosition[0]-me[0],At=mt*mt,Gt=e.touchData.startPosition[1]-me[1],Ct=Gt*Gt,Rt=At+Ct,gt=Rt*Ae*Ae;e.touchData.singleTouchMoved||(ie||Y.$(":selected").unselect(["tapunselect"]),i(ie,["tap","vclick"],q,{x:me[0],y:me[1]}),Be=!1,q.timeStamp-Ne<=Y.multiClickDebounceTime()?(Ie&&clearTimeout(Ie),Be=!0,Ne=null,i(ie,["dbltap","vdblclick"],q,{x:me[0],y:me[1]})):(Ie=setTimeout(function(){Be||i(ie,["onetap","voneclick"],q,{x:me[0],y:me[1]})},Y.multiClickDebounceTime()),Ne=q.timeStamp)),ie!=null&&!e.dragData.didDrag&&ie._private.selectable&>"u"){var re=[],ue=s(function(q){return{clientX:q.clientX,clientY:q.clientY,force:1,identifier:q.pointerId,pageX:q.pageX,pageY:q.pageY,radiusX:q.width/2,radiusY:q.height/2,screenX:q.screenX,screenY:q.screenY,target:q.target}},"makeTouch"),le=s(function(q){return{event:q,touch:ue(q)}},"makePointer"),he=s(function(q){re.push(le(q))},"addPointer"),Te=s(function(q){for(var ie=0;ie0)return z[0]}return null},"getCurveT"),g=Object.keys(p),y=0;y0?m:T2e(a,o,t,r,n,i,l,u)},"intersectLine"),checkPoint:s(function(t,r,n,i,a,o,l,u){u=u==="auto"?Hf(i,a):u;var h=2*u;if($h(t,r,this.points,o,l,i,a-h,[0,-1],n)||$h(t,r,this.points,o,l,i-h,a,[0,-1],n))return!0;var d=i/2+2*n,f=a/2+2*n,p=[o-d,l-f,o-d,l,o+d,l,o+d,l-f];return!!(bo(t,r,p)||tg(t,r,h,h,o+i/2-u,l+a/2-u,n)||tg(t,r,h,h,o-i/2+u,l+a/2-u,n))},"checkPoint")}};Fh.registerNodeShapes=function(){var e=this.nodeShapes={},t=this;this.generateEllipse(),this.generatePolygon("triangle",Vs(3,0)),this.generateRoundPolygon("round-triangle",Vs(3,0)),this.generatePolygon("rectangle",Vs(4,0)),e.square=e.rectangle,this.generateRoundRectangle(),this.generateCutRectangle(),this.generateBarrel(),this.generateBottomRoundrectangle();{var r=[0,1,1,0,0,-1,-1,0];this.generatePolygon("diamond",r),this.generateRoundPolygon("round-diamond",r)}this.generatePolygon("pentagon",Vs(5,0)),this.generateRoundPolygon("round-pentagon",Vs(5,0)),this.generatePolygon("hexagon",Vs(6,0)),this.generateRoundPolygon("round-hexagon",Vs(6,0)),this.generatePolygon("heptagon",Vs(7,0)),this.generateRoundPolygon("round-heptagon",Vs(7,0)),this.generatePolygon("octagon",Vs(8,0)),this.generateRoundPolygon("round-octagon",Vs(8,0));var n=new Array(20);{var i=iF(5,0),a=iF(5,Math.PI/5),o=.5*(3-Math.sqrt(5));o*=1.57;for(var l=0;l=t.deqFastCost*w)break}else if(h){if(b>=t.deqCost*m||b>=t.deqAvgCost*p)break}else if(T>=t.deqNoDrawCost*X$)break;var C=t.deq(n,v,y);if(C.length>0)for(var S=0;S0&&(t.onDeqd(n,g),!h&&t.shouldRedraw(n,g,v,y)&&a())},"dequeue"),l=t.priority||RF;i.beforeRender(o,l(n))}},"setupDequeueingImpl")},"setupDequeueing")},Cpt=(function(){function e(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:B5;Xf(this,e),this.idsByKey=new Oh,this.keyForId=new Oh,this.cachesByLvl=new Oh,this.lvls=[],this.getKey=t,this.doesEleInvalidateKey=r}return s(e,"ElementTextureCacheLookup"),Kf(e,[{key:"getIdsFor",value:s(function(r){r==null&&xi("Can not get id list for null key");var n=this.idsByKey,i=this.idsByKey.get(r);return i||(i=new G1,n.set(r,i)),i},"getIdsFor")},{key:"addIdForKey",value:s(function(r,n){r!=null&&this.getIdsFor(r).add(n)},"addIdForKey")},{key:"deleteIdForKey",value:s(function(r,n){r!=null&&this.getIdsFor(r).delete(n)},"deleteIdForKey")},{key:"getNumberOfIdsForKey",value:s(function(r){return r==null?0:this.getIdsFor(r).size},"getNumberOfIdsForKey")},{key:"updateKeyMappingFor",value:s(function(r){var n=r.id(),i=this.keyForId.get(n),a=this.getKey(r);this.deleteIdForKey(i,n),this.addIdForKey(a,n),this.keyForId.set(n,a)},"updateKeyMappingFor")},{key:"deleteKeyMappingFor",value:s(function(r){var n=r.id(),i=this.keyForId.get(n);this.deleteIdForKey(i,n),this.keyForId.delete(n)},"deleteKeyMappingFor")},{key:"keyHasChangedFor",value:s(function(r){var n=r.id(),i=this.keyForId.get(n),a=this.getKey(r);return i!==a},"keyHasChangedFor")},{key:"isInvalid",value:s(function(r){return this.keyHasChangedFor(r)||this.doesEleInvalidateKey(r)},"isInvalid")},{key:"getCachesAt",value:s(function(r){var n=this.cachesByLvl,i=this.lvls,a=n.get(r);return a||(a=new Oh,n.set(r,a),i.push(r)),a},"getCachesAt")},{key:"getCache",value:s(function(r,n){return this.getCachesAt(n).get(r)},"getCache")},{key:"get",value:s(function(r,n){var i=this.getKey(r),a=this.getCache(i,n);return a!=null&&this.updateKeyMappingFor(r),a},"get")},{key:"getForCachedKey",value:s(function(r,n){var i=this.keyForId.get(r.id()),a=this.getCache(i,n);return a},"getForCachedKey")},{key:"hasCache",value:s(function(r,n){return this.getCachesAt(n).has(r)},"hasCache")},{key:"has",value:s(function(r,n){var i=this.getKey(r);return this.hasCache(i,n)},"has")},{key:"setCache",value:s(function(r,n,i){i.key=r,this.getCachesAt(n).set(r,i)},"setCache")},{key:"set",value:s(function(r,n,i){var a=this.getKey(r);this.setCache(a,n,i),this.updateKeyMappingFor(r)},"set")},{key:"deleteCache",value:s(function(r,n){this.getCachesAt(n).delete(r)},"deleteCache")},{key:"delete",value:s(function(r,n){var i=this.getKey(r);this.deleteCache(i,n)},"_delete")},{key:"invalidateKey",value:s(function(r){var n=this;this.lvls.forEach(function(i){return n.deleteCache(r,i)})},"invalidateKey")},{key:"invalidate",value:s(function(r){var n=r.id(),i=this.keyForId.get(n);this.deleteKeyMappingFor(r);var a=this.doesEleInvalidateKey(r);return a&&this.invalidateKey(i),a||this.getNumberOfIdsForKey(i)===0},"invalidate")}])})(),Bbe=25,T5=50,I5=-4,vF=3,ETe=7.99,Spt=8,wpt=1024,kpt=1024,Ept=1024,Apt=.2,Rpt=.8,_pt=10,Lpt=.15,Dpt=.1,Ipt=.9,Mpt=.9,Npt=100,Ppt=1,L1={dequeue:"dequeue",downscale:"downscale",highQuality:"highQuality"},Opt=ts({getKey:null,doesEleInvalidateKey:B5,drawElement:null,getBoundingBox:null,getRotationPoint:null,getRotationOffset:null,isVisible:p2e,allowEdgeTxrCaching:!0,allowParentTxrCaching:!0}),_T=s(function(t,r){var n=this;n.renderer=t,n.onDequeues=[];var i=Opt(r);kr(n,i),n.lookup=new Cpt(i.getKey,i.doesEleInvalidateKey),n.setupDequeueing()},"ElementTextureCache"),va=_T.prototype;va.reasons=L1;va.getTextureQueue=function(e){var t=this;return t.eleImgCaches=t.eleImgCaches||{},t.eleImgCaches[e]=t.eleImgCaches[e]||[]};va.getRetiredTextureQueue=function(e){var t=this,r=t.eleImgCaches.retired=t.eleImgCaches.retired||{},n=r[e]=r[e]||[];return n};va.getElementQueue=function(){var e=this,t=e.eleCacheQueue=e.eleCacheQueue||new XT(function(r,n){return n.reqs-r.reqs});return t};va.getElementKeyToQueue=function(){var e=this,t=e.eleKeyToCacheQueue=e.eleKeyToCacheQueue||{};return t};va.getElement=function(e,t,r,n,i){var a=this,o=this.renderer,l=o.cy.zoom(),u=this.lookup;if(!t||t.w===0||t.h===0||isNaN(t.w)||isNaN(t.h)||!e.visible()||e.removed()||!a.allowEdgeTxrCaching&&e.isEdge()||!a.allowParentTxrCaching&&e.isParent())return null;if(n==null&&(n=Math.ceil(LF(l*r))),n=ETe||n>vF)return null;var h=Math.pow(2,n),d=t.h*h,f=t.w*h,p=o.eleTextBiggerThanMin(e,h);if(!this.isVisible(e,p))return null;var m=u.get(e,n);if(m&&m.invalidated&&(m.invalidated=!1,m.texture.invalidatedWidth-=m.width),m)return m;var g;if(d<=Bbe?g=Bbe:d<=T5?g=T5:g=Math.ceil(d/T5)*T5,d>Ept||f>kpt)return null;var y=a.getTextureQueue(g),v=y[y.length-2],x=s(function(){return a.recycleTexture(g,f)||a.addTexture(g,f)},"addNewTxr");v||(v=y[y.length-1]),v||(v=x()),v.width-v.usedWidthn;N--)M=a.getElement(e,t,r,N,L1.downscale);P()}else return a.queueElement(e,S.level-1),S;else{var _;if(!T&&!w&&!C)for(var A=n-1;A>=I5;A--){var I=u.get(e,A);if(I){_=I;break}}if(b(_))return a.queueElement(e,n),_;v.context.translate(v.usedWidth,0),v.context.scale(h,h),this.drawElement(v.context,e,t,p,!1),v.context.scale(1/h,1/h),v.context.translate(-v.usedWidth,0)}return m={x:v.usedWidth,texture:v,level:n,scale:h,width:f,height:d,scaledLabelShown:p},v.usedWidth+=Math.ceil(f+Spt),v.eleCaches.push(m),u.set(e,n,m),a.checkTextureFullness(v),m};va.invalidateElements=function(e){for(var t=0;t=Apt*e.width&&this.retireTexture(e)};va.checkTextureFullness=function(e){var t=this,r=t.getTextureQueue(e.height);e.usedWidth/e.width>Rpt&&e.fullnessChecks>=_pt?qf(r,e):e.fullnessChecks++};va.retireTexture=function(e){var t=this,r=e.height,n=t.getTextureQueue(r),i=this.lookup;qf(n,e),e.retired=!0;for(var a=e.eleCaches,o=0;o=t)return o.retired=!1,o.usedWidth=0,o.invalidatedWidth=0,o.fullnessChecks=0,_F(o.eleCaches),o.context.setTransform(1,0,0,1,0,0),o.context.clearRect(0,0,o.width,o.height),qf(i,o),n.push(o),o}};va.queueElement=function(e,t){var r=this,n=r.getElementQueue(),i=r.getElementKeyToQueue(),a=this.getKey(e),o=i[a];if(o)o.level=Math.max(o.level,t),o.eles.merge(e),o.reqs++,n.updateItem(o);else{var l={eles:e.spawn().merge(e),level:t,reqs:1,key:a};n.push(l),i[a]=l}};va.dequeue=function(e){for(var t=this,r=t.getElementQueue(),n=t.getElementKeyToQueue(),i=[],a=t.lookup,o=0;o0;o++){var l=r.pop(),u=l.key,h=l.eles[0],d=a.hasCache(h,l.level);if(n[u]=null,d)continue;i.push(l);var f=t.getBoundingBox(h);t.getElement(h,f,e,l.level,L1.dequeue)}return i};va.removeFromQueue=function(e){var t=this,r=t.getElementQueue(),n=t.getElementKeyToQueue(),i=this.getKey(e),a=n[i];a!=null&&(a.eles.length===1?(a.reqs=AF,r.updateItem(a),r.pop(),n[i]=null):a.eles.unmerge(e))};va.onDequeue=function(e){this.onDequeues.push(e)};va.offDequeue=function(e){qf(this.onDequeues,e)};va.setupDequeueing=kTe.setupDequeueing({deqRedrawThreshold:Npt,deqCost:Lpt,deqAvgCost:Dpt,deqNoDrawCost:Ipt,deqFastCost:Mpt,deq:s(function(t,r,n){return t.dequeue(r,n)},"deq"),onDeqd:s(function(t,r){for(var n=0;n=$pt||r>H5)return null}n.validateLayersElesOrdering(r,e);var u=n.layersByLevel,h=Math.pow(2,r),d=u[r]=u[r]||[],f,p=n.levelIsComplete(r,e),m,g=s(function(){var P=s(function(D){if(n.validateLayersElesOrdering(D,e),n.levelIsComplete(D,e))return m=u[D],!0},"canUseAsTmpLvl"),N=s(function(D){if(!m)for(var L=r+D;DT<=L&&L<=H5&&!P(L);L+=D);},"checkLvls");N(1),N(-1);for(var _=d.length-1;_>=0;_--){var A=d[_];A.invalid&&qf(d,A)}},"checkTempLevels");if(!p)g();else return d;var y=s(function(){if(!f){f=qs();for(var P=0;PFbe||A>Fbe)return null;var I=_*A;if(I>Upt)return null;var D=n.makeLayer(f,r);if(N!=null){var L=d.indexOf(N)+1;d.splice(L,0,D)}else(P.insert===void 0||P.insert)&&d.unshift(D);return D},"makeLayer");if(n.skipping&&!l)return null;for(var x=null,b=e.length/Bpt,T=!l,w=0;w=b||!b2e(x.bb,C.boundingBox()))&&(x=v({insert:!0,after:x}),!x))return null;m||T?n.queueLayer(x,C):n.drawEleInLayer(x,C,r,t),x.eles.push(C),k[r]=x}return m||(T?null:d)};rs.getEleLevelForLayerLevel=function(e,t){return e};rs.drawEleInLayer=function(e,t,r,n){var i=this,a=this.renderer,o=e.context,l=t.boundingBox();l.w===0||l.h===0||!t.visible()||(r=i.getEleLevelForLayerLevel(r,n),a.setImgSmoothing(o,!1),a.drawCachedElement(o,t,null,null,r,Ypt),a.setImgSmoothing(o,!0))};rs.levelIsComplete=function(e,t){var r=this,n=r.layersByLevel[e];if(!n||n.length===0)return!1;for(var i=0,a=0;a0||o.invalid)return!1;i+=o.eles.length}return i===t.length};rs.validateLayersElesOrdering=function(e,t){var r=this.layersByLevel[e];if(r)for(var n=0;n0){t=!0;break}}return t};rs.invalidateElements=function(e){var t=this;e.length!==0&&(t.lastInvalidationTime=Bh(),!(e.length===0||!t.haveLayers())&&t.updateElementsInLayers(e,s(function(n,i,a){t.invalidateLayer(n)},"invalAssocLayers")))};rs.invalidateLayer=function(e){if(this.lastInvalidationTime=Bh(),!e.invalid){var t=e.level,r=e.eles,n=this.layersByLevel[t];qf(n,e),e.elesQueue=[],e.invalid=!0,e.replacement&&(e.replacement.invalid=!0);for(var i=0;i3&&arguments[3]!==void 0?arguments[3]:!0,i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,a=arguments.length>5&&arguments[5]!==void 0?arguments[5]:!0,o=this,l=t._private.rscratch;if(!(a&&!t.visible())&&!(l.badLine||l.allpts==null||isNaN(l.allpts[0]))){var u;r&&(u=r,e.translate(-u.x1,-u.y1));var h=a?t.pstyle("opacity").value:1,d=a?t.pstyle("line-opacity").value:1,f=t.pstyle("curve-style").value,p=t.pstyle("line-style").value,m=t.pstyle("width").pfValue,g=t.pstyle("line-cap").value,y=t.pstyle("line-outline-width").value,v=t.pstyle("line-outline-color").value,x=h*d,b=h*d,T=s(function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:x;f==="straight-triangle"?(o.eleStrokeStyle(e,t,D),o.drawEdgeTrianglePath(t,e,l.allpts)):(e.lineWidth=m,e.lineCap=g,o.eleStrokeStyle(e,t,D),o.drawEdgePath(t,e,l.allpts,p),e.lineCap="butt")},"drawLine"),w=s(function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:x;if(e.lineWidth=m+y,e.lineCap=g,y>0)o.colorStrokeStyle(e,v[0],v[1],v[2],D);else{e.lineCap="butt";return}f==="straight-triangle"?o.drawEdgeTrianglePath(t,e,l.allpts):(o.drawEdgePath(t,e,l.allpts,p),e.lineCap="butt")},"drawLineOutline"),C=s(function(){i&&o.drawEdgeOverlay(e,t)},"drawOverlay"),S=s(function(){i&&o.drawEdgeUnderlay(e,t)},"drawUnderlay"),k=s(function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:b;o.drawArrowheads(e,t,D)},"drawArrows"),R=s(function(){o.drawElementText(e,t,null,n)},"drawText");e.lineJoin="round";var M=t.pstyle("ghost").value==="yes";if(M){var P=t.pstyle("ghost-offset-x").pfValue,N=t.pstyle("ghost-offset-y").pfValue,_=t.pstyle("ghost-opacity").value,A=x*_;e.translate(P,N),T(A),k(A),e.translate(-P,-N)}else w();S(),T(),k(),C(),R(),r&&e.translate(u.x1,u.y1)}};_Te=s(function(t){if(!["overlay","underlay"].includes(t))throw new Error("Invalid state");return function(r,n){if(n.visible()){var i=n.pstyle("".concat(t,"-opacity")).value;if(i!==0){var a=this,o=a.usePaths(),l=n._private.rscratch,u=n.pstyle("".concat(t,"-padding")).pfValue,h=2*u,d=n.pstyle("".concat(t,"-color")).value;r.lineWidth=h,l.edgeType==="self"&&!o?r.lineCap="butt":r.lineCap="round",a.colorStrokeStyle(r,d[0],d[1],d[2],i),a.drawEdgePath(n,r,l.allpts,"solid")}}}},"drawEdgeOverlayUnderlay");Gh.drawEdgeOverlay=_Te("overlay");Gh.drawEdgeUnderlay=_Te("underlay");Gh.drawEdgePath=function(e,t,r,n){var i=e._private.rscratch,a=t,o,l=!1,u=this.usePaths(),h=e.pstyle("line-dash-pattern").pfValue,d=e.pstyle("line-dash-offset").pfValue;if(u){var f=r.join("$"),p=i.pathCacheKey&&i.pathCacheKey===f;p?(o=t=i.pathCache,l=!0):(o=t=new Path2D,i.pathCacheKey=f,i.pathCache=o)}if(a.setLineDash)switch(n){case"dotted":a.setLineDash([1,1]);break;case"dashed":a.setLineDash(h),a.lineDashOffset=d;break;case"solid":a.setLineDash([]);break}if(!l&&!i.badLine)switch(t.beginPath&&t.beginPath(),t.moveTo(r[0],r[1]),i.edgeType){case"bezier":case"self":case"compound":case"multibezier":for(var m=2;m+35&&arguments[5]!==void 0?arguments[5]:!0,o=this;if(n==null){if(a&&!o.eleTextBiggerThanMin(t))return}else if(n===!1)return;if(t.isNode()){var l=t.pstyle("label");if(!l||!l.value)return;var u=o.getLabelJustification(t),h=t.pstyle("text-metrics").strValue==="glyph";e.textAlign=u,e.textBaseline=h?"alphabetic":"bottom"}else{var d=t.element()._private.rscratch.badLine,f=t.pstyle("label"),p=t.pstyle("source-label"),m=t.pstyle("target-label");if(d||(!f||!f.value)&&(!p||!p.value)&&(!m||!m.value))return;e.textAlign="center",e.textBaseline="bottom"}var g=!r,y;r&&(y=r,e.translate(-y.x1,-y.y1)),i==null?(o.drawText(e,t,null,g,a),t.isEdge()&&(o.drawText(e,t,"source",g,a),o.drawText(e,t,"target",g,a))):o.drawText(e,t,i,g,a),r&&e.translate(y.x1,y.y1)};lg.getFontCache=function(e){var t;this.fontCaches=this.fontCaches||[];for(var r=0;r2&&arguments[2]!==void 0?arguments[2]:!0,n=t.pstyle("font-style").strValue,i=t.pstyle("font-size").pfValue+"px",a=t.pstyle("font-family").strValue,o=t.pstyle("font-weight").strValue,l=r?t.effectiveOpacity()*t.pstyle("text-opacity").value:1,u=t.pstyle("text-outline-opacity").value*l,h=t.pstyle("color").value,d=t.pstyle("text-outline-color").value;e.font=n+" "+o+" "+i+" "+a,e.lineJoin="round",this.colorFillStyle(e,h[0],h[1],h[2],l),this.colorStrokeStyle(e,d[0],d[1],d[2],u)};s(imt,"circle");s(Wbe,"roundRect");lg.getTextAngle=function(e,t){var r,n=e._private,i=n.rscratch,a=t?t+"-":"",o=e.pstyle(a+"text-rotation");if(o.strValue==="autorotate"){var l=Ws(i,"labelAngle",t);r=e.isEdge()?l:0}else o.strValue==="none"?r=0:r=o.pfValue;return r};lg.drawText=function(e,t,r){var n=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!0,i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,a=t._private,o=a.rscratch,l=i?t.effectiveOpacity():1;if(!(i&&(l===0||t.pstyle("text-opacity").value===0))){r==="main"&&(r=null);var u=Ws(o,"labelX",r),h=Ws(o,"labelY",r),d,f,p=this.getLabelText(t,r);if(p!=null&&p!==""&&!isNaN(u)&&!isNaN(h)){this.setupTextStyle(e,t,i);var m=r?r+"-":"",g=Ws(o,"labelWidth",r),y=Ws(o,"labelHeight",r),v=Ws(o,"labelActualDescent",r),x=t.pstyle(m+"text-margin-x").pfValue,b=t.pstyle(m+"text-margin-y").pfValue,T=t.isEdge(),w=t.pstyle("text-halign").value,C=t.pstyle("text-valign").value;T&&(w="center",C="center"),u+=x,h+=b;var S;n?S=this.getTextAngle(t,r):S=0,S!==0&&(d=u,f=h,e.translate(d,f),e.rotate(S),u=0,h=0);var k=$1(w),R=F1(C);switch(R){case"top":break;case"center":h+=y/2;break;case"bottom":h+=y;break}var M=t.pstyle("text-background-opacity").value,P=t.pstyle("text-border-opacity").value,N=t.pstyle("text-border-width").pfValue,_=t.pstyle("text-background-padding").pfValue,A=t.pstyle("text-background-shape").strValue,I=A==="round-rectangle"||A==="roundrectangle",D=A==="circle",L=2;if(M>0||N>0&&P>0){var B=e.fillStyle,O=e.strokeStyle,$=e.lineWidth,W=t.pstyle("text-background-color").value,V=t.pstyle("text-border-color").value,z=t.pstyle("text-border-style").value,H=M>0,K=N>0&&P>0,J=u-_;switch(k){case"left":J-=g;break;case"center":J-=g/2;break}var ne=h-y-_,Z=g+2*_,pe=y+2*_;if(H&&(e.fillStyle="rgba(".concat(W[0],",").concat(W[1],",").concat(W[2],",").concat(M*l,")")),K&&(e.strokeStyle="rgba(".concat(V[0],",").concat(V[1],",").concat(V[2],",").concat(P*l,")"),e.lineWidth=N,e.setLineDash))switch(z){case"dotted":e.setLineDash([1,1]);break;case"dashed":e.setLineDash([4,2]);break;case"double":e.lineWidth=N/4,e.setLineDash([]);break;case"solid":default:e.setLineDash([]);break}if(I?(e.beginPath(),Wbe(e,J,ne,Z,pe,L)):D?(e.beginPath(),imt(e,J,ne,Z,pe)):(e.beginPath(),e.rect(J,ne,Z,pe)),H&&e.fill(),K&&e.stroke(),K&&z==="double"){var te=N/2;e.beginPath(),I?Wbe(e,J+te,ne+te,Z-2*te,pe-2*te,L):e.rect(J+te,ne+te,Z-2*te,pe-2*te),e.stroke()}e.fillStyle=B,e.strokeStyle=O,e.lineWidth=$,e.setLineDash&&e.setLineDash([])}var fe=2*t.pstyle("text-outline-width").pfValue;if(fe>0&&(e.lineWidth=fe),h-=v,t.pstyle("text-wrap").value==="wrap"){var ce=Ws(o,"labelWrapCachedLines",r),de=Ws(o,"labelLineHeight",r),Ee=g/2,we=this.getLabelJustification(t);switch(we==="auto"||(k==="left"?we==="left"?u+=-g:we==="center"&&(u+=-Ee):k==="center"?we==="left"?u+=-Ee:we==="right"&&(u+=Ee):k==="right"&&(we==="center"?u+=Ee:we==="right"&&(u+=g))),R){case"top":h-=(ce.length-1)*de;break;case"center":case"bottom":h-=(ce.length-1)*de;break}for(var Fe=0;Fe0&&e.strokeText(ce[Fe],u,h),e.fillText(ce[Fe],u,h),h+=de}else fe>0&&e.strokeText(p,u,h),e.fillText(p,u,h);S!==0&&(e.rotate(-S),e.translate(-d,-f))}}};Qf={};Qf.drawNode=function(e,t,r){var n=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!0,i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,a=arguments.length>5&&arguments[5]!==void 0?arguments[5]:!0,o=this,l,u,h=t._private,d=h.rscratch,f=t.position();if(!(!Vt(f.x)||!Vt(f.y))&&!(a&&!t.visible())){var p=a?t.effectiveOpacity():1,m=o.usePaths(),g,y=!1,v=t.padding();l=t.width()+2*v,u=t.height()+2*v;var x;r&&(x=r,e.translate(-x.x1,-x.y1));for(var b=t.pstyle("background-image"),T=b.value,w=new Array(T.length),C=new Array(T.length),S=0,k=0;k0&&arguments[0]!==void 0?arguments[0]:A;o.eleFillStyle(e,t,q)},"setupShapeColor"),te=s(function(){var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:V;o.colorStrokeStyle(e,I[0],I[1],I[2],q)},"setupBorderColor"),fe=s(function(){var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:J;o.colorStrokeStyle(e,H[0],H[1],H[2],q)},"setupOutlineColor"),ce=s(function(q,ie,j,U){var Y=o.nodePathCache=o.nodePathCache||[],Ae=f2e(j==="polygon"?j+","+U.join(","):j,""+ie,""+q,""+Z),me=Y[Ae],Se,Ce=!1;return me!=null?(Se=me,Ce=!0,d.pathCache=Se):(Se=new Path2D,Y[Ae]=d.pathCache=Se),{path:Se,cacheHit:Ce}},"getPath"),de=t.pstyle("shape").strValue,Ee=t.pstyle("shape-polygon-points").pfValue;if(m){e.translate(f.x,f.y);var we=ce(l,u,de,Ee);g=we.path,y=we.cacheHit}var Fe=s(function(){if(!y){var q=f;m&&(q={x:0,y:0}),o.nodeShapes[o.getNodeShape(t)].draw(g||e,q.x,q.y,l,u,Z,d)}m?e.fill(g):e.fill()},"drawShape"),Ye=s(function(){for(var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:p,ie=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,j=h.backgrounding,U=0,Y=0;Y0&&arguments[0]!==void 0?arguments[0]:!1,ie=arguments.length>1&&arguments[1]!==void 0?arguments[1]:p;o.hasPie(t)&&(o.drawPie(e,t,ie),q&&(m||o.nodeShapes[o.getNodeShape(t)].draw(e,f.x,f.y,l,u,Z,d)))},"drawPie"),ye=s(function(){var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!1,ie=arguments.length>1&&arguments[1]!==void 0?arguments[1]:p;o.hasStripe(t)&&(e.save(),m?e.clip(d.pathCache):(o.nodeShapes[o.getNodeShape(t)].draw(e,f.x,f.y,l,u,Z,d),e.clip()),o.drawStripe(e,t,ie),e.restore(),q&&(m||o.nodeShapes[o.getNodeShape(t)].draw(e,f.x,f.y,l,u,Z,d)))},"drawStripe"),Be=s(function(){var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:p,ie=(N>0?N:-N)*q,j=N>0?0:255;N!==0&&(o.colorFillStyle(e,j,j,j,ie),m?e.fill(g):e.fill())},"darken"),Ie=s(function(){if(_>0){if(e.lineWidth=_,e.lineCap=B,e.lineJoin=L,e.setLineDash)switch(D){case"dotted":e.setLineDash([1,1]);break;case"dashed":e.setLineDash($),e.lineDashOffset=W;break;case"solid":case"double":e.setLineDash([]);break}if(O!=="center"){if(e.save(),e.lineWidth*=2,O==="inside")m?e.clip(g):e.clip();else{var q=new Path2D;q.rect(-l/2-_,-u/2-_,l+2*_,u+2*_),q.addPath(g),e.clip(q,"evenodd")}m?e.stroke(g):e.stroke(),e.restore()}else m?e.stroke(g):e.stroke();if(D==="double"){e.lineWidth=_/3;var ie=e.globalCompositeOperation;e.globalCompositeOperation="destination-out",m?e.stroke(g):e.stroke(),e.globalCompositeOperation=ie}e.setLineDash&&e.setLineDash([])}},"drawBorder"),Ne=s(function(){if(z>0){if(e.lineWidth=z,e.lineCap="butt",e.setLineDash)switch(K){case"dotted":e.setLineDash([1,1]);break;case"dashed":e.setLineDash([4,2]);break;case"solid":case"double":e.setLineDash([]);break}var q=f;m&&(q={x:0,y:0});var ie=o.getNodeShape(t),j=_;O==="inside"&&(j=0),O==="outside"&&(j*=2);var U=(l+j+(z+ne))/l,Y=(u+j+(z+ne))/u,Ae=l*U,me=u*Y,Se=o.nodeShapes[ie].points,Ce;if(m){var Me=ce(Ae,me,ie,Se);Ce=Me.path}if(ie==="ellipse")o.drawEllipsePath(Ce||e,q.x,q.y,Ae,me);else if(["round-diamond","round-heptagon","round-hexagon","round-octagon","round-pentagon","round-polygon","round-triangle","round-tag"].includes(ie)){var _e=0,ze=0,Le=0;ie==="round-diamond"?_e=(j+ne+z)*1.4:ie==="round-heptagon"?(_e=(j+ne+z)*1.075,Le=-(j/2+ne+z)/35):ie==="round-hexagon"?_e=(j+ne+z)*1.12:ie==="round-pentagon"?(_e=(j+ne+z)*1.13,Le=-(j/2+ne+z)/15):ie==="round-tag"?(_e=(j+ne+z)*1.12,ze=(j/2+z+ne)*.07):ie==="round-triangle"&&(_e=(j+ne+z)*(Math.PI/2),Le=-(j+ne/2+z)/Math.PI),_e!==0&&(U=(l+_e)/l,Ae=l*U,["round-hexagon","round-tag"].includes(ie)||(Y=(u+_e)/u,me=u*Y)),Z=Z==="auto"?S2e(Ae,me):Z;for(var qe=Ae/2,ve=me/2,lt=Z+(j+z+ne)/2,Oe=new Array(Se.length/2),mt=new Array(Se.length/2),At=0;At0){if(i=i||n.position(),a==null||o==null){var m=n.padding();a=n.width()+2*m,o=n.height()+2*m}l.colorFillStyle(r,d[0],d[1],d[2],h),l.nodeShapes[f].draw(r,i.x,i.y,a+u*2,o+u*2,p),r.fill()}}}},"drawNodeOverlayUnderlay");Qf.drawNodeOverlay=LTe("overlay");Qf.drawNodeUnderlay=LTe("underlay");Qf.hasPie=function(e){return e=e[0],e._private.hasPie};Qf.hasStripe=function(e){return e=e[0],e._private.hasStripe};Qf.drawPie=function(e,t,r,n){t=t[0],n=n||t.position();var i=t.cy().style(),a=t.pstyle("pie-size"),o=t.pstyle("pie-hole"),l=t.pstyle("pie-start-angle").pfValue,u=n.x,h=n.y,d=t.width(),f=t.height(),p=Math.min(d,f)/2,m,g=0,y=this.usePaths();if(y&&(u=0,h=0),a.units==="%"?p=p*a.pfValue:a.pfValue!==void 0&&(p=a.pfValue/2),o.units==="%"?m=p*o.pfValue:o.pfValue!==void 0&&(m=o.pfValue/2),!(m>=p))for(var v=1;v<=i.pieBackgroundN;v++){var x=t.pstyle("pie-"+v+"-background-size").value,b=t.pstyle("pie-"+v+"-background-color").value,T=t.pstyle("pie-"+v+"-background-opacity").value*r,w=x/100;w+g>1&&(w=1-g);var C=1.5*Math.PI+2*Math.PI*g;C+=l;var S=2*Math.PI*w,k=C+S;x===0||g>=1||g+w>1||(m===0?(e.beginPath(),e.moveTo(u,h),e.arc(u,h,p,C,k),e.closePath()):(e.beginPath(),e.arc(u,h,p,C,k),e.arc(u,h,m,k,C,!0),e.closePath()),this.colorFillStyle(e,b[0],b[1],b[2],T),e.fill(),g+=w)}};Qf.drawStripe=function(e,t,r,n){t=t[0],n=n||t.position();var i=t.cy().style(),a=n.x,o=n.y,l=t.width(),u=t.height(),h=0,d=this.usePaths();e.save();var f=t.pstyle("stripe-direction").value,p=t.pstyle("stripe-size");switch(f){case"vertical":break;case"righward":e.rotate(-Math.PI/2);break}var m=l,g=u;p.units==="%"?(m=m*p.pfValue,g=g*p.pfValue):p.pfValue!==void 0&&(m=p.pfValue,g=p.pfValue),d&&(a=0,o=0),o-=m/2,a-=g/2;for(var y=1;y<=i.stripeBackgroundN;y++){var v=t.pstyle("stripe-"+y+"-background-size").value,x=t.pstyle("stripe-"+y+"-background-color").value,b=t.pstyle("stripe-"+y+"-background-opacity").value*r,T=v/100;T+h>1&&(T=1-h),!(v===0||h>=1||h+T>1)&&(e.beginPath(),e.rect(a,o+g*h,m,g*T),e.closePath(),this.colorFillStyle(e,x[0],x[1],x[2],b),e.fill(),h+=T)}e.restore()};Hs={},amt=100;Hs.getPixelRatio=function(){var e=this.data.contexts[0];if(this.forcedPixelRatio!=null)return this.forcedPixelRatio;var t=this.cy.window(),r=e.backingStorePixelRatio||e.webkitBackingStorePixelRatio||e.mozBackingStorePixelRatio||e.msBackingStorePixelRatio||e.oBackingStorePixelRatio||e.backingStorePixelRatio||1;return(t.devicePixelRatio||1)/r};Hs.paintCache=function(e){for(var t=this.paintCaches=this.paintCaches||[],r=!0,n,i=0;it.minMbLowQualFrames&&(t.motionBlurPxRatio=t.mbPxRBlurry)),t.clearingMotionBlur&&(t.motionBlurPxRatio=1),t.textureDrawLastFrame&&!f&&(d[t.NODE]=!0,d[t.SELECT_BOX]=!0);var b=r.style(),T=r.zoom(),w=o!==void 0?o:T,C=r.pan(),S={x:C.x,y:C.y},k={zoom:T,pan:{x:C.x,y:C.y}},R=t.prevViewport,M=R===void 0||k.zoom!==R.zoom||k.pan.x!==R.pan.x||k.pan.y!==R.pan.y;!M&&!(y&&!g)&&(t.motionBlurPxRatio=1),l&&(S=l),w*=u,S.x*=u,S.y*=u;var P=t.getCachedZSortedEles();function N(te,fe,ce,de,Ee){var we=te.globalCompositeOperation;te.globalCompositeOperation="destination-out",t.colorFillStyle(te,255,255,255,t.motionBlurTransparency),te.fillRect(fe,ce,de,Ee),te.globalCompositeOperation=we}s(N,"mbclear");function _(te,fe){var ce,de,Ee,we;!t.clearingMotionBlur&&(te===h.bufferContexts[t.MOTIONBLUR_BUFFER_NODE]||te===h.bufferContexts[t.MOTIONBLUR_BUFFER_DRAG])?(ce={x:C.x*m,y:C.y*m},de=T*m,Ee=t.canvasWidth*m,we=t.canvasHeight*m):(ce=S,de=w,Ee=t.canvasWidth,we=t.canvasHeight),te.setTransform(1,0,0,1,0,0),fe==="motionBlur"?N(te,0,0,Ee,we):!n&&(fe===void 0||fe)&&te.clearRect(0,0,Ee,we),i||(te.translate(ce.x,ce.y),te.scale(de,de)),l&&te.translate(l.x,l.y),o&&te.scale(o,o)}if(s(_,"setContextTransform"),f||(t.textureDrawLastFrame=!1),f){if(t.textureDrawLastFrame=!0,!t.textureCache){t.textureCache={},t.textureCache.bb=r.mutableElements().boundingBox(),t.textureCache.texture=t.data.bufferCanvases[t.TEXTURE_BUFFER];var A=t.data.bufferContexts[t.TEXTURE_BUFFER];A.setTransform(1,0,0,1,0,0),A.clearRect(0,0,t.canvasWidth*t.textureMult,t.canvasHeight*t.textureMult),t.render({forcedContext:A,drawOnlyNodeLayer:!0,forcedPxRatio:u*t.textureMult});var k=t.textureCache.viewport={zoom:r.zoom(),pan:r.pan(),width:t.canvasWidth,height:t.canvasHeight};k.mpan={x:(0-k.pan.x)/k.zoom,y:(0-k.pan.y)/k.zoom}}d[t.DRAG]=!1,d[t.NODE]=!1;var I=h.contexts[t.NODE],D=t.textureCache.texture,k=t.textureCache.viewport;I.setTransform(1,0,0,1,0,0),p?N(I,0,0,k.width,k.height):I.clearRect(0,0,k.width,k.height);var L=b.core("outside-texture-bg-color").value,B=b.core("outside-texture-bg-opacity").value;t.colorFillStyle(I,L[0],L[1],L[2],B),I.fillRect(0,0,k.width,k.height);var T=r.zoom();_(I,!1),I.clearRect(k.mpan.x,k.mpan.y,k.width/k.zoom/u,k.height/k.zoom/u),I.drawImage(D,k.mpan.x,k.mpan.y,k.width/k.zoom/u,k.height/k.zoom/u)}else t.textureOnViewport&&!n&&(t.textureCache=null);var O=r.extent(),$=t.pinching||t.hoverData.dragging||t.swipePanning||t.data.wheelZooming||t.hoverData.draggingEles||t.cy.animated(),W=t.hideEdgesOnViewport&&$,V=[];if(V[t.NODE]=!d[t.NODE]&&p&&!t.clearedForMotionBlur[t.NODE]||t.clearingMotionBlur,V[t.NODE]&&(t.clearedForMotionBlur[t.NODE]=!0),V[t.DRAG]=!d[t.DRAG]&&p&&!t.clearedForMotionBlur[t.DRAG]||t.clearingMotionBlur,V[t.DRAG]&&(t.clearedForMotionBlur[t.DRAG]=!0),d[t.NODE]||i||a||V[t.NODE]){var z=p&&!V[t.NODE]&&m!==1,I=n||(z?t.data.bufferContexts[t.MOTIONBLUR_BUFFER_NODE]:h.contexts[t.NODE]),H=p&&!z?"motionBlur":void 0;_(I,H),W?t.drawCachedNodes(I,P.nondrag,u,O):t.drawLayeredElements(I,P.nondrag,u,O),t.debug&&t.drawDebugPoints(I,P.nondrag),!i&&!p&&(d[t.NODE]=!1)}if(!a&&(d[t.DRAG]||i||V[t.DRAG])){var z=p&&!V[t.DRAG]&&m!==1,I=n||(z?t.data.bufferContexts[t.MOTIONBLUR_BUFFER_DRAG]:h.contexts[t.DRAG]);_(I,p&&!z?"motionBlur":void 0),W?t.drawCachedNodes(I,P.drag,u,O):t.drawCachedElements(I,P.drag,u,O),t.debug&&t.drawDebugPoints(I,P.drag),!i&&!p&&(d[t.DRAG]=!1)}if(this.drawSelectionRectangle(e,_),p&&m!==1){var K=h.contexts[t.NODE],J=t.data.bufferCanvases[t.MOTIONBLUR_BUFFER_NODE],ne=h.contexts[t.DRAG],Z=t.data.bufferCanvases[t.MOTIONBLUR_BUFFER_DRAG],pe=s(function(fe,ce,de){fe.setTransform(1,0,0,1,0,0),de||!x?fe.clearRect(0,0,t.canvasWidth,t.canvasHeight):N(fe,0,0,t.canvasWidth,t.canvasHeight);var Ee=m;fe.drawImage(ce,0,0,t.canvasWidth*Ee,t.canvasHeight*Ee,0,0,t.canvasWidth,t.canvasHeight)},"drawMotionBlur");(d[t.NODE]||V[t.NODE])&&(pe(K,J,V[t.NODE]),d[t.NODE]=!1),(d[t.DRAG]||V[t.DRAG])&&(pe(ne,Z,V[t.DRAG]),d[t.DRAG]=!1)}t.prevViewport=k,t.clearingMotionBlur&&(t.clearingMotionBlur=!1,t.motionBlurCleared=!0,t.motionBlur=!0),p&&(t.motionBlurTimeout=setTimeout(function(){t.motionBlurTimeout=null,t.clearedForMotionBlur[t.NODE]=!1,t.clearedForMotionBlur[t.DRAG]=!1,t.motionBlur=!1,t.clearingMotionBlur=!f,t.mbFrames=0,d[t.NODE]=!0,d[t.DRAG]=!0,t.redraw()},amt)),n||r.emit("render")};Hs.drawSelectionRectangle=function(e,t){var r=this,n=r.cy,i=r.data,a=n.style(),o=e.drawOnlyNodeLayer,l=e.drawAllLayers,u=i.canvasNeedsRedraw,h=e.forcedContext;if(r.showFps||!o&&u[r.SELECT_BOX]&&!l){var d=h||i.contexts[r.SELECT_BOX];if(t(d),r.selection[4]==1&&(r.hoverData.selecting||r.touchData.selecting)){var f=r.cy.zoom(),p=a.core("selection-box-border-width").value/f;d.lineWidth=p,d.fillStyle="rgba("+a.core("selection-box-color").value[0]+","+a.core("selection-box-color").value[1]+","+a.core("selection-box-color").value[2]+","+a.core("selection-box-opacity").value+")",d.fillRect(r.selection[0],r.selection[1],r.selection[2]-r.selection[0],r.selection[3]-r.selection[1]),p>0&&(d.strokeStyle="rgba("+a.core("selection-box-border-color").value[0]+","+a.core("selection-box-border-color").value[1]+","+a.core("selection-box-border-color").value[2]+","+a.core("selection-box-opacity").value+")",d.strokeRect(r.selection[0],r.selection[1],r.selection[2]-r.selection[0],r.selection[3]-r.selection[1]))}if(i.bgActivePosistion&&!r.hoverData.selecting){var f=r.cy.zoom(),m=i.bgActivePosistion;d.fillStyle="rgba("+a.core("active-bg-color").value[0]+","+a.core("active-bg-color").value[1]+","+a.core("active-bg-color").value[2]+","+a.core("active-bg-opacity").value+")",d.beginPath(),d.arc(m.x,m.y,a.core("active-bg-size").pfValue/f,0,2*Math.PI),d.fill()}var g=r.lastRedrawTime;if(r.showFps&&g){g=Math.round(g);var y=Math.round(1e3/g),v="1 frame = "+g+" ms = "+y+" fps";if(d.setTransform(1,0,0,1,0,0),d.fillStyle="rgba(255, 0, 0, 0.75)",d.strokeStyle="rgba(255, 0, 0, 0.75)",d.font="30px Arial",!wT){var x=d.measureText(v);wT=x.actualBoundingBoxAscent}d.fillText(v,0,wT);var b=60;d.strokeRect(0,wT+10,250,20),d.fillRect(0,wT+10,250*Math.min(y/b,1),20)}l||(u[r.SELECT_BOX]=!1)}};s(qbe,"compileShader");s(smt,"createProgram");s(omt,"createTextureCanvas");s(YF,"getEffectivePanZoom");s(lmt,"getEffectiveZoom");s(cmt,"modelToRenderedPosition");s(umt,"isSimpleShape");s(hmt,"arrayEqual");s(Xm,"toWebGLColor");s(k1,"indexToVec4");s(dmt,"vec4ToIndex");s(fmt,"createTexture");s(DTe,"getTypeInfo");s(ITe,"createTypedArray");s(pmt,"createTypedArrayView");s(mmt,"createBufferStaticDraw");s(bu,"createBufferDynamicDraw");s(gmt,"create3x3MatrixBufferDynamicDraw");s(ymt,"createPickingFrameBuffer");Hbe=typeof Float32Array<"u"?Float32Array:Array;Math.hypot||(Math.hypot=function(){for(var e=0,t=arguments.length;t--;)e+=arguments[t]*arguments[t];return Math.sqrt(e)});s(Z$,"create");s(Ube,"identity");s(vmt,"multiply");s(M5,"translate");s(Ybe,"rotate");s(xF,"scale");s(xmt,"projection");bmt=(function(){function e(t,r,n,i){Xf(this,e),this.debugID=Math.floor(Math.random()*1e4),this.r=t,this.texSize=r,this.texRows=n,this.texHeight=Math.floor(r/n),this.enableWrapping=!0,this.locked=!1,this.texture=null,this.needsBuffer=!0,this.freePointer={x:0,row:0},this.keyToLocation=new Map,this.canvas=i(t,r,r),this.scratch=i(t,r,this.texHeight,"scratch")}return s(e,"Atlas"),Kf(e,[{key:"lock",value:s(function(){this.locked=!0},"lock")},{key:"getKeys",value:s(function(){return new Set(this.keyToLocation.keys())},"getKeys")},{key:"getScale",value:s(function(r){var n=r.w,i=r.h,a=this.texHeight,o=this.texSize,l=a/i,u=n*l,h=i*l;return u>o&&(l=o/n,u=n*l,h=i*l),{scale:l,texW:u,texH:h}},"getScale")},{key:"draw",value:s(function(r,n,i){var a=this;if(this.locked)throw new Error("can't draw, atlas is locked");var o=this.texSize,l=this.texRows,u=this.texHeight,h=this.getScale(n),d=h.scale,f=h.texW,p=h.texH,m=s(function(T,w){if(i&&w){var C=w.context,S=T.x,k=T.row,R=S,M=u*k;C.save(),C.translate(R,M),C.scale(d,d),i(C,n),C.restore()}},"drawAt"),g=[null,null],y=s(function(){m(a.freePointer,a.canvas),g[0]={x:a.freePointer.x,y:a.freePointer.row*u,w:f,h:p},g[1]={x:a.freePointer.x+f,y:a.freePointer.row*u,w:0,h:p},a.freePointer.x+=f,a.freePointer.x==o&&(a.freePointer.x=0,a.freePointer.row++)},"drawNormal"),v=s(function(){var T=a.scratch,w=a.canvas;T.clear(),m({x:0,row:0},T);var C=o-a.freePointer.x,S=f-C,k=u;{var R=a.freePointer.x,M=a.freePointer.row*u,P=C;w.context.drawImage(T,0,0,P,k,R,M,P,k),g[0]={x:R,y:M,w:P,h:p}}{var N=C,_=(a.freePointer.row+1)*u,A=S;w&&w.context.drawImage(T,N,0,A,k,0,_,A,k),g[1]={x:0,y:_,w:A,h:p}}a.freePointer.x=S,a.freePointer.row++},"drawWrapped"),x=s(function(){a.freePointer.x=0,a.freePointer.row++},"moveToStartOfNextRow");if(this.freePointer.x+f<=o)y();else{if(this.freePointer.row>=l-1)return!1;this.freePointer.x===o?(x(),y()):this.enableWrapping?v():(x(),y())}return this.keyToLocation.set(r,g),this.needsBuffer=!0,g},"draw")},{key:"getOffsets",value:s(function(r){return this.keyToLocation.get(r)},"getOffsets")},{key:"isEmpty",value:s(function(){return this.freePointer.x===0&&this.freePointer.row===0},"isEmpty")},{key:"canFit",value:s(function(r){if(this.locked)return!1;var n=this.texSize,i=this.texRows,a=this.getScale(r),o=a.texW;return this.freePointer.x+o>n?this.freePointer.row1&&arguments[1]!==void 0?arguments[1]:{},a=i.forceRedraw,o=a===void 0?!1:a,l=i.filterEle,u=l===void 0?function(){return!0}:l,h=i.filterType,d=h===void 0?function(){return!0}:h,f=!1,p=!1,m=To(r),g;try{for(m.s();!(g=m.n()).done;){var y=g.value;if(u(y)){var v=To(this.renderTypes.values()),x;try{var b=s(function(){var w=x.value,C=w.type;if(d(C)){var S=n.collections.get(w.collection),k=w.getKey(y),R=Array.isArray(k)?k:[k];if(o)R.forEach(function(_){return S.markKeyForGC(_)}),p=!0;else{var M=w.getID?w.getID(y):y.id(),P=n._key(C,M),N=n.typeAndIdToKey.get(P);N!==void 0&&!hmt(R,N)&&(f=!0,n.typeAndIdToKey.delete(P),N.forEach(function(_){return S.markKeyForGC(_)}))}}},"_loop2");for(v.s();!(x=v.n()).done;)b()}catch(T){v.e(T)}finally{v.f()}}}}catch(T){m.e(T)}finally{m.f()}return p&&(this.gc(),f=!1),f},"invalidate")},{key:"gc",value:s(function(){var r=To(this.collections.values()),n;try{for(r.s();!(n=r.n()).done;){var i=n.value;i.gc()}}catch(a){r.e(a)}finally{r.f()}},"gc")},{key:"getOrCreateAtlas",value:s(function(r,n,i,a){var o=this.renderTypes.get(n),l=this.collections.get(o.collection),u=!1,h=l.draw(a,i,function(p){o.drawClipped?(p.save(),p.beginPath(),p.rect(0,0,i.w,i.h),p.clip(),o.drawElement(p,r,i,!0,!0),p.restore()):o.drawElement(p,r,i,!0,!0),u=!0});if(u){var d=o.getID?o.getID(r):r.id(),f=this._key(n,d);this.typeAndIdToKey.has(f)?this.typeAndIdToKey.get(f).push(a):this.typeAndIdToKey.set(f,[a])}return h},"getOrCreateAtlas")},{key:"getAtlasInfo",value:s(function(r,n){var i=this,a=this.renderTypes.get(n),o=a.getKey(r),l=Array.isArray(o)?o:[o];return l.map(function(u){var h=a.getBoundingBox(r,u),d=i.getOrCreateAtlas(r,n,h,u),f=d.getOffsets(u),p=aa(f,2),m=p[0],g=p[1];return{atlas:d,tex:m,tex1:m,tex2:g,bb:h}})},"getAtlasInfo")},{key:"getDebugInfo",value:s(function(){var r=[],n=To(this.collections),i;try{for(n.s();!(i=n.n()).done;){var a=aa(i.value,2),o=a[0],l=a[1],u=l.getCounts(),h=u.keyCount,d=u.atlasCount;r.push({type:o,keyCount:h,atlasCount:d})}}catch(f){n.e(f)}finally{n.f()}return r},"getDebugInfo")}])})(),wmt=(function(){function e(t){Xf(this,e),this.globalOptions=t,this.atlasSize=t.webglTexSize,this.maxAtlasesPerBatch=t.webglTexPerBatch,this.batchAtlases=[]}return s(e,"AtlasBatchManager"),Kf(e,[{key:"getMaxAtlasesPerBatch",value:s(function(){return this.maxAtlasesPerBatch},"getMaxAtlasesPerBatch")},{key:"getAtlasSize",value:s(function(){return this.atlasSize},"getAtlasSize")},{key:"getIndexArray",value:s(function(){return Array.from({length:this.maxAtlasesPerBatch},function(r,n){return n})},"getIndexArray")},{key:"startBatch",value:s(function(){this.batchAtlases=[]},"startBatch")},{key:"getAtlasCount",value:s(function(){return this.batchAtlases.length},"getAtlasCount")},{key:"getAtlases",value:s(function(){return this.batchAtlases},"getAtlases")},{key:"canAddToCurrentBatch",value:s(function(r){return this.batchAtlases.length===this.maxAtlasesPerBatch?this.batchAtlases.includes(r):!0},"canAddToCurrentBatch")},{key:"getAtlasIndexForBatch",value:s(function(r){var n=this.batchAtlases.indexOf(r);if(n<0){if(this.batchAtlases.length===this.maxAtlasesPerBatch)throw new Error("cannot add more atlases to batch");this.batchAtlases.push(r),n=this.batchAtlases.length-1}return n},"getAtlasIndexForBatch")}])})(),kmt=` + float circleSD(vec2 p, float r) { + return distance(vec2(0), p) - r; // signed distance + } +`,Emt=` + float rectangleSD(vec2 p, vec2 b) { + vec2 d = abs(p)-b; + return distance(vec2(0),max(d,0.0)) + min(max(d.x,d.y),0.0); + } +`,Amt=` + float roundRectangleSD(vec2 p, vec2 b, vec4 cr) { + cr.xy = (p.x > 0.0) ? cr.xy : cr.zw; + cr.x = (p.y > 0.0) ? cr.x : cr.y; + vec2 q = abs(p) - b + cr.x; + return min(max(q.x, q.y), 0.0) + distance(vec2(0), max(q, 0.0)) - cr.x; + } +`,Rmt=` + float ellipseSD(vec2 p, vec2 ab) { + p = abs( p ); // symmetry + + // find root with Newton solver + vec2 q = ab*(p-ab); + float w = (q.x1.0) ? d : -d; + } +`,IT={SCREEN:{name:"screen",screen:!0},PICKING:{name:"picking",picking:!0}},U5={IGNORE:1,USE_BB:2},Q$=0,jbe=1,Xbe=2,J$=3,E1=4,C5=5,kT=6,ET=7,_mt=(function(){function e(t,r,n){Xf(this,e),this.r=t,this.gl=r,this.maxInstances=n.webglBatchSize,this.atlasSize=n.webglTexSize,this.bgColor=n.bgColor,this.debug=n.webglDebug,this.batchDebugInfo=[],n.enableWrapping=!0,n.createTextureCanvas=omt,this.atlasManager=new Smt(t,n),this.batchManager=new wmt(n),this.simpleShapeOptions=new Map,this.program=this._createShaderProgram(IT.SCREEN),this.pickingProgram=this._createShaderProgram(IT.PICKING),this.vao=this._createVAO()}return s(e,"ElementDrawingWebGL"),Kf(e,[{key:"addAtlasCollection",value:s(function(r,n){this.atlasManager.addAtlasCollection(r,n)},"addAtlasCollection")},{key:"addTextureAtlasRenderType",value:s(function(r,n){this.atlasManager.addRenderType(r,n)},"addTextureAtlasRenderType")},{key:"addSimpleShapeRenderType",value:s(function(r,n){this.simpleShapeOptions.set(r,n)},"addSimpleShapeRenderType")},{key:"invalidate",value:s(function(r){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},i=n.type,a=this.atlasManager;return i?a.invalidate(r,{filterType:s(function(l){return l===i},"filterType"),forceRedraw:!0}):a.invalidate(r)},"invalidate")},{key:"gc",value:s(function(){this.atlasManager.gc()},"gc")},{key:"_createShaderProgram",value:s(function(r){var n=this.gl,i=`#version 300 es + precision highp float; + + uniform mat3 uPanZoomMatrix; + uniform int uAtlasSize; + + // instanced + in vec2 aPosition; // a vertex from the unit square + + in mat3 aTransform; // used to transform verticies, eg into a bounding box + in int aVertType; // the type of thing we are rendering + + // the z-index that is output when using picking mode + in vec4 aIndex; + + // For textures + in int aAtlasId; // which shader unit/atlas to use + in vec4 aTex; // x/y/w/h of texture in atlas + + // for edges + in vec4 aPointAPointB; + in vec4 aPointCPointD; + in vec2 aLineWidth; // also used for node border width + + // simple shapes + in vec4 aCornerRadius; // for round-rectangle [top-right, bottom-right, top-left, bottom-left] + in vec4 aColor; // also used for edges + in vec4 aBorderColor; // aLineWidth is used for border width + + // output values passed to the fragment shader + out vec2 vTexCoord; + out vec4 vColor; + out vec2 vPosition; + // flat values are not interpolated + flat out int vAtlasId; + flat out int vVertType; + flat out vec2 vTopRight; + flat out vec2 vBotLeft; + flat out vec4 vCornerRadius; + flat out vec4 vBorderColor; + flat out vec2 vBorderWidth; + flat out vec4 vIndex; + + void main(void) { + int vid = gl_VertexID; + vec2 position = aPosition; // TODO make this a vec3, simplifies some code below + + if(aVertType == `.concat(Q$,`) { + float texX = aTex.x; // texture coordinates + float texY = aTex.y; + float texW = aTex.z; + float texH = aTex.w; + + if(vid == 1 || vid == 2 || vid == 4) { + texX += texW; + } + if(vid == 2 || vid == 4 || vid == 5) { + texY += texH; + } + + float d = float(uAtlasSize); + vTexCoord = vec2(texX / d, texY / d); // tex coords must be between 0 and 1 + + gl_Position = vec4(uPanZoomMatrix * aTransform * vec3(position, 1.0), 1.0); + } + else if(aVertType == `).concat(E1," || aVertType == ").concat(ET,` + || aVertType == `).concat(C5," || aVertType == ").concat(kT,`) { // simple shapes + + // the bounding box is needed by the fragment shader + vBotLeft = (aTransform * vec3(0, 0, 1)).xy; // flat + vTopRight = (aTransform * vec3(1, 1, 1)).xy; // flat + vPosition = (aTransform * vec3(position, 1)).xy; // will be interpolated + + // calculations are done in the fragment shader, just pass these along + vColor = aColor; + vCornerRadius = aCornerRadius; + vBorderColor = aBorderColor; + vBorderWidth = aLineWidth; + + gl_Position = vec4(uPanZoomMatrix * aTransform * vec3(position, 1.0), 1.0); + } + else if(aVertType == `).concat(jbe,`) { + vec2 source = aPointAPointB.xy; + vec2 target = aPointAPointB.zw; + + // adjust the geometry so that the line is centered on the edge + position.y = position.y - 0.5; + + // stretch the unit square into a long skinny rectangle + vec2 xBasis = target - source; + vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x)); + vec2 point = source + xBasis * position.x + yBasis * aLineWidth[0] * position.y; + + gl_Position = vec4(uPanZoomMatrix * vec3(point, 1.0), 1.0); + vColor = aColor; + } + else if(aVertType == `).concat(Xbe,`) { + vec2 pointA = aPointAPointB.xy; + vec2 pointB = aPointAPointB.zw; + vec2 pointC = aPointCPointD.xy; + vec2 pointD = aPointCPointD.zw; + + // adjust the geometry so that the line is centered on the edge + position.y = position.y - 0.5; + + vec2 p0, p1, p2, pos; + if(position.x == 0.0) { // The left side of the unit square + p0 = pointA; + p1 = pointB; + p2 = pointC; + pos = position; + } else { // The right side of the unit square, use same approach but flip the geometry upside down + p0 = pointD; + p1 = pointC; + p2 = pointB; + pos = vec2(0.0, -position.y); + } + + vec2 p01 = p1 - p0; + vec2 p12 = p2 - p1; + vec2 p21 = p1 - p2; + + // Find the normal vector. + vec2 tangent = normalize(normalize(p12) + normalize(p01)); + vec2 normal = vec2(-tangent.y, tangent.x); + + // Find the vector perpendicular to p0 -> p1. + vec2 p01Norm = normalize(vec2(-p01.y, p01.x)); + + // Determine the bend direction. + float sigma = sign(dot(p01 + p21, normal)); + float width = aLineWidth[0]; + + if(sign(pos.y) == -sigma) { + // This is an intersecting vertex. Adjust the position so that there's no overlap. + vec2 point = 0.5 * width * normal * -sigma / dot(normal, p01Norm); + gl_Position = vec4(uPanZoomMatrix * vec3(p1 + point, 1.0), 1.0); + } else { + // This is a non-intersecting vertex. Treat it like a mitre join. + vec2 point = 0.5 * width * normal * sigma * dot(normal, p01Norm); + gl_Position = vec4(uPanZoomMatrix * vec3(p1 + point, 1.0), 1.0); + } + + vColor = aColor; + } + else if(aVertType == `).concat(J$,` && vid < 3) { + // massage the first triangle into an edge arrow + if(vid == 0) + position = vec2(-0.15, -0.3); + if(vid == 1) + position = vec2( 0.0, 0.0); + if(vid == 2) + position = vec2( 0.15, -0.3); + + gl_Position = vec4(uPanZoomMatrix * aTransform * vec3(position, 1.0), 1.0); + vColor = aColor; + } + else { + gl_Position = vec4(2.0, 0.0, 0.0, 1.0); // discard vertex by putting it outside webgl clip space + } + + vAtlasId = aAtlasId; + vVertType = aVertType; + vIndex = aIndex; + } + `),a=this.batchManager.getIndexArray(),o=`#version 300 es + precision highp float; + + // declare texture unit for each texture atlas in the batch + `.concat(a.map(function(h){return"uniform sampler2D uTexture".concat(h,";")}).join(` + `),` + + uniform vec4 uBGColor; + uniform float uZoom; + + in vec2 vTexCoord; + in vec4 vColor; + in vec2 vPosition; // model coordinates + + flat in int vAtlasId; + flat in vec4 vIndex; + flat in int vVertType; + flat in vec2 vTopRight; + flat in vec2 vBotLeft; + flat in vec4 vCornerRadius; + flat in vec4 vBorderColor; + flat in vec2 vBorderWidth; + + out vec4 outColor; + + `).concat(kmt,` + `).concat(Emt,` + `).concat(Amt,` + `).concat(Rmt,` + + vec4 blend(vec4 top, vec4 bot) { // blend colors with premultiplied alpha + return vec4( + top.rgb + (bot.rgb * (1.0 - top.a)), + top.a + (bot.a * (1.0 - top.a)) + ); + } + + vec4 distInterp(vec4 cA, vec4 cB, float d) { // interpolate color using Signed Distance + // scale to the zoom level so that borders don't look blurry when zoomed in + // note 1.5 is an aribitrary value chosen because it looks good + return mix(cA, cB, 1.0 - smoothstep(0.0, 1.5 / uZoom, abs(d))); + } + + void main(void) { + if(vVertType == `).concat(Q$,`) { + // look up the texel from the texture unit + `).concat(a.map(function(h){return"if(vAtlasId == ".concat(h,") outColor = texture(uTexture").concat(h,", vTexCoord);")}).join(` + else `),` + } + else if(vVertType == `).concat(J$,`) { + // mimics how canvas renderer uses context.globalCompositeOperation = 'destination-out'; + outColor = blend(vColor, uBGColor); + outColor.a = 1.0; // make opaque, masks out line under arrow + } + else if(vVertType == `).concat(E1,` && vBorderWidth == vec2(0.0)) { // simple rectangle with no border + outColor = vColor; // unit square is already transformed to the rectangle, nothing else needs to be done + } + else if(vVertType == `).concat(E1," || vVertType == ").concat(ET,` + || vVertType == `).concat(C5," || vVertType == ").concat(kT,`) { // use SDF + + float outerBorder = vBorderWidth[0]; + float innerBorder = vBorderWidth[1]; + float borderPadding = outerBorder * 2.0; + float w = vTopRight.x - vBotLeft.x - borderPadding; + float h = vTopRight.y - vBotLeft.y - borderPadding; + vec2 b = vec2(w/2.0, h/2.0); // half width, half height + vec2 p = vPosition - vec2(vTopRight.x - b[0] - outerBorder, vTopRight.y - b[1] - outerBorder); // translate to center + + float d; // signed distance + if(vVertType == `).concat(E1,`) { + d = rectangleSD(p, b); + } else if(vVertType == `).concat(ET,` && w == h) { + d = circleSD(p, b.x); // faster than ellipse + } else if(vVertType == `).concat(ET,`) { + d = ellipseSD(p, b); + } else { + d = roundRectangleSD(p, b, vCornerRadius.wzyx); + } + + // use the distance to interpolate a color to smooth the edges of the shape, doesn't need multisampling + // we must smooth colors inwards, because we can't change pixels outside the shape's bounding box + if(d > 0.0) { + if(d > outerBorder) { + discard; + } else { + outColor = distInterp(vBorderColor, vec4(0), d - outerBorder); + } + } else { + if(d > innerBorder) { + vec4 outerColor = outerBorder == 0.0 ? vec4(0) : vBorderColor; + vec4 innerBorderColor = blend(vBorderColor, vColor); + outColor = distInterp(innerBorderColor, outerColor, d); + } + else { + vec4 outerColor; + if(innerBorder == 0.0 && outerBorder == 0.0) { + outerColor = vec4(0); + } else if(innerBorder == 0.0) { + outerColor = vBorderColor; + } else { + outerColor = blend(vBorderColor, vColor); + } + outColor = distInterp(vColor, outerColor, d - innerBorder); + } + } + } + else { + outColor = vColor; + } + + `).concat(r.picking?`if(outColor.a == 0.0) discard; + else outColor = vIndex;`:"",` + } + `),l=smt(n,i,o);l.aPosition=n.getAttribLocation(l,"aPosition"),l.aIndex=n.getAttribLocation(l,"aIndex"),l.aVertType=n.getAttribLocation(l,"aVertType"),l.aTransform=n.getAttribLocation(l,"aTransform"),l.aAtlasId=n.getAttribLocation(l,"aAtlasId"),l.aTex=n.getAttribLocation(l,"aTex"),l.aPointAPointB=n.getAttribLocation(l,"aPointAPointB"),l.aPointCPointD=n.getAttribLocation(l,"aPointCPointD"),l.aLineWidth=n.getAttribLocation(l,"aLineWidth"),l.aColor=n.getAttribLocation(l,"aColor"),l.aCornerRadius=n.getAttribLocation(l,"aCornerRadius"),l.aBorderColor=n.getAttribLocation(l,"aBorderColor"),l.uPanZoomMatrix=n.getUniformLocation(l,"uPanZoomMatrix"),l.uAtlasSize=n.getUniformLocation(l,"uAtlasSize"),l.uBGColor=n.getUniformLocation(l,"uBGColor"),l.uZoom=n.getUniformLocation(l,"uZoom"),l.uTextures=[];for(var u=0;u1&&arguments[1]!==void 0?arguments[1]:IT.SCREEN;this.panZoomMatrix=r,this.renderTarget=n,this.batchDebugInfo=[],this.wrappedCount=0,this.simpleCount=0,this.startBatch()},"startFrame")},{key:"startBatch",value:s(function(){this.instanceCount=0,this.batchManager.startBatch()},"startBatch")},{key:"endFrame",value:s(function(){this.endBatch()},"endFrame")},{key:"_isVisible",value:s(function(r,n){return r.visible()?n&&n.isVisible?n.isVisible(r):!0:!1},"_isVisible")},{key:"drawTexture",value:s(function(r,n,i){var a=this.atlasManager,o=this.batchManager,l=a.getRenderTypeOpts(i);if(this._isVisible(r,l)&&!(r.isEdge()&&!this._isValidEdge(r))){if(this.renderTarget.picking&&l.getTexPickingMode){var u=l.getTexPickingMode(r);if(u===U5.IGNORE)return;if(u==U5.USE_BB){this.drawPickingRectangle(r,n,i);return}}var h=a.getAtlasInfo(r,i),d=To(h),f;try{for(d.s();!(f=d.n()).done;){var p=f.value,m=p.atlas,g=p.tex1,y=p.tex2;o.canAddToCurrentBatch(m)||this.endBatch();for(var v=o.getAtlasIndexForBatch(m),x=0,b=[[g,!0],[y,!1]];x=this.maxInstances&&this.endBatch()}}}}catch(N){d.e(N)}finally{d.f()}}},"drawTexture")},{key:"setTransformMatrix",value:s(function(r,n,i,a){var o=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,l=0;if(i.shapeProps&&i.shapeProps.padding&&(l=r.pstyle(i.shapeProps.padding).pfValue),a){var u=a.bb,h=a.tex1,d=a.tex2,f=h.w/(h.w+d.w);o||(f=1-f);var p=this._getAdjustedBB(u,l,o,f);this._applyTransformMatrix(n,p,i,r)}else{var m=i.getBoundingBox(r),g=this._getAdjustedBB(m,l,!0,1);this._applyTransformMatrix(n,g,i,r)}},"setTransformMatrix")},{key:"_applyTransformMatrix",value:s(function(r,n,i,a){var o,l;Ube(r);var u=i.getRotation?i.getRotation(a):0;if(u!==0){var h=i.getRotationPoint(a),d=h.x,f=h.y;M5(r,r,[d,f]),Ybe(r,r,u);var p=i.getRotationOffset(a);o=p.x+(n.xOffset||0),l=p.y+(n.yOffset||0)}else o=n.x1,l=n.y1;M5(r,r,[o,l]),xF(r,r,[n.w,n.h])},"_applyTransformMatrix")},{key:"_getAdjustedBB",value:s(function(r,n,i,a){var o=r.x1,l=r.y1,u=r.w,h=r.h,d=r.yOffset;n&&(o-=n,l-=n,u+=2*n,h+=2*n);var f=0,p=u*a;return i&&a<1?u=p:!i&&a<1&&(f=u-p,o+=f,u=p),{x1:o,y1:l,w:u,h,xOffset:f,yOffset:d}},"_getAdjustedBB")},{key:"drawPickingRectangle",value:s(function(r,n,i){var a=this.atlasManager.getRenderTypeOpts(i),o=this.instanceCount;this.vertTypeBuffer.getView(o)[0]=E1;var l=this.indexBuffer.getView(o);k1(n,l);var u=this.colorBuffer.getView(o);Xm([0,0,0],1,u);var h=this.transformBuffer.getMatrixView(o);this.setTransformMatrix(r,h,a),this.simpleCount++,this.instanceCount++,this.instanceCount>=this.maxInstances&&this.endBatch()},"drawPickingRectangle")},{key:"drawNode",value:s(function(r,n,i){var a=this.simpleShapeOptions.get(i);if(this._isVisible(r,a)){var o=a.shapeProps,l=this._getVertTypeForShape(r,o.shape);if(l===void 0||a.isSimple&&!a.isSimple(r,this.renderTarget)){this.drawTexture(r,n,i);return}var u=this.instanceCount;if(this.vertTypeBuffer.getView(u)[0]=l,l===C5||l===kT){var h=a.getBoundingBox(r),d=this._getCornerRadius(r,o.radius,h),f=this.cornerRadiusBuffer.getView(u);f[0]=d,f[1]=d,f[2]=d,f[3]=d,l===kT&&(f[0]=0,f[2]=0)}var p=this.indexBuffer.getView(u);k1(n,p);var m=this.renderTarget.picking?1:i==="node-body"?r.effectiveOpacity():1,g=this.renderTarget.picking?1:r.pstyle(o.opacity).value*m,y=r.pstyle(o.color).value,v=this.colorBuffer.getView(u);Xm(y,g,v);var x=this.lineWidthBuffer.getView(u);if(x[0]=0,x[1]=0,o.border){var b=r.pstyle("border-width").value;if(b>0){var T=r.pstyle("border-color").value,w=m*r.pstyle("border-opacity").value,C=this.borderColorBuffer.getView(u);Xm(T,w,C);var S=r.pstyle("border-position").value;if(S==="inside")x[0]=0,x[1]=-b;else if(S==="outside")x[0]=b,x[1]=0;else{var k=b/2;x[0]=k,x[1]=-k}}}var R=this.transformBuffer.getMatrixView(u);this.setTransformMatrix(r,R,a),this.simpleCount++,this.instanceCount++,this.instanceCount>=this.maxInstances&&this.endBatch()}},"drawNode")},{key:"_getVertTypeForShape",value:s(function(r,n){var i=r.pstyle(n).value;switch(i){case"rectangle":return E1;case"ellipse":return ET;case"roundrectangle":case"round-rectangle":return C5;case"bottom-round-rectangle":return kT;default:return}},"_getVertTypeForShape")},{key:"_getCornerRadius",value:s(function(r,n,i){var a=i.w,o=i.h;if(r.pstyle(n).value==="auto")return Hf(a,o);var l=r.pstyle(n).pfValue,u=a/2,h=o/2;return Math.min(l,h,u)},"_getCornerRadius")},{key:"drawEdgeArrow",value:s(function(r,n,i){if(r.visible()){var a=r._private.rscratch,o,l,u;if(i==="source"?(o=a.arrowStartX,l=a.arrowStartY,u=a.srcArrowAngle):(o=a.arrowEndX,l=a.arrowEndY,u=a.tgtArrowAngle),!(isNaN(o)||o==null||isNaN(l)||l==null||isNaN(u)||u==null)){var h=r.pstyle(i+"-arrow-shape").value;if(h!=="none"){var d=r.pstyle(i+"-arrow-color").value,f=r.pstyle("opacity").value,p=r.pstyle("line-opacity").value,m=f*p,g=r.pstyle("width").pfValue,y=r.pstyle("arrow-scale").value,v=this.r.getArrowWidth(g,y),x=this.instanceCount,b=this.transformBuffer.getMatrixView(x);Ube(b),M5(b,b,[o,l]),xF(b,b,[v,v]),Ybe(b,b,u),this.vertTypeBuffer.getView(x)[0]=J$;var T=this.indexBuffer.getView(x);k1(n,T);var w=this.colorBuffer.getView(x);Xm(d,m,w),this.instanceCount++,this.instanceCount>=this.maxInstances&&this.endBatch()}}}},"drawEdgeArrow")},{key:"drawEdgeLine",value:s(function(r,n){if(r.visible()){var i=this._getEdgePoints(r);if(i){var a=r.pstyle("opacity").value,o=r.pstyle("line-opacity").value,l=r.pstyle("width").pfValue,u=r.pstyle("line-color").value,h=a*o;if(i.length/2+this.instanceCount>this.maxInstances&&this.endBatch(),i.length==4){var d=this.instanceCount;this.vertTypeBuffer.getView(d)[0]=jbe;var f=this.indexBuffer.getView(d);k1(n,f);var p=this.colorBuffer.getView(d);Xm(u,h,p);var m=this.lineWidthBuffer.getView(d);m[0]=l;var g=this.pointAPointBBuffer.getView(d);g[0]=i[0],g[1]=i[1],g[2]=i[2],g[3]=i[3],this.instanceCount++,this.instanceCount>=this.maxInstances&&this.endBatch()}else for(var y=0;y=this.maxInstances&&this.endBatch()}}}},"drawEdgeLine")},{key:"_isValidEdge",value:s(function(r){var n=r._private.rscratch;return!(n.badLine||n.allpts==null||isNaN(n.allpts[0]))},"_isValidEdge")},{key:"_getEdgePoints",value:s(function(r){var n=r._private.rscratch;if(this._isValidEdge(r)){var i=n.allpts;if(i.length==4)return i;var a=this._getNumSegments(r);return this._getCurveSegmentPoints(i,a)}},"_getEdgePoints")},{key:"_getNumSegments",value:s(function(r){var n=15;return Math.min(Math.max(n,5),this.maxInstances)},"_getNumSegments")},{key:"_getCurveSegmentPoints",value:s(function(r,n){if(r.length==4)return r;for(var i=Array((n+1)*2),a=0;a<=n;a++)if(a==0)i[0]=r[0],i[1]=r[1];else if(a==n)i[a*2]=r[r.length-2],i[a*2+1]=r[r.length-1];else{var o=a/n;this._setCurvePoint(r,o,i,a*2)}return i},"_getCurveSegmentPoints")},{key:"_setCurvePoint",value:s(function(r,n,i,a){if(r.length<=2)i[a]=r[0],i[a+1]=r[1];else{for(var o=Array(r.length-2),l=0;l0}},"isLayerVisible"),l=s(function(f){var p=f.pstyle("text-events").strValue==="yes";return p?U5.USE_BB:U5.IGNORE},"getTexPickingMode"),u=s(function(f){var p=f.position(),m=p.x,g=p.y,y=f.outerWidth(),v=f.outerHeight();return{w:y,h:v,x1:m-y/2,y1:g-v/2}},"getBBForSimpleShape");r.drawing.addAtlasCollection("node",{texRows:e.webglTexRowsNodes}),r.drawing.addAtlasCollection("label",{texRows:e.webglTexRows}),r.drawing.addTextureAtlasRenderType("node-body",{collection:"node",getKey:t.getStyleKey,getBoundingBox:t.getElementBox,drawElement:t.drawElement}),r.drawing.addSimpleShapeRenderType("node-body",{getBoundingBox:u,isSimple:umt,shapeProps:{shape:"shape",color:"background-color",opacity:"background-opacity",radius:"corner-radius",border:!0}}),r.drawing.addSimpleShapeRenderType("node-overlay",{getBoundingBox:u,isVisible:o("overlay"),shapeProps:{shape:"overlay-shape",color:"overlay-color",opacity:"overlay-opacity",padding:"overlay-padding",radius:"overlay-corner-radius"}}),r.drawing.addSimpleShapeRenderType("node-underlay",{getBoundingBox:u,isVisible:o("underlay"),shapeProps:{shape:"underlay-shape",color:"underlay-color",opacity:"underlay-opacity",padding:"underlay-padding",radius:"underlay-corner-radius"}}),r.drawing.addTextureAtlasRenderType("label",{collection:"label",getTexPickingMode:l,getKey:eF(t.getLabelKey,null),getBoundingBox:tF(t.getLabelBox,null),drawClipped:!0,drawElement:t.drawLabel,getRotation:i(null),getRotationPoint:t.getLabelRotationPoint,getRotationOffset:t.getLabelRotationOffset,isVisible:a("label")}),r.drawing.addTextureAtlasRenderType("edge-source-label",{collection:"label",getTexPickingMode:l,getKey:eF(t.getSourceLabelKey,"source"),getBoundingBox:tF(t.getSourceLabelBox,"source"),drawClipped:!0,drawElement:t.drawSourceLabel,getRotation:i("source"),getRotationPoint:t.getSourceLabelRotationPoint,getRotationOffset:t.getSourceLabelRotationOffset,isVisible:a("source-label")}),r.drawing.addTextureAtlasRenderType("edge-target-label",{collection:"label",getTexPickingMode:l,getKey:eF(t.getTargetLabelKey,"target"),getBoundingBox:tF(t.getTargetLabelBox,"target"),drawClipped:!0,drawElement:t.drawTargetLabel,getRotation:i("target"),getRotationPoint:t.getTargetLabelRotationPoint,getRotationOffset:t.getTargetLabelRotationOffset,isVisible:a("target-label")});var h=jT(function(){console.log("garbage collect flag set"),r.data.gc=!0},1e4);r.onUpdateEleCalcs(function(d,f){var p=!1;f&&f.length>0&&(p|=r.drawing.invalidate(f)),p&&h()}),Dmt(r)};s(Lmt,"getBGColor");s(NTe,"getLabelLines");eF=s(function(t,r){return function(n){var i=t(n),a=NTe(n,r);return a.length>1?a.map(function(o,l){return"".concat(i,"_").concat(l)}):i}},"getStyleKeysForLabel"),tF=s(function(t,r){return function(n,i){var a=t(n);if(typeof i=="string"){var o=i.indexOf("_");if(o>0){var l=Number(i.substring(o+1)),u=NTe(n,r),h=a.h/u.length,d=h*l,f=a.y1+d;return{x1:a.x1,w:a.w,y1:f,h,yOffset:d}}}return a}},"getBoundingBoxForLabel");s(Dmt,"overrideCanvasRendererFunctions");s(Imt,"clearWebgl");s(Mmt,"clearCanvas");s(Nmt,"createPanZoomMatrix");s(PTe,"setContextTransform");s(Pmt,"drawSelectionRectangle");s(Omt,"drawAxes");s(Bmt,"drawAtlases");s($mt,"getPickingIndexes");s(Fmt,"findNearestElementsWebgl");s(rF,"drawEle");s(OTe,"renderWebgl");Jf={};Jf.drawPolygonPath=function(e,t,r,n,i,a){var o=n/2,l=i/2;e.beginPath&&e.beginPath(),e.moveTo(t+o*a[0],r+l*a[1]);for(var u=1;u0&&o>0){m.clearRect(0,0,a,o),m.globalCompositeOperation="source-over";var g=this.getCachedZSortedEles();if(e.full)m.translate(-n.x1*h,-n.y1*h),m.scale(h,h),this.drawElements(m,g),m.scale(1/h,1/h),m.translate(n.x1*h,n.y1*h);else{var y=t.pan(),v={x:y.x*h,y:y.y*h};h*=t.zoom(),m.translate(v.x,v.y),m.scale(h,h),this.drawElements(m,g),m.scale(1/h,1/h),m.translate(-v.x,-v.y)}e.bg&&(m.globalCompositeOperation="destination-over",m.fillStyle=e.bg,m.rect(0,0,a,o),m.fill())}return p};s(Gmt,"b64ToBlob");s(Qbe,"b64UriToB64");s($Te,"output");eC.png=function(e){return $Te(e,this.bufferCanvasImage(e),"image/png")};eC.jpg=function(e){return $Te(e,this.bufferCanvasImage(e),"image/jpeg")};FTe={};FTe.nodeShapeImpl=function(e,t,r,n,i,a,o,l){switch(e){case"ellipse":return this.drawEllipsePath(t,r,n,i,a);case"polygon":return this.drawPolygonPath(t,r,n,i,a,o);case"round-polygon":return this.drawRoundPolygonPath(t,r,n,i,a,o,l);case"roundrectangle":case"round-rectangle":return this.drawRoundRectanglePath(t,r,n,i,a,l);case"cutrectangle":case"cut-rectangle":return this.drawCutRectanglePath(t,r,n,i,a,o,l);case"bottomroundrectangle":case"bottom-round-rectangle":return this.drawBottomRoundRectanglePath(t,r,n,i,a,l);case"barrel":return this.drawBarrelPath(t,r,n,i,a)}};zmt=GTe,en=GTe.prototype;en.CANVAS_LAYERS=3;en.SELECT_BOX=0;en.DRAG=1;en.NODE=2;en.WEBGL=3;en.CANVAS_TYPES=["2d","2d","2d","webgl2"];en.BUFFER_COUNT=3;en.TEXTURE_BUFFER=0;en.MOTIONBLUR_BUFFER_NODE=1;en.MOTIONBLUR_BUFFER_DRAG=2;s(GTe,"CanvasRenderer");en.redrawHint=function(e,t){var r=this;switch(e){case"eles":r.data.canvasNeedsRedraw[en.NODE]=t;break;case"drag":r.data.canvasNeedsRedraw[en.DRAG]=t;break;case"select":r.data.canvasNeedsRedraw[en.SELECT_BOX]=t;break;case"gc":r.data.gc=!0;break}};Vmt=typeof Path2D<"u";en.path2dEnabled=function(e){if(e===void 0)return this.pathsEnabled;this.pathsEnabled=!!e};en.usePaths=function(){return Vmt&&this.pathsEnabled};en.setImgSmoothing=function(e,t){e.imageSmoothingEnabled!=null?e.imageSmoothingEnabled=t:(e.webkitImageSmoothingEnabled=t,e.mozImageSmoothingEnabled=t,e.msImageSmoothingEnabled=t)};en.getImgSmoothing=function(e){return e.imageSmoothingEnabled!=null?e.imageSmoothingEnabled:e.webkitImageSmoothingEnabled||e.mozImageSmoothingEnabled||e.msImageSmoothingEnabled};en.makeOffscreenCanvas=function(e,t){var r;if((typeof OffscreenCanvas>"u"?"undefined":ya(OffscreenCanvas))!=="undefined")r=new OffscreenCanvas(e,t);else{var n=this.cy.window(),i=n.document;r=i.createElement("canvas"),r.width=e,r.height=t}return r};[RTe,Eu,Gh,UF,lg,Qf,Hs,MTe,Jf,eC,FTe].forEach(function(e){kr(en,e)});Wmt=[{name:"null",impl:fTe},{name:"base",impl:wTe},{name:"canvas",impl:zmt}],qmt=[{type:"layout",extensions:fpt},{type:"renderer",extensions:Wmt}],zTe={},VTe={};s(WTe,"setExtension");s(qTe,"getExtension");s(Hmt,"setModule");s(Umt,"getModule");CF=s(function(){if(arguments.length===2)return qTe.apply(null,arguments);if(arguments.length===3)return WTe.apply(null,arguments);if(arguments.length===4)return Umt.apply(null,arguments);if(arguments.length===5)return Hmt.apply(null,arguments);xi("Invalid extension access syntax")},"extension");zT.prototype.extension=CF;qmt.forEach(function(e){e.extensions.forEach(function(t){WTe(e.type,t.name,t.impl)})});Y5=s(function(){if(!(this instanceof Y5))return new Y5;this.length=0},"Stylesheet"),sg=Y5.prototype;sg.instanceString=function(){return"stylesheet"};sg.selector=function(e){var t=this.length++;return this[t]={selector:e,properties:[]},this};sg.css=function(e,t){var r=this.length-1;if(yr(e))this[r].properties.push({name:e,value:t});else if(mn(e))for(var n=e,i=Object.keys(n),a=0;a{"use strict";s((function(t,r){typeof tC=="object"&&typeof XF=="object"?XF.exports=r():typeof define=="function"&&define.amd?define([],r):typeof tC=="object"?tC.layoutBase=r():t.layoutBase=r()}),"webpackUniversalModuleDefinition")(tC,function(){return(function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return s(r,"__webpack_require__"),r.m=e,r.c=t,r.i=function(n){return n},r.d=function(n,i,a){r.o(n,i)||Object.defineProperty(n,i,{configurable:!1,enumerable:!0,get:a})},r.n=function(n){var i=n&&n.__esModule?s(function(){return n.default},"getDefault"):s(function(){return n},"getModuleExports");return r.d(i,"a",i),i},r.o=function(n,i){return Object.prototype.hasOwnProperty.call(n,i)},r.p="",r(r.s=26)})([(function(e,t,r){"use strict";function n(){}s(n,"LayoutConstants"),n.QUALITY=1,n.DEFAULT_CREATE_BENDS_AS_NEEDED=!1,n.DEFAULT_INCREMENTAL=!1,n.DEFAULT_ANIMATION_ON_LAYOUT=!0,n.DEFAULT_ANIMATION_DURING_LAYOUT=!1,n.DEFAULT_ANIMATION_PERIOD=50,n.DEFAULT_UNIFORM_LEAF_NODE_SIZES=!1,n.DEFAULT_GRAPH_MARGIN=15,n.NODE_DIMENSIONS_INCLUDE_LABELS=!1,n.SIMPLE_NODE_SIZE=40,n.SIMPLE_NODE_HALF_SIZE=n.SIMPLE_NODE_SIZE/2,n.EMPTY_COMPOUND_NODE_SIZE=40,n.MIN_EDGE_LENGTH=1,n.WORLD_BOUNDARY=1e6,n.INITIAL_WORLD_BOUNDARY=n.WORLD_BOUNDARY/1e3,n.WORLD_CENTER_X=1200,n.WORLD_CENTER_Y=900,e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(8),a=r(9);function o(u,h,d){n.call(this,d),this.isOverlapingSourceAndTarget=!1,this.vGraphObject=d,this.bendpoints=[],this.source=u,this.target=h}s(o,"LEdge"),o.prototype=Object.create(n.prototype);for(var l in n)o[l]=n[l];o.prototype.getSource=function(){return this.source},o.prototype.getTarget=function(){return this.target},o.prototype.isInterGraph=function(){return this.isInterGraph},o.prototype.getLength=function(){return this.length},o.prototype.isOverlapingSourceAndTarget=function(){return this.isOverlapingSourceAndTarget},o.prototype.getBendpoints=function(){return this.bendpoints},o.prototype.getLca=function(){return this.lca},o.prototype.getSourceInLca=function(){return this.sourceInLca},o.prototype.getTargetInLca=function(){return this.targetInLca},o.prototype.getOtherEnd=function(u){if(this.source===u)return this.target;if(this.target===u)return this.source;throw"Node is not incident with this edge"},o.prototype.getOtherEndInGraph=function(u,h){for(var d=this.getOtherEnd(u),f=h.getGraphManager().getRoot();;){if(d.getOwner()==h)return d;if(d.getOwner()==f)break;d=d.getOwner().getParent()}return null},o.prototype.updateLength=function(){var u=new Array(4);this.isOverlapingSourceAndTarget=i.getIntersection(this.target.getRect(),this.source.getRect(),u),this.isOverlapingSourceAndTarget||(this.lengthX=u[0]-u[2],this.lengthY=u[1]-u[3],Math.abs(this.lengthX)<1&&(this.lengthX=a.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=a.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY))},o.prototype.updateLengthSimple=function(){this.lengthX=this.target.getCenterX()-this.source.getCenterX(),this.lengthY=this.target.getCenterY()-this.source.getCenterY(),Math.abs(this.lengthX)<1&&(this.lengthX=a.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=a.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY)},e.exports=o}),(function(e,t,r){"use strict";function n(i){this.vGraphObject=i}s(n,"LGraphObject"),e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(10),a=r(13),o=r(0),l=r(16),u=r(4);function h(f,p,m,g){m==null&&g==null&&(g=p),n.call(this,g),f.graphManager!=null&&(f=f.graphManager),this.estimatedSize=i.MIN_VALUE,this.inclusionTreeDepth=i.MAX_VALUE,this.vGraphObject=g,this.edges=[],this.graphManager=f,m!=null&&p!=null?this.rect=new a(p.x,p.y,m.width,m.height):this.rect=new a}s(h,"LNode"),h.prototype=Object.create(n.prototype);for(var d in n)h[d]=n[d];h.prototype.getEdges=function(){return this.edges},h.prototype.getChild=function(){return this.child},h.prototype.getOwner=function(){return this.owner},h.prototype.getWidth=function(){return this.rect.width},h.prototype.setWidth=function(f){this.rect.width=f},h.prototype.getHeight=function(){return this.rect.height},h.prototype.setHeight=function(f){this.rect.height=f},h.prototype.getCenterX=function(){return this.rect.x+this.rect.width/2},h.prototype.getCenterY=function(){return this.rect.y+this.rect.height/2},h.prototype.getCenter=function(){return new u(this.rect.x+this.rect.width/2,this.rect.y+this.rect.height/2)},h.prototype.getLocation=function(){return new u(this.rect.x,this.rect.y)},h.prototype.getRect=function(){return this.rect},h.prototype.getDiagonal=function(){return Math.sqrt(this.rect.width*this.rect.width+this.rect.height*this.rect.height)},h.prototype.getHalfTheDiagonal=function(){return Math.sqrt(this.rect.height*this.rect.height+this.rect.width*this.rect.width)/2},h.prototype.setRect=function(f,p){this.rect.x=f.x,this.rect.y=f.y,this.rect.width=p.width,this.rect.height=p.height},h.prototype.setCenter=function(f,p){this.rect.x=f-this.rect.width/2,this.rect.y=p-this.rect.height/2},h.prototype.setLocation=function(f,p){this.rect.x=f,this.rect.y=p},h.prototype.moveBy=function(f,p){this.rect.x+=f,this.rect.y+=p},h.prototype.getEdgeListToNode=function(f){var p=[],m,g=this;return g.edges.forEach(function(y){if(y.target==f){if(y.source!=g)throw"Incorrect edge source!";p.push(y)}}),p},h.prototype.getEdgesBetween=function(f){var p=[],m,g=this;return g.edges.forEach(function(y){if(!(y.source==g||y.target==g))throw"Incorrect edge source and/or target";(y.target==f||y.source==f)&&p.push(y)}),p},h.prototype.getNeighborsList=function(){var f=new Set,p=this;return p.edges.forEach(function(m){if(m.source==p)f.add(m.target);else{if(m.target!=p)throw"Incorrect incidency!";f.add(m.source)}}),f},h.prototype.withChildren=function(){var f=new Set,p,m;if(f.add(this),this.child!=null)for(var g=this.child.getNodes(),y=0;yp&&(this.rect.x-=(this.labelWidth-p)/2,this.setWidth(this.labelWidth)),this.labelHeight>m&&(this.labelPos=="center"?this.rect.y-=(this.labelHeight-m)/2:this.labelPos=="top"&&(this.rect.y-=this.labelHeight-m),this.setHeight(this.labelHeight))}}},h.prototype.getInclusionTreeDepth=function(){if(this.inclusionTreeDepth==i.MAX_VALUE)throw"assert failed";return this.inclusionTreeDepth},h.prototype.transform=function(f){var p=this.rect.x;p>o.WORLD_BOUNDARY?p=o.WORLD_BOUNDARY:p<-o.WORLD_BOUNDARY&&(p=-o.WORLD_BOUNDARY);var m=this.rect.y;m>o.WORLD_BOUNDARY?m=o.WORLD_BOUNDARY:m<-o.WORLD_BOUNDARY&&(m=-o.WORLD_BOUNDARY);var g=new u(p,m),y=f.inverseTransformPoint(g);this.setLocation(y.x,y.y)},h.prototype.getLeft=function(){return this.rect.x},h.prototype.getRight=function(){return this.rect.x+this.rect.width},h.prototype.getTop=function(){return this.rect.y},h.prototype.getBottom=function(){return this.rect.y+this.rect.height},h.prototype.getParent=function(){return this.owner==null?null:this.owner.getParent()},e.exports=h}),(function(e,t,r){"use strict";function n(i,a){i==null&&a==null?(this.x=0,this.y=0):(this.x=i,this.y=a)}s(n,"PointD"),n.prototype.getX=function(){return this.x},n.prototype.getY=function(){return this.y},n.prototype.setX=function(i){this.x=i},n.prototype.setY=function(i){this.y=i},n.prototype.getDifference=function(i){return new DimensionD(this.x-i.x,this.y-i.y)},n.prototype.getCopy=function(){return new n(this.x,this.y)},n.prototype.translate=function(i){return this.x+=i.width,this.y+=i.height,this},e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(10),a=r(0),o=r(6),l=r(3),u=r(1),h=r(13),d=r(12),f=r(11);function p(g,y,v){n.call(this,v),this.estimatedSize=i.MIN_VALUE,this.margin=a.DEFAULT_GRAPH_MARGIN,this.edges=[],this.nodes=[],this.isConnected=!1,this.parent=g,y!=null&&y instanceof o?this.graphManager=y:y!=null&&y instanceof Layout&&(this.graphManager=y.graphManager)}s(p,"LGraph"),p.prototype=Object.create(n.prototype);for(var m in n)p[m]=n[m];p.prototype.getNodes=function(){return this.nodes},p.prototype.getEdges=function(){return this.edges},p.prototype.getGraphManager=function(){return this.graphManager},p.prototype.getParent=function(){return this.parent},p.prototype.getLeft=function(){return this.left},p.prototype.getRight=function(){return this.right},p.prototype.getTop=function(){return this.top},p.prototype.getBottom=function(){return this.bottom},p.prototype.isConnected=function(){return this.isConnected},p.prototype.add=function(g,y,v){if(y==null&&v==null){var x=g;if(this.graphManager==null)throw"Graph has no graph mgr!";if(this.getNodes().indexOf(x)>-1)throw"Node already in graph!";return x.owner=this,this.getNodes().push(x),x}else{var b=g;if(!(this.getNodes().indexOf(y)>-1&&this.getNodes().indexOf(v)>-1))throw"Source or target not in graph!";if(!(y.owner==v.owner&&y.owner==this))throw"Both owners must be this graph!";return y.owner!=v.owner?null:(b.source=y,b.target=v,b.isInterGraph=!1,this.getEdges().push(b),y.edges.push(b),v!=y&&v.edges.push(b),b)}},p.prototype.remove=function(g){var y=g;if(g instanceof l){if(y==null)throw"Node is null!";if(!(y.owner!=null&&y.owner==this))throw"Owner graph is invalid!";if(this.graphManager==null)throw"Owner graph manager is invalid!";for(var v=y.edges.slice(),x,b=v.length,T=0;T-1&&S>-1))throw"Source and/or target doesn't know this edge!";x.source.edges.splice(C,1),x.target!=x.source&&x.target.edges.splice(S,1);var w=x.source.owner.getEdges().indexOf(x);if(w==-1)throw"Not in owner's edge list!";x.source.owner.getEdges().splice(w,1)}},p.prototype.updateLeftTop=function(){for(var g=i.MAX_VALUE,y=i.MAX_VALUE,v,x,b,T=this.getNodes(),w=T.length,C=0;Cv&&(g=v),y>x&&(y=x)}return g==i.MAX_VALUE?null:(T[0].getParent().paddingLeft!=null?b=T[0].getParent().paddingLeft:b=this.margin,this.left=y-b,this.top=g-b,new d(this.left,this.top))},p.prototype.updateBounds=function(g){for(var y=i.MAX_VALUE,v=-i.MAX_VALUE,x=i.MAX_VALUE,b=-i.MAX_VALUE,T,w,C,S,k,R=this.nodes,M=R.length,P=0;PT&&(y=T),vC&&(x=C),bT&&(y=T),vC&&(x=C),b=this.nodes.length){var M=0;v.forEach(function(P){P.owner==g&&M++}),M==this.nodes.length&&(this.isConnected=!0)}},e.exports=p}),(function(e,t,r){"use strict";var n,i=r(1);function a(o){n=r(5),this.layout=o,this.graphs=[],this.edges=[]}s(a,"LGraphManager"),a.prototype.addRoot=function(){var o=this.layout.newGraph(),l=this.layout.newNode(null),u=this.add(o,l);return this.setRootGraph(u),this.rootGraph},a.prototype.add=function(o,l,u,h,d){if(u==null&&h==null&&d==null){if(o==null)throw"Graph is null!";if(l==null)throw"Parent node is null!";if(this.graphs.indexOf(o)>-1)throw"Graph already in this graph mgr!";if(this.graphs.push(o),o.parent!=null)throw"Already has a parent!";if(l.child!=null)throw"Already has a child!";return o.parent=l,l.child=o,o}else{d=u,h=l,u=o;var f=h.getOwner(),p=d.getOwner();if(!(f!=null&&f.getGraphManager()==this))throw"Source not in this graph mgr!";if(!(p!=null&&p.getGraphManager()==this))throw"Target not in this graph mgr!";if(f==p)return u.isInterGraph=!1,f.add(u,h,d);if(u.isInterGraph=!0,u.source=h,u.target=d,this.edges.indexOf(u)>-1)throw"Edge already in inter-graph edge list!";if(this.edges.push(u),!(u.source!=null&&u.target!=null))throw"Edge source and/or target is null!";if(!(u.source.edges.indexOf(u)==-1&&u.target.edges.indexOf(u)==-1))throw"Edge already in source and/or target incidency list!";return u.source.edges.push(u),u.target.edges.push(u),u}},a.prototype.remove=function(o){if(o instanceof n){var l=o;if(l.getGraphManager()!=this)throw"Graph not in this graph mgr";if(!(l==this.rootGraph||l.parent!=null&&l.parent.graphManager==this))throw"Invalid parent node!";var u=[];u=u.concat(l.getEdges());for(var h,d=u.length,f=0;f=o.getRight()?l[0]+=Math.min(o.getX()-a.getX(),a.getRight()-o.getRight()):o.getX()<=a.getX()&&o.getRight()>=a.getRight()&&(l[0]+=Math.min(a.getX()-o.getX(),o.getRight()-a.getRight())),a.getY()<=o.getY()&&a.getBottom()>=o.getBottom()?l[1]+=Math.min(o.getY()-a.getY(),a.getBottom()-o.getBottom()):o.getY()<=a.getY()&&o.getBottom()>=a.getBottom()&&(l[1]+=Math.min(a.getY()-o.getY(),o.getBottom()-a.getBottom()));var d=Math.abs((o.getCenterY()-a.getCenterY())/(o.getCenterX()-a.getCenterX()));o.getCenterY()===a.getCenterY()&&o.getCenterX()===a.getCenterX()&&(d=1);var f=d*l[0],p=l[1]/d;l[0]f)return l[0]=u,l[1]=m,l[2]=d,l[3]=R,!1;if(hd)return l[0]=p,l[1]=h,l[2]=S,l[3]=f,!1;if(ud?(l[0]=y,l[1]=v,_=!0):(l[0]=g,l[1]=m,_=!0):I===L&&(u>d?(l[0]=p,l[1]=m,_=!0):(l[0]=x,l[1]=v,_=!0)),-D===L?d>u?(l[2]=k,l[3]=R,A=!0):(l[2]=S,l[3]=C,A=!0):D===L&&(d>u?(l[2]=w,l[3]=C,A=!0):(l[2]=M,l[3]=R,A=!0)),_&&A)return!1;if(u>d?h>f?(B=this.getCardinalDirection(I,L,4),O=this.getCardinalDirection(D,L,2)):(B=this.getCardinalDirection(-I,L,3),O=this.getCardinalDirection(-D,L,1)):h>f?(B=this.getCardinalDirection(-I,L,1),O=this.getCardinalDirection(-D,L,3)):(B=this.getCardinalDirection(I,L,2),O=this.getCardinalDirection(D,L,4)),!_)switch(B){case 1:W=m,$=u+-T/L,l[0]=$,l[1]=W;break;case 2:$=x,W=h+b*L,l[0]=$,l[1]=W;break;case 3:W=v,$=u+T/L,l[0]=$,l[1]=W;break;case 4:$=y,W=h+-b*L,l[0]=$,l[1]=W;break}if(!A)switch(O){case 1:z=C,V=d+-N/L,l[2]=V,l[3]=z;break;case 2:V=M,z=f+P*L,l[2]=V,l[3]=z;break;case 3:z=R,V=d+N/L,l[2]=V,l[3]=z;break;case 4:V=k,z=f+-P*L,l[2]=V,l[3]=z;break}}return!1},i.getCardinalDirection=function(a,o,l){return a>o?l:1+l%4},i.getIntersection=function(a,o,l,u){if(u==null)return this.getIntersection2(a,o,l);var h=a.x,d=a.y,f=o.x,p=o.y,m=l.x,g=l.y,y=u.x,v=u.y,x=void 0,b=void 0,T=void 0,w=void 0,C=void 0,S=void 0,k=void 0,R=void 0,M=void 0;return T=p-d,C=h-f,k=f*d-h*p,w=v-g,S=m-y,R=y*g-m*v,M=T*S-w*C,M===0?null:(x=(C*R-S*k)/M,b=(w*k-T*R)/M,new n(x,b))},i.angleOfVector=function(a,o,l,u){var h=void 0;return a!==l?(h=Math.atan((u-o)/(l-a)),l0?1:i<0?-1:0},n.floor=function(i){return i<0?Math.ceil(i):Math.floor(i)},n.ceil=function(i){return i<0?Math.floor(i):Math.ceil(i)},e.exports=n}),(function(e,t,r){"use strict";function n(){}s(n,"Integer"),n.MAX_VALUE=2147483647,n.MIN_VALUE=-2147483648,e.exports=n}),(function(e,t,r){"use strict";var n=(function(){function h(d,f){for(var p=0;p"u"?"undefined":n(a);return a==null||o!="object"&&o!="function"},e.exports=i}),(function(e,t,r){"use strict";function n(m){if(Array.isArray(m)){for(var g=0,y=Array(m.length);g0&&g;){for(T.push(C[0]);T.length>0&&g;){var S=T[0];T.splice(0,1),b.add(S);for(var k=S.getEdges(),x=0;x-1&&C.splice(N,1)}b=new Set,w=new Map}}return m},p.prototype.createDummyNodesForBendpoints=function(m){for(var g=[],y=m.source,v=this.graphManager.calcLowestCommonAncestor(m.source,m.target),x=0;x0){for(var v=this.edgeToDummyNodes.get(y),x=0;x=0&&g.splice(R,1);var M=w.getNeighborsList();M.forEach(function(_){if(y.indexOf(_)<0){var A=v.get(_),I=A-1;I==1&&S.push(_),v.set(_,I)}})}y=y.concat(S),(g.length==1||g.length==2)&&(x=!0,b=g[0])}return b},p.prototype.setGraphManager=function(m){this.graphManager=m},e.exports=p}),(function(e,t,r){"use strict";function n(){}s(n,"RandomSeed"),n.seed=1,n.x=0,n.nextDouble=function(){return n.x=Math.sin(n.seed++)*1e4,n.x-Math.floor(n.x)},e.exports=n}),(function(e,t,r){"use strict";var n=r(4);function i(a,o){this.lworldOrgX=0,this.lworldOrgY=0,this.ldeviceOrgX=0,this.ldeviceOrgY=0,this.lworldExtX=1,this.lworldExtY=1,this.ldeviceExtX=1,this.ldeviceExtY=1}s(i,"Transform"),i.prototype.getWorldOrgX=function(){return this.lworldOrgX},i.prototype.setWorldOrgX=function(a){this.lworldOrgX=a},i.prototype.getWorldOrgY=function(){return this.lworldOrgY},i.prototype.setWorldOrgY=function(a){this.lworldOrgY=a},i.prototype.getWorldExtX=function(){return this.lworldExtX},i.prototype.setWorldExtX=function(a){this.lworldExtX=a},i.prototype.getWorldExtY=function(){return this.lworldExtY},i.prototype.setWorldExtY=function(a){this.lworldExtY=a},i.prototype.getDeviceOrgX=function(){return this.ldeviceOrgX},i.prototype.setDeviceOrgX=function(a){this.ldeviceOrgX=a},i.prototype.getDeviceOrgY=function(){return this.ldeviceOrgY},i.prototype.setDeviceOrgY=function(a){this.ldeviceOrgY=a},i.prototype.getDeviceExtX=function(){return this.ldeviceExtX},i.prototype.setDeviceExtX=function(a){this.ldeviceExtX=a},i.prototype.getDeviceExtY=function(){return this.ldeviceExtY},i.prototype.setDeviceExtY=function(a){this.ldeviceExtY=a},i.prototype.transformX=function(a){var o=0,l=this.lworldExtX;return l!=0&&(o=this.ldeviceOrgX+(a-this.lworldOrgX)*this.ldeviceExtX/l),o},i.prototype.transformY=function(a){var o=0,l=this.lworldExtY;return l!=0&&(o=this.ldeviceOrgY+(a-this.lworldOrgY)*this.ldeviceExtY/l),o},i.prototype.inverseTransformX=function(a){var o=0,l=this.ldeviceExtX;return l!=0&&(o=this.lworldOrgX+(a-this.ldeviceOrgX)*this.lworldExtX/l),o},i.prototype.inverseTransformY=function(a){var o=0,l=this.ldeviceExtY;return l!=0&&(o=this.lworldOrgY+(a-this.ldeviceOrgY)*this.lworldExtY/l),o},i.prototype.inverseTransformPoint=function(a){var o=new n(this.inverseTransformX(a.x),this.inverseTransformY(a.y));return o},e.exports=i}),(function(e,t,r){"use strict";function n(f){if(Array.isArray(f)){for(var p=0,m=Array(f.length);pa.ADAPTATION_LOWER_NODE_LIMIT&&(this.coolingFactor=Math.max(this.coolingFactor*a.COOLING_ADAPTATION_FACTOR,this.coolingFactor-(f-a.ADAPTATION_LOWER_NODE_LIMIT)/(a.ADAPTATION_UPPER_NODE_LIMIT-a.ADAPTATION_LOWER_NODE_LIMIT)*this.coolingFactor*(1-a.COOLING_ADAPTATION_FACTOR))),this.maxNodeDisplacement=a.MAX_NODE_DISPLACEMENT_INCREMENTAL):(f>a.ADAPTATION_LOWER_NODE_LIMIT?this.coolingFactor=Math.max(a.COOLING_ADAPTATION_FACTOR,1-(f-a.ADAPTATION_LOWER_NODE_LIMIT)/(a.ADAPTATION_UPPER_NODE_LIMIT-a.ADAPTATION_LOWER_NODE_LIMIT)*(1-a.COOLING_ADAPTATION_FACTOR)):this.coolingFactor=1,this.initialCoolingFactor=this.coolingFactor,this.maxNodeDisplacement=a.MAX_NODE_DISPLACEMENT),this.maxIterations=Math.max(this.getAllNodes().length*5,this.maxIterations),this.totalDisplacementThreshold=this.displacementThresholdPerNode*this.getAllNodes().length,this.repulsionRange=this.calcRepulsionRange()},h.prototype.calcSpringForces=function(){for(var f=this.getAllEdges(),p,m=0;m0&&arguments[0]!==void 0?arguments[0]:!0,p=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1,m,g,y,v,x=this.getAllNodes(),b;if(this.useFRGridVariant)for(this.totalIterations%a.GRID_CALCULATION_CHECK_PERIOD==1&&f&&this.updateGrid(),b=new Set,m=0;mT||b>T)&&(f.gravitationForceX=-this.gravityConstant*y,f.gravitationForceY=-this.gravityConstant*v)):(T=p.getEstimatedSize()*this.compoundGravityRangeFactor,(x>T||b>T)&&(f.gravitationForceX=-this.gravityConstant*y*this.compoundGravityConstant,f.gravitationForceY=-this.gravityConstant*v*this.compoundGravityConstant))},h.prototype.isConverged=function(){var f,p=!1;return this.totalIterations>this.maxIterations/3&&(p=Math.abs(this.totalDisplacement-this.oldTotalDisplacement)<2),f=this.totalDisplacement=x.length||T>=x[0].length)){for(var w=0;wh},"_defaultCompareFunction")}]),l})();e.exports=o}),(function(e,t,r){"use strict";var n=(function(){function o(l,u){for(var h=0;h2&&arguments[2]!==void 0?arguments[2]:1,d=arguments.length>3&&arguments[3]!==void 0?arguments[3]:-1,f=arguments.length>4&&arguments[4]!==void 0?arguments[4]:-1;i(this,o),this.sequence1=l,this.sequence2=u,this.match_score=h,this.mismatch_penalty=d,this.gap_penalty=f,this.iMax=l.length+1,this.jMax=u.length+1,this.grid=new Array(this.iMax);for(var p=0;p=0;l--){var u=this.listeners[l];u.event===a&&u.callback===o&&this.listeners.splice(l,1)}},i.emit=function(a,o){for(var l=0;l{"use strict";s((function(t,r){typeof rC=="object"&&typeof ZF=="object"?ZF.exports=r(KF()):typeof define=="function"&&define.amd?define(["layout-base"],r):typeof rC=="object"?rC.coseBase=r(KF()):t.coseBase=r(t.layoutBase)}),"webpackUniversalModuleDefinition")(rC,function(e){return(function(t){var r={};function n(i){if(r[i])return r[i].exports;var a=r[i]={i,l:!1,exports:{}};return t[i].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return s(n,"__webpack_require__"),n.m=t,n.c=r,n.i=function(i){return i},n.d=function(i,a,o){n.o(i,a)||Object.defineProperty(i,a,{configurable:!1,enumerable:!0,get:o})},n.n=function(i){var a=i&&i.__esModule?s(function(){return i.default},"getDefault"):s(function(){return i},"getModuleExports");return n.d(a,"a",a),a},n.o=function(i,a){return Object.prototype.hasOwnProperty.call(i,a)},n.p="",n(n.s=7)})([(function(t,r){t.exports=e}),(function(t,r,n){"use strict";var i=n(0).FDLayoutConstants;function a(){}s(a,"CoSEConstants");for(var o in i)a[o]=i[o];a.DEFAULT_USE_MULTI_LEVEL_SCALING=!1,a.DEFAULT_RADIAL_SEPARATION=i.DEFAULT_EDGE_LENGTH,a.DEFAULT_COMPONENT_SEPERATION=60,a.TILE=!0,a.TILING_PADDING_VERTICAL=10,a.TILING_PADDING_HORIZONTAL=10,a.TREE_REDUCTION_ON_INCREMENTAL=!1,t.exports=a}),(function(t,r,n){"use strict";var i=n(0).FDLayoutEdge;function a(l,u,h){i.call(this,l,u,h)}s(a,"CoSEEdge"),a.prototype=Object.create(i.prototype);for(var o in i)a[o]=i[o];t.exports=a}),(function(t,r,n){"use strict";var i=n(0).LGraph;function a(l,u,h){i.call(this,l,u,h)}s(a,"CoSEGraph"),a.prototype=Object.create(i.prototype);for(var o in i)a[o]=i[o];t.exports=a}),(function(t,r,n){"use strict";var i=n(0).LGraphManager;function a(l){i.call(this,l)}s(a,"CoSEGraphManager"),a.prototype=Object.create(i.prototype);for(var o in i)a[o]=i[o];t.exports=a}),(function(t,r,n){"use strict";var i=n(0).FDLayoutNode,a=n(0).IMath;function o(u,h,d,f){i.call(this,u,h,d,f)}s(o,"CoSENode"),o.prototype=Object.create(i.prototype);for(var l in i)o[l]=i[l];o.prototype.move=function(){var u=this.graphManager.getLayout();this.displacementX=u.coolingFactor*(this.springForceX+this.repulsionForceX+this.gravitationForceX)/this.noOfChildren,this.displacementY=u.coolingFactor*(this.springForceY+this.repulsionForceY+this.gravitationForceY)/this.noOfChildren,Math.abs(this.displacementX)>u.coolingFactor*u.maxNodeDisplacement&&(this.displacementX=u.coolingFactor*u.maxNodeDisplacement*a.sign(this.displacementX)),Math.abs(this.displacementY)>u.coolingFactor*u.maxNodeDisplacement&&(this.displacementY=u.coolingFactor*u.maxNodeDisplacement*a.sign(this.displacementY)),this.child==null?this.moveBy(this.displacementX,this.displacementY):this.child.getNodes().length==0?this.moveBy(this.displacementX,this.displacementY):this.propogateDisplacementToChildren(this.displacementX,this.displacementY),u.totalDisplacement+=Math.abs(this.displacementX)+Math.abs(this.displacementY),this.springForceX=0,this.springForceY=0,this.repulsionForceX=0,this.repulsionForceY=0,this.gravitationForceX=0,this.gravitationForceY=0,this.displacementX=0,this.displacementY=0},o.prototype.propogateDisplacementToChildren=function(u,h){for(var d=this.getChild().getNodes(),f,p=0;p0)this.positionNodesRadially(C);else{this.reduceTrees(),this.graphManager.resetAllNodesToApplyGravitation();var S=new Set(this.getAllNodes()),k=this.nodesWithGravity.filter(function(R){return S.has(R)});this.graphManager.setAllNodesToApplyGravitation(k),this.positionNodesRandomly()}}return this.initSpringEmbedder(),this.runSpringEmbedder(),!0},T.prototype.tick=function(){if(this.totalIterations++,this.totalIterations===this.maxIterations&&!this.isTreeGrowing&&!this.isGrowthFinished)if(this.prunedNodesAll.length>0)this.isTreeGrowing=!0;else return!0;if(this.totalIterations%d.CONVERGENCE_CHECK_PERIOD==0&&!this.isTreeGrowing&&!this.isGrowthFinished){if(this.isConverged())if(this.prunedNodesAll.length>0)this.isTreeGrowing=!0;else return!0;this.coolingCycle++,this.layoutQuality==0?this.coolingAdjuster=this.coolingCycle:this.layoutQuality==1&&(this.coolingAdjuster=this.coolingCycle/3),this.coolingFactor=Math.max(this.initialCoolingFactor-Math.pow(this.coolingCycle,Math.log(100*(this.initialCoolingFactor-this.finalTemperature))/Math.log(this.maxCoolingCycle))/100*this.coolingAdjuster,this.finalTemperature),this.animationPeriod=Math.ceil(this.initialAnimationPeriod*Math.sqrt(this.coolingFactor))}if(this.isTreeGrowing){if(this.growTreeIterations%10==0)if(this.prunedNodesAll.length>0){this.graphManager.updateBounds(),this.updateGrid(),this.growTree(this.prunedNodesAll),this.graphManager.resetAllNodesToApplyGravitation();var C=new Set(this.getAllNodes()),S=this.nodesWithGravity.filter(function(M){return C.has(M)});this.graphManager.setAllNodesToApplyGravitation(S),this.graphManager.updateBounds(),this.updateGrid(),this.coolingFactor=d.DEFAULT_COOLING_FACTOR_INCREMENTAL}else this.isTreeGrowing=!1,this.isGrowthFinished=!0;this.growTreeIterations++}if(this.isGrowthFinished){if(this.isConverged())return!0;this.afterGrowthIterations%10==0&&(this.graphManager.updateBounds(),this.updateGrid()),this.coolingFactor=d.DEFAULT_COOLING_FACTOR_INCREMENTAL*((100-this.afterGrowthIterations)/100),this.afterGrowthIterations++}var k=!this.isTreeGrowing&&!this.isGrowthFinished,R=this.growTreeIterations%10==1&&this.isTreeGrowing||this.afterGrowthIterations%10==1&&this.isGrowthFinished;return this.totalDisplacement=0,this.graphManager.updateBounds(),this.calcSpringForces(),this.calcRepulsionForces(k,R),this.calcGravitationalForces(),this.moveNodes(),this.animate(),!1},T.prototype.getPositionsData=function(){for(var C=this.graphManager.getAllNodes(),S={},k=0;k1){var _;for(_=0;_R&&(R=Math.floor(N.y)),P=Math.floor(N.x+h.DEFAULT_COMPONENT_SEPERATION)}this.transform(new m(f.WORLD_CENTER_X-N.x/2,f.WORLD_CENTER_Y-N.y/2))},T.radialLayout=function(C,S,k){var R=Math.max(this.maxDiagonalInTree(C),h.DEFAULT_RADIAL_SEPARATION);T.branchRadialLayout(S,null,0,359,0,R);var M=x.calculateBounds(C),P=new b;P.setDeviceOrgX(M.getMinX()),P.setDeviceOrgY(M.getMinY()),P.setWorldOrgX(k.x),P.setWorldOrgY(k.y);for(var N=0;N1;){var H=z[0];z.splice(0,1);var K=B.indexOf(H);K>=0&&B.splice(K,1),W--,O--}S!=null?V=(B.indexOf(z[0])+1)%W:V=0;for(var J=Math.abs(R-k)/O,ne=V;$!=O;ne=++ne%W){var Z=B[ne].getOtherEnd(C);if(Z!=S){var pe=(k+$*J)%360,te=(pe+J)%360;T.branchRadialLayout(Z,C,pe,te,M+P,P),$++}}},T.maxDiagonalInTree=function(C){for(var S=y.MIN_VALUE,k=0;kS&&(S=M)}return S},T.prototype.calcRepulsionRange=function(){return 2*(this.level+1)*this.idealEdgeLength},T.prototype.groupZeroDegreeMembers=function(){var C=this,S={};this.memberGroups={},this.idToDummyNode={};for(var k=[],R=this.graphManager.getAllNodes(),M=0;M"u"&&(S[_]=[]),S[_]=S[_].concat(P)}Object.keys(S).forEach(function(A){if(S[A].length>1){var I="DummyCompound_"+A;C.memberGroups[I]=S[A];var D=S[A][0].getParent(),L=new l(C.graphManager);L.id=I,L.paddingLeft=D.paddingLeft||0,L.paddingRight=D.paddingRight||0,L.paddingBottom=D.paddingBottom||0,L.paddingTop=D.paddingTop||0,C.idToDummyNode[I]=L;var B=C.getGraphManager().add(C.newGraph(),L),O=D.getChild();O.add(L);for(var $=0;$=0;C--){var S=this.compoundOrder[C],k=S.id,R=S.paddingLeft,M=S.paddingTop;this.adjustLocations(this.tiledMemberPack[k],S.rect.x,S.rect.y,R,M)}},T.prototype.repopulateZeroDegreeMembers=function(){var C=this,S=this.tiledZeroDegreePack;Object.keys(S).forEach(function(k){var R=C.idToDummyNode[k],M=R.paddingLeft,P=R.paddingTop;C.adjustLocations(S[k],R.rect.x,R.rect.y,M,P)})},T.prototype.getToBeTiled=function(C){var S=C.id;if(this.toBeTiled[S]!=null)return this.toBeTiled[S];var k=C.getChild();if(k==null)return this.toBeTiled[S]=!1,!1;for(var R=k.getNodes(),M=0;M0)return this.toBeTiled[S]=!1,!1;if(P.getChild()==null){this.toBeTiled[P.id]=!1;continue}if(!this.getToBeTiled(P))return this.toBeTiled[S]=!1,!1}return this.toBeTiled[S]=!0,!0},T.prototype.getNodeDegree=function(C){for(var S=C.id,k=C.getEdges(),R=0,M=0;MA&&(A=D.rect.height)}k+=A+C.verticalPadding}},T.prototype.tileCompoundMembers=function(C,S){var k=this;this.tiledMemberPack=[],Object.keys(C).forEach(function(R){var M=S[R];k.tiledMemberPack[R]=k.tileNodes(C[R],M.paddingLeft+M.paddingRight),M.rect.width=k.tiledMemberPack[R].width,M.rect.height=k.tiledMemberPack[R].height})},T.prototype.tileNodes=function(C,S){var k=h.TILING_PADDING_VERTICAL,R=h.TILING_PADDING_HORIZONTAL,M={rows:[],rowWidth:[],rowHeight:[],width:0,height:S,verticalPadding:k,horizontalPadding:R};C.sort(function(_,A){return _.rect.width*_.rect.height>A.rect.width*A.rect.height?-1:_.rect.width*_.rect.height0&&(N+=C.horizontalPadding),C.rowWidth[k]=N,C.width0&&(_+=C.verticalPadding);var A=0;_>C.rowHeight[k]&&(A=C.rowHeight[k],C.rowHeight[k]=_,A=C.rowHeight[k]-A),C.height+=A,C.rows[k].push(S)},T.prototype.getShortestRowIndex=function(C){for(var S=-1,k=Number.MAX_VALUE,R=0;Rk&&(S=R,k=C.rowWidth[R]);return S},T.prototype.canAddHorizontal=function(C,S,k){var R=this.getShortestRowIndex(C);if(R<0)return!0;var M=C.rowWidth[R];if(M+C.horizontalPadding+S<=C.width)return!0;var P=0;C.rowHeight[R]0&&(P=k+C.verticalPadding-C.rowHeight[R]);var N;C.width-M>=S+C.horizontalPadding?N=(C.height+P)/(M+S+C.horizontalPadding):N=(C.height+P)/C.width,P=k+C.verticalPadding;var _;return C.widthP&&S!=k){R.splice(-1,1),C.rows[k].push(M),C.rowWidth[S]=C.rowWidth[S]-P,C.rowWidth[k]=C.rowWidth[k]+P,C.width=C.rowWidth[instance.getLongestRowIndex(C)];for(var N=Number.MIN_VALUE,_=0;_N&&(N=R[_].height);S>0&&(N+=C.verticalPadding);var A=C.rowHeight[S]+C.rowHeight[k];C.rowHeight[S]=N,C.rowHeight[k]0)for(var O=M;O<=P;O++)B[0]+=this.grid[O][N-1].length+this.grid[O][N].length-1;if(P0)for(var O=N;O<=_;O++)B[3]+=this.grid[M-1][O].length+this.grid[M][O].length-1;for(var $=y.MAX_VALUE,W,V,z=0;z{"use strict";s((function(t,r){typeof nC=="object"&&typeof JF=="object"?JF.exports=r(QF()):typeof define=="function"&&define.amd?define(["cose-base"],r):typeof nC=="object"?nC.cytoscapeCoseBilkent=r(QF()):t.cytoscapeCoseBilkent=r(t.coseBase)}),"webpackUniversalModuleDefinition")(nC,function(e){return(function(t){var r={};function n(i){if(r[i])return r[i].exports;var a=r[i]={i,l:!1,exports:{}};return t[i].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return s(n,"__webpack_require__"),n.m=t,n.c=r,n.i=function(i){return i},n.d=function(i,a,o){n.o(i,a)||Object.defineProperty(i,a,{configurable:!1,enumerable:!0,get:o})},n.n=function(i){var a=i&&i.__esModule?s(function(){return i.default},"getDefault"):s(function(){return i},"getModuleExports");return n.d(a,"a",a),a},n.o=function(i,a){return Object.prototype.hasOwnProperty.call(i,a)},n.p="",n(n.s=1)})([(function(t,r){t.exports=e}),(function(t,r,n){"use strict";var i=n(0).layoutBase.LayoutConstants,a=n(0).layoutBase.FDLayoutConstants,o=n(0).CoSEConstants,l=n(0).CoSELayout,u=n(0).CoSENode,h=n(0).layoutBase.PointD,d=n(0).layoutBase.DimensionD,f={ready:s(function(){},"ready"),stop:s(function(){},"stop"),quality:"default",nodeDimensionsIncludeLabels:!1,refresh:30,fit:!0,padding:10,randomize:!0,nodeRepulsion:4500,idealEdgeLength:50,edgeElasticity:.45,nestingFactor:.1,gravity:.25,numIter:2500,tile:!0,animate:"end",animationDuration:500,tilingPaddingVertical:10,tilingPaddingHorizontal:10,gravityRangeCompound:1.5,gravityCompound:1,gravityRange:3.8,initialEnergyOnIncremental:.5};function p(v,x){var b={};for(var T in v)b[T]=v[T];for(var T in x)b[T]=x[T];return b}s(p,"extend");function m(v){this.options=p(f,v),g(this.options)}s(m,"_CoSELayout");var g=s(function(x){x.nodeRepulsion!=null&&(o.DEFAULT_REPULSION_STRENGTH=a.DEFAULT_REPULSION_STRENGTH=x.nodeRepulsion),x.idealEdgeLength!=null&&(o.DEFAULT_EDGE_LENGTH=a.DEFAULT_EDGE_LENGTH=x.idealEdgeLength),x.edgeElasticity!=null&&(o.DEFAULT_SPRING_STRENGTH=a.DEFAULT_SPRING_STRENGTH=x.edgeElasticity),x.nestingFactor!=null&&(o.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=a.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=x.nestingFactor),x.gravity!=null&&(o.DEFAULT_GRAVITY_STRENGTH=a.DEFAULT_GRAVITY_STRENGTH=x.gravity),x.numIter!=null&&(o.MAX_ITERATIONS=a.MAX_ITERATIONS=x.numIter),x.gravityRange!=null&&(o.DEFAULT_GRAVITY_RANGE_FACTOR=a.DEFAULT_GRAVITY_RANGE_FACTOR=x.gravityRange),x.gravityCompound!=null&&(o.DEFAULT_COMPOUND_GRAVITY_STRENGTH=a.DEFAULT_COMPOUND_GRAVITY_STRENGTH=x.gravityCompound),x.gravityRangeCompound!=null&&(o.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=a.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=x.gravityRangeCompound),x.initialEnergyOnIncremental!=null&&(o.DEFAULT_COOLING_FACTOR_INCREMENTAL=a.DEFAULT_COOLING_FACTOR_INCREMENTAL=x.initialEnergyOnIncremental),x.quality=="draft"?i.QUALITY=0:x.quality=="proof"?i.QUALITY=2:i.QUALITY=1,o.NODE_DIMENSIONS_INCLUDE_LABELS=a.NODE_DIMENSIONS_INCLUDE_LABELS=i.NODE_DIMENSIONS_INCLUDE_LABELS=x.nodeDimensionsIncludeLabels,o.DEFAULT_INCREMENTAL=a.DEFAULT_INCREMENTAL=i.DEFAULT_INCREMENTAL=!x.randomize,o.ANIMATE=a.ANIMATE=i.ANIMATE=x.animate,o.TILE=x.tile,o.TILING_PADDING_VERTICAL=typeof x.tilingPaddingVertical=="function"?x.tilingPaddingVertical.call():x.tilingPaddingVertical,o.TILING_PADDING_HORIZONTAL=typeof x.tilingPaddingHorizontal=="function"?x.tilingPaddingHorizontal.call():x.tilingPaddingHorizontal},"getUserOptions");m.prototype.run=function(){var v,x,b=this.options,T=this.idToLNode={},w=this.layout=new l,C=this;C.stopped=!1,this.cy=this.options.cy,this.cy.trigger({type:"layoutstart",layout:this});var S=w.newGraphManager();this.gm=S;var k=this.options.eles.nodes(),R=this.options.eles.edges();this.root=S.addRoot(),this.processChildrenList(this.root,this.getTopMostNodes(k),w);for(var M=0;M0){var _;_=b.getGraphManager().add(b.newGraph(),k),this.processChildrenList(_,S,b)}}},m.prototype.stop=function(){return this.stopped=!0,this};var y=s(function(x){x("layout","cose-bilkent",m)},"register");typeof cytoscape<"u"&&y(cytoscape),t.exports=y})])})});function jmt(e,t){e.forEach(r=>{let n={id:r.id,labelText:r.label,height:r.height,width:r.width,padding:r.padding??0};Object.keys(r).forEach(i=>{["id","label","height","width","padding","x","y"].includes(i)||(n[i]=r[i])}),t.add({group:"nodes",data:n,position:{x:r.x??0,y:r.y??0}})})}function Xmt(e,t){e.forEach(r=>{let n={id:r.id,source:r.start,target:r.end};Object.keys(r).forEach(i=>{["id","start","end"].includes(i)||(n[i]=r[i])}),t.add({group:"edges",data:n})})}function YTe(e){return new Promise(t=>{let r=ut("body").append("div").attr("id","cy").attr("style","display:none"),n=Ll({container:document.getElementById("cy"),style:[{selector:"edge",style:{"curve-style":"bezier"}}]});r.remove(),jmt(e.nodes,n),Xmt(e.edges,n),n.nodes().forEach(function(a){a.layoutDimensions=()=>{let o=a.data();return{w:o.width,h:o.height}}});let i={name:"cose-bilkent",quality:"proof",styleEnabled:!1,animate:!1};n.layout(i).run(),n.ready(a=>{se.info("Cytoscape ready",a),t(n)})})}function jTe(e){return e.nodes().map(t=>{let r=t.data(),n=t.position(),i={id:r.id,x:n.x,y:n.y};return Object.keys(r).forEach(a=>{a!=="id"&&(i[a]=r[a])}),i})}function XTe(e){return e.edges().map(t=>{let r=t.data(),n=t._private.rscratch,i={id:r.id,source:r.source,target:r.target,startX:n.startX,startY:n.startY,midX:n.midX,midY:n.midY,endX:n.endX,endY:n.endY};return Object.keys(r).forEach(a=>{["id","source","target"].includes(a)||(i[a]=r[a])}),i})}var UTe,KTe=F(()=>{"use strict";jF();UTe=Rs(HTe(),1);Wr();St();Ll.use(UTe.default);s(jmt,"addNodes");s(Xmt,"addEdges");s(YTe,"createCytoscapeInstance");s(jTe,"extractPositionedNodes");s(XTe,"extractPositionedEdges")});async function ZTe(e,t){se.debug("Starting cose-bilkent layout algorithm");try{Kmt(e);let r=await YTe(e),n=jTe(r),i=XTe(r);return se.debug(`Layout completed: ${n.length} nodes, ${i.length} edges`),{nodes:n,edges:i}}catch(r){throw se.error("Error in cose-bilkent layout algorithm:",r),r}}function Kmt(e){if(!e)throw new Error("Layout data is required");if(!e.config)throw new Error("Configuration is required in layout data");if(!e.rootNode)throw new Error("Root node is required");if(!e.nodes||!Array.isArray(e.nodes))throw new Error("No nodes found in layout data");if(!Array.isArray(e.edges))throw new Error("Edges array is required in layout data");return!0}var QTe=F(()=>{"use strict";St();KTe();s(ZTe,"executeCoseBilkentLayout");s(Kmt,"validateLayoutData")});var JTe,eCe=F(()=>{"use strict";QTe();JTe=s(async(e,t,{insertCluster:r,insertEdge:n,insertEdgeLabel:i,insertMarkers:a,insertNode:o,log:l,positionEdgeLabel:u},{algorithm:h})=>{let d={},f={},p=t.select("g");a(p,e.markers,e.type,e.diagramId);let m=p.insert("g").attr("class","subgraphs"),g=p.insert("g").attr("class","edgePaths"),y=p.insert("g").attr("class","edgeLabels"),v=p.insert("g").attr("class","nodes");l.debug("Inserting nodes into DOM for dimension calculation"),await Promise.all(e.nodes.map(async T=>{if(T.isGroup){let w={...T};f[T.id]=w,d[T.id]=w,await r(m,T)}else{let w={...T};d[T.id]=w;let C=await o(v,T,{config:e.config,dir:e.direction||"TB"}),S=C.node().getBBox();w.width=S.width,w.height=S.height,w.domId=C,l.debug(`Node ${T.id} dimensions: ${S.width}x${S.height}`)}})),l.debug("Running cose-bilkent layout algorithm");let x={...e,nodes:e.nodes.map(T=>{let w=d[T.id];return{...T,width:w.width,height:w.height}})},b=await ZTe(x,e.config);l.debug("Positioning nodes based on layout results"),b.nodes.forEach(T=>{let w=d[T.id];w?.domId&&(w.domId.attr("transform",`translate(${T.x}, ${T.y})`),w.x=T.x,w.y=T.y,l.debug(`Positioned node ${w.id} at center (${T.x}, ${T.y})`))}),b.edges.forEach(T=>{let w=e.edges.find(C=>C.id===T.id);w&&(w.points=[{x:T.startX,y:T.startY},{x:T.midX,y:T.midY},{x:T.endX,y:T.endY}])}),l.debug("Inserting and positioning edges"),await Promise.all(e.edges.map(async T=>{let w=await i(y,T),C=d[T.start??""],S=d[T.end??""];if(C&&S){let k=b.edges.find(R=>R.id===T.id);if(k){l.debug("APA01 positionedEdge",k);let R={...T},M=n(g,R,f,e.type,C,S,e.diagramId);u(R,M)}else{let R={...T,points:[{x:C.x||0,y:C.y||0},{x:S.x||0,y:S.y||0}]},M=n(g,R,f,e.type,C,S,e.diagramId);u(R,M)}}})),l.debug("Cose-bilkent rendering completed")},"render")});var tCe={};sr(tCe,{render:()=>Zmt});var Zmt,rCe=F(()=>{"use strict";eCe();Zmt=JTe});var iC,eG,Qmt,Dl,Au,ep=F(()=>{"use strict";zue();St();iC={},eG=s(e=>{for(let t of e)iC[t.name]=t},"registerLayoutLoaders"),Qmt=s(()=>{eG([{name:"dagre",loader:s(async()=>await Promise.resolve().then(()=>(Cye(),Tye)),"loader")},{name:"swimlane",loader:s(async()=>await Promise.resolve().then(()=>(wve(),Sve)),"loader")},{name:"cose-bilkent",loader:s(async()=>await Promise.resolve().then(()=>(rCe(),tCe)),"loader")}])},"registerDefaultLayoutLoaders");Qmt();Dl=s(async(e,t)=>{if(!(e.layoutAlgorithm in iC))throw new Error(`Unknown layout algorithm: ${e.layoutAlgorithm}`);if(e.diagramId)for(let d of e.nodes){let f=d.domId||d.id;d.domId=`${e.diagramId}-${f}`}let r=iC[e.layoutAlgorithm],n=await r.loader(),{theme:i,themeVariables:a}=e.config,{useGradient:o,gradientStart:l,gradientStop:u}=a,h=t.attr("id");if(t.append("defs").append("filter").attr("id",`${h}-drop-shadow`).attr("height","130%").attr("width","130%").append("feDropShadow").attr("dx","4").attr("dy","4").attr("stdDeviation",0).attr("flood-opacity","0.06").attr("flood-color",`${i?.includes("dark")?"#FFFFFF":"#000000"}`),t.append("defs").append("filter").attr("id",`${h}-drop-shadow-small`).attr("height","150%").attr("width","150%").append("feDropShadow").attr("dx","2").attr("dy","2").attr("stdDeviation",0).attr("flood-opacity","0.06").attr("flood-color",`${i?.includes("dark")?"#FFFFFF":"#000000"}`),o){let d=t.append("linearGradient").attr("id",t.attr("id")+"-gradient").attr("gradientUnits","objectBoundingBox").attr("x1","0%").attr("y1","0%").attr("x2","100%").attr("y2","0%");d.append("svg:stop").attr("offset","0%").attr("stop-color",l).attr("stop-opacity",1),d.append("svg:stop").attr("offset","100%").attr("stop-color",u).attr("stop-opacity",1)}return n.render(e,t,Gue,{algorithm:r.algorithm})},"render"),Au=s((e="",{fallback:t="dagre"}={})=>{if(e in iC)return e;if(t in iC)return se.warn(`Layout algorithm ${e} is not registered. Using ${t} as fallback.`),t;throw new Error(`Both layout algorithms ${e} and ${t} are not registered.`)},"getRegisteredLayoutAlgorithm")});var Co,Jmt,egt,tp=F(()=>{"use strict";Vn();St();Co=s((e,t,r,n)=>{e.attr("class",r);let{width:i,height:a,x:o,y:l}=Jmt(e,t);jr(e,a,i,n);let u=egt(o,l,i,a,t);e.attr("viewBox",u),se.debug(`viewBox configured: ${u} with padding: ${t}`)},"setupViewPortForSVG"),Jmt=s((e,t)=>{let r=e.node()?.getBBox()||{width:0,height:0,x:0,y:0};return{width:r.width+t*2,height:r.height+t*2,x:r.x,y:r.y}},"calculateDimensionsWithPadding"),egt=s((e,t,r,n,i)=>`${e-i} ${t-i} ${r} ${n}`,"createViewBox")});var tgt,rgt,nCe,iCe=F(()=>{"use strict";Jt();St();Rm();ep();tp();er();tgt=s(function(e,t){return t.db.getClasses()},"getClasses"),rgt=s(async function(e,t,r,n){se.info("REF0:"),se.info("Drawing state diagram (v2)",t);let{securityLevel:i,flowchart:a,layout:o}=Pe();n.db.setDiagramId(t),se.debug("Before getData: ");let l=n.db.getData();se.debug("Data: ",l);let u=xl(t,i),h=n.db.getDirection();l.type=n.type,l.layoutAlgorithm=Au(o),l.layoutAlgorithm==="dagre"&&o==="elk"&&se.warn("flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](https://github.com/mermaid-js/mermaid/releases/tag/v11.0.0) for more details. This diagram will be rendered using `dagre` layout as a fallback."),l.direction=h,l.nodeSpacing=a?.nodeSpacing||50,l.rankSpacing=a?.rankSpacing||50,l.markers=["point","circle","cross"],l.diagramId=t,se.debug("REF1:",l),await Dl(l,u);let d=l.config.flowchart?.diagramPadding??8;or.insertTitle(u,"flowchartTitleText",a?.titleTopMargin||0,n.db.getDiagramTitle()),Co(u,d,"flowchart",a?.useMaxWidth||!1)},"draw"),nCe={getClasses:tgt,draw:rgt}});var tG,rG,aCe=F(()=>{"use strict";tG=(function(){var e=s(function(Pt,Et,Ot,zt){for(Ot=Ot||{},zt=Pt.length;zt--;Ot[Pt[zt]]=Et);return Ot},"o"),t=[1,4],r=[1,3],n=[1,5],i=[1,8,9,10,11,27,34,36,38,44,60,84,85,86,87,88,89,102,105,106,109,111,114,115,116,121,122,123,124,125],a=[2,2],o=[1,13],l=[1,14],u=[1,15],h=[1,16],d=[1,23],f=[1,25],p=[1,26],m=[1,27],g=[1,50],y=[1,49],v=[1,29],x=[1,30],b=[1,31],T=[1,32],w=[1,33],C=[1,45],S=[1,47],k=[1,43],R=[1,48],M=[1,44],P=[1,51],N=[1,46],_=[1,52],A=[1,53],I=[1,34],D=[1,35],L=[1,36],B=[1,37],O=[1,38],$=[1,58],W=[1,8,9,10,11,27,32,34,36,38,44,60,84,85,86,87,88,89,102,105,106,109,111,114,115,116,121,122,123,124,125],V=[1,62],z=[1,61],H=[1,63],K=[8,9,11,75,77,78],J=[1,79],ne=[1,92],Z=[1,97],pe=[1,96],te=[1,93],fe=[1,89],ce=[1,95],de=[1,91],Ee=[1,98],we=[1,94],Fe=[1,99],Ye=[1,90],He=[8,9,10,11,40,75,77,78],ye=[8,9,10,11,40,46,75,77,78],Be=[8,9,10,11,29,40,44,46,48,50,52,54,56,58,60,63,65,67,68,70,75,77,78,89,102,105,106,109,111,114,115,116],Ie=[8,9,11,44,60,75,77,78,89,102,105,106,109,111,114,115,116],Ne=[44,60,89,102,105,106,109,111,114,115,116],re=[1,122],ue=[1,123],le=[1,125],he=[1,124],Te=[44,60,62,74,89,102,105,106,109,111,114,115,116],oe=[1,134],G=[1,148],X=[1,149],ee=[1,150],q=[1,151],ie=[1,136],j=[1,138],U=[1,142],Y=[1,143],Ae=[1,144],me=[1,145],Se=[1,146],Ce=[1,147],Me=[1,152],_e=[1,153],ze=[1,132],Le=[1,133],qe=[1,140],ve=[1,135],lt=[1,139],Oe=[1,137],mt=[8,9,10,11,27,32,34,36,38,44,60,84,85,86,87,88,89,102,105,106,109,111,114,115,116,121,122,123,124,125],At=[1,155],Gt=[1,157],Ct=[8,9,11],Rt=[8,9,10,11,14,44,60,89,105,106,109,111,114,115,116],gt=[1,177],be=[1,173],ct=[1,174],xt=[1,178],Ke=[1,175],De=[1,176],tt=[77,116,119],Ze=[8,9,10,11,12,14,27,29,32,44,60,75,84,85,86,87,88,89,90,105,109,111,114,115,116],dt=[10,106],Ut=[31,49,51,53,55,57,62,64,66,67,69,71,116,117,118],$t=[1,248],Yt=[1,246],Sr=[1,250],lr=[1,244],Wt=[1,245],ar=[1,247],et=[1,249],pt=[1,251],kt=[1,269],vr=[8,9,11,106],tr=[8,9,10,11,60,84,105,106,109,110,111,112],Ur={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,graphConfig:4,document:5,line:6,statement:7,SEMI:8,NEWLINE:9,SPACE:10,EOF:11,GRAPH:12,NODIR:13,DIR:14,FirstStmtSeparator:15,ending:16,endToken:17,spaceList:18,spaceListNewline:19,vertexStatement:20,separator:21,styleStatement:22,linkStyleStatement:23,classDefStatement:24,classStatement:25,clickStatement:26,subgraph:27,textNoTags:28,SQS:29,text:30,SQE:31,end:32,direction:33,acc_title:34,acc_title_value:35,acc_descr:36,acc_descr_value:37,acc_descr_multiline_value:38,shapeData:39,SHAPE_DATA:40,link:41,node:42,styledVertex:43,AMP:44,vertex:45,STYLE_SEPARATOR:46,idString:47,DOUBLECIRCLESTART:48,DOUBLECIRCLEEND:49,PS:50,PE:51,"(-":52,"-)":53,STADIUMSTART:54,STADIUMEND:55,SUBROUTINESTART:56,SUBROUTINEEND:57,VERTEX_WITH_PROPS_START:58,"NODE_STRING[field]":59,COLON:60,"NODE_STRING[value]":61,PIPE:62,CYLINDERSTART:63,CYLINDEREND:64,DIAMOND_START:65,DIAMOND_STOP:66,TAGEND:67,TRAPSTART:68,TRAPEND:69,INVTRAPSTART:70,INVTRAPEND:71,linkStatement:72,arrowText:73,TESTSTR:74,START_LINK:75,edgeText:76,LINK:77,LINK_ID:78,edgeTextToken:79,STR:80,MD_STR:81,textToken:82,keywords:83,STYLE:84,LINKSTYLE:85,CLASSDEF:86,CLASS:87,CLICK:88,DOWN:89,UP:90,textNoTagsToken:91,stylesOpt:92,"idString[vertex]":93,"idString[class]":94,CALLBACKNAME:95,CALLBACKARGS:96,HREF:97,LINK_TARGET:98,"STR[link]":99,"STR[tooltip]":100,alphaNum:101,DEFAULT:102,numList:103,INTERPOLATE:104,NUM:105,COMMA:106,style:107,styleComponent:108,NODE_STRING:109,UNIT:110,BRKT:111,PCT:112,idStringToken:113,MINUS:114,MULT:115,UNICODE_TEXT:116,TEXT:117,TAGSTART:118,EDGE_TEXT:119,alphaNumToken:120,direction_tb:121,direction_bt:122,direction_rl:123,direction_lr:124,direction_td:125,$accept:0,$end:1},terminals_:{2:"error",8:"SEMI",9:"NEWLINE",10:"SPACE",11:"EOF",12:"GRAPH",13:"NODIR",14:"DIR",27:"subgraph",29:"SQS",31:"SQE",32:"end",34:"acc_title",35:"acc_title_value",36:"acc_descr",37:"acc_descr_value",38:"acc_descr_multiline_value",40:"SHAPE_DATA",44:"AMP",46:"STYLE_SEPARATOR",48:"DOUBLECIRCLESTART",49:"DOUBLECIRCLEEND",50:"PS",51:"PE",52:"(-",53:"-)",54:"STADIUMSTART",55:"STADIUMEND",56:"SUBROUTINESTART",57:"SUBROUTINEEND",58:"VERTEX_WITH_PROPS_START",59:"NODE_STRING[field]",60:"COLON",61:"NODE_STRING[value]",62:"PIPE",63:"CYLINDERSTART",64:"CYLINDEREND",65:"DIAMOND_START",66:"DIAMOND_STOP",67:"TAGEND",68:"TRAPSTART",69:"TRAPEND",70:"INVTRAPSTART",71:"INVTRAPEND",74:"TESTSTR",75:"START_LINK",77:"LINK",78:"LINK_ID",80:"STR",81:"MD_STR",84:"STYLE",85:"LINKSTYLE",86:"CLASSDEF",87:"CLASS",88:"CLICK",89:"DOWN",90:"UP",93:"idString[vertex]",94:"idString[class]",95:"CALLBACKNAME",96:"CALLBACKARGS",97:"HREF",98:"LINK_TARGET",99:"STR[link]",100:"STR[tooltip]",102:"DEFAULT",104:"INTERPOLATE",105:"NUM",106:"COMMA",109:"NODE_STRING",110:"UNIT",111:"BRKT",112:"PCT",114:"MINUS",115:"MULT",116:"UNICODE_TEXT",117:"TEXT",118:"TAGSTART",119:"EDGE_TEXT",121:"direction_tb",122:"direction_bt",123:"direction_rl",124:"direction_lr",125:"direction_td"},productions_:[0,[3,2],[5,0],[5,2],[6,1],[6,1],[6,1],[6,1],[6,1],[4,2],[4,2],[4,2],[4,3],[16,2],[16,1],[17,1],[17,1],[17,1],[15,1],[15,1],[15,2],[19,2],[19,2],[19,1],[19,1],[18,2],[18,1],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,9],[7,6],[7,4],[7,1],[7,2],[7,2],[7,1],[21,1],[21,1],[21,1],[39,2],[39,1],[20,4],[20,3],[20,4],[20,2],[20,2],[20,1],[42,1],[42,6],[42,5],[43,1],[43,3],[45,4],[45,4],[45,6],[45,4],[45,4],[45,4],[45,8],[45,4],[45,4],[45,4],[45,6],[45,4],[45,4],[45,4],[45,4],[45,4],[45,1],[41,2],[41,3],[41,3],[41,1],[41,3],[41,4],[76,1],[76,2],[76,1],[76,1],[72,1],[72,2],[73,3],[30,1],[30,2],[30,1],[30,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[28,1],[28,2],[28,1],[28,1],[24,5],[25,5],[26,2],[26,4],[26,3],[26,5],[26,3],[26,5],[26,5],[26,7],[26,2],[26,4],[26,2],[26,4],[26,4],[26,6],[22,5],[23,5],[23,5],[23,9],[23,9],[23,7],[23,7],[103,1],[103,3],[92,1],[92,3],[107,1],[107,2],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[82,1],[82,1],[82,1],[82,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[79,1],[79,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[47,1],[47,2],[101,1],[101,2],[33,1],[33,1],[33,1],[33,1],[33,1]],performAction:s(function(Et,Ot,zt,wt,cr,Re,ln){var Ge=Re.length-1;switch(cr){case 2:this.$=[];break;case 3:(!Array.isArray(Re[Ge])||Re[Ge].length>0)&&Re[Ge-1].push(Re[Ge]),this.$=Re[Ge-1];break;case 4:case 183:this.$=Re[Ge];break;case 11:wt.setDirection("TB"),this.$="TB";break;case 12:wt.setDirection(Re[Ge-1]),this.$=Re[Ge-1];break;case 27:this.$=Re[Ge-1].nodes;break;case 28:case 29:case 30:case 31:case 32:this.$=[];break;case 33:this.$=wt.addSubGraph(Re[Ge-6],Re[Ge-1],Re[Ge-4]);break;case 34:this.$=wt.addSubGraph(Re[Ge-3],Re[Ge-1],Re[Ge-3]);break;case 35:this.$=wt.addSubGraph(void 0,Re[Ge-1],void 0);break;case 37:this.$=Re[Ge].trim(),wt.setAccTitle(this.$);break;case 38:case 39:this.$=Re[Ge].trim(),wt.setAccDescription(this.$);break;case 43:this.$=Re[Ge-1]+Re[Ge];break;case 44:this.$=Re[Ge];break;case 45:wt.addVertex(Re[Ge-1][Re[Ge-1].length-1],void 0,void 0,void 0,void 0,void 0,void 0,Re[Ge]),wt.addLink(Re[Ge-3].stmt,Re[Ge-1],Re[Ge-2]),this.$={stmt:Re[Ge-1],nodes:Re[Ge-1].concat(Re[Ge-3].nodes)};break;case 46:wt.addLink(Re[Ge-2].stmt,Re[Ge],Re[Ge-1]),this.$={stmt:Re[Ge],nodes:Re[Ge].concat(Re[Ge-2].nodes)};break;case 47:wt.addLink(Re[Ge-3].stmt,Re[Ge-1],Re[Ge-2]),this.$={stmt:Re[Ge-1],nodes:Re[Ge-1].concat(Re[Ge-3].nodes)};break;case 48:this.$={stmt:Re[Ge-1],nodes:Re[Ge-1]};break;case 49:wt.addVertex(Re[Ge-1][Re[Ge-1].length-1],void 0,void 0,void 0,void 0,void 0,void 0,Re[Ge]),this.$={stmt:Re[Ge-1],nodes:Re[Ge-1],shapeData:Re[Ge]};break;case 50:this.$={stmt:Re[Ge],nodes:Re[Ge]};break;case 51:this.$=[Re[Ge]];break;case 52:wt.addVertex(Re[Ge-5][Re[Ge-5].length-1],void 0,void 0,void 0,void 0,void 0,void 0,Re[Ge-4]),this.$=Re[Ge-5].concat(Re[Ge]);break;case 53:this.$=Re[Ge-4].concat(Re[Ge]);break;case 54:this.$=Re[Ge];break;case 55:this.$=Re[Ge-2],wt.setClass(Re[Ge-2],Re[Ge]);break;case 56:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"square");break;case 57:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"doublecircle");break;case 58:this.$=Re[Ge-5],wt.addVertex(Re[Ge-5],Re[Ge-2],"circle");break;case 59:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"ellipse");break;case 60:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"stadium");break;case 61:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"subroutine");break;case 62:this.$=Re[Ge-7],wt.addVertex(Re[Ge-7],Re[Ge-1],"rect",void 0,void 0,void 0,Object.fromEntries([[Re[Ge-5],Re[Ge-3]]]));break;case 63:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"cylinder");break;case 64:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"round");break;case 65:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"diamond");break;case 66:this.$=Re[Ge-5],wt.addVertex(Re[Ge-5],Re[Ge-2],"hexagon");break;case 67:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"odd");break;case 68:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"trapezoid");break;case 69:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"inv_trapezoid");break;case 70:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"lean_right");break;case 71:this.$=Re[Ge-3],wt.addVertex(Re[Ge-3],Re[Ge-1],"lean_left");break;case 72:this.$=Re[Ge],wt.addVertex(Re[Ge]);break;case 73:Re[Ge-1].text=Re[Ge],this.$=Re[Ge-1];break;case 74:case 75:Re[Ge-2].text=Re[Ge-1],this.$=Re[Ge-2];break;case 76:this.$=Re[Ge];break;case 77:var _r=wt.destructLink(Re[Ge],Re[Ge-2]);this.$={type:_r.type,stroke:_r.stroke,length:_r.length,text:Re[Ge-1]};break;case 78:var _r=wt.destructLink(Re[Ge],Re[Ge-2]);this.$={type:_r.type,stroke:_r.stroke,length:_r.length,text:Re[Ge-1],id:Re[Ge-3]};break;case 79:this.$={text:Re[Ge],type:"text"};break;case 80:this.$={text:Re[Ge-1].text+""+Re[Ge],type:Re[Ge-1].type};break;case 81:this.$={text:Re[Ge],type:"string"};break;case 82:this.$={text:Re[Ge],type:"markdown"};break;case 83:var _r=wt.destructLink(Re[Ge]);this.$={type:_r.type,stroke:_r.stroke,length:_r.length};break;case 84:var _r=wt.destructLink(Re[Ge]);this.$={type:_r.type,stroke:_r.stroke,length:_r.length,id:Re[Ge-1]};break;case 85:this.$=Re[Ge-1];break;case 86:this.$={text:Re[Ge],type:"text"};break;case 87:this.$={text:Re[Ge-1].text+""+Re[Ge],type:Re[Ge-1].type};break;case 88:this.$={text:Re[Ge],type:"string"};break;case 89:case 104:this.$={text:Re[Ge],type:"markdown"};break;case 101:this.$={text:Re[Ge],type:"text"};break;case 102:this.$={text:Re[Ge-1].text+""+Re[Ge],type:Re[Ge-1].type};break;case 103:this.$={text:Re[Ge],type:"text"};break;case 105:this.$=Re[Ge-4],wt.addClass(Re[Ge-2],Re[Ge]);break;case 106:this.$=Re[Ge-4],wt.setClass(Re[Ge-2],Re[Ge]);break;case 107:case 115:this.$=Re[Ge-1],wt.setClickEvent(Re[Ge-1],Re[Ge]);break;case 108:case 116:this.$=Re[Ge-3],wt.setClickEvent(Re[Ge-3],Re[Ge-2]),wt.setTooltip(Re[Ge-3],Re[Ge]);break;case 109:this.$=Re[Ge-2],wt.setClickEvent(Re[Ge-2],Re[Ge-1],Re[Ge]);break;case 110:this.$=Re[Ge-4],wt.setClickEvent(Re[Ge-4],Re[Ge-3],Re[Ge-2]),wt.setTooltip(Re[Ge-4],Re[Ge]);break;case 111:this.$=Re[Ge-2],wt.setLink(Re[Ge-2],Re[Ge]);break;case 112:this.$=Re[Ge-4],wt.setLink(Re[Ge-4],Re[Ge-2]),wt.setTooltip(Re[Ge-4],Re[Ge]);break;case 113:this.$=Re[Ge-4],wt.setLink(Re[Ge-4],Re[Ge-2],Re[Ge]);break;case 114:this.$=Re[Ge-6],wt.setLink(Re[Ge-6],Re[Ge-4],Re[Ge]),wt.setTooltip(Re[Ge-6],Re[Ge-2]);break;case 117:this.$=Re[Ge-1],wt.setLink(Re[Ge-1],Re[Ge]);break;case 118:this.$=Re[Ge-3],wt.setLink(Re[Ge-3],Re[Ge-2]),wt.setTooltip(Re[Ge-3],Re[Ge]);break;case 119:this.$=Re[Ge-3],wt.setLink(Re[Ge-3],Re[Ge-2],Re[Ge]);break;case 120:this.$=Re[Ge-5],wt.setLink(Re[Ge-5],Re[Ge-4],Re[Ge]),wt.setTooltip(Re[Ge-5],Re[Ge-2]);break;case 121:this.$=Re[Ge-4],wt.addVertex(Re[Ge-2],void 0,void 0,Re[Ge]);break;case 122:this.$=Re[Ge-4],wt.updateLink([Re[Ge-2]],Re[Ge]);break;case 123:this.$=Re[Ge-4],wt.updateLink(Re[Ge-2],Re[Ge]);break;case 124:this.$=Re[Ge-8],wt.updateLinkInterpolate([Re[Ge-6]],Re[Ge-2]),wt.updateLink([Re[Ge-6]],Re[Ge]);break;case 125:this.$=Re[Ge-8],wt.updateLinkInterpolate(Re[Ge-6],Re[Ge-2]),wt.updateLink(Re[Ge-6],Re[Ge]);break;case 126:this.$=Re[Ge-6],wt.updateLinkInterpolate([Re[Ge-4]],Re[Ge]);break;case 127:this.$=Re[Ge-6],wt.updateLinkInterpolate(Re[Ge-4],Re[Ge]);break;case 128:case 130:this.$=[Re[Ge]];break;case 129:case 131:Re[Ge-2].push(Re[Ge]),this.$=Re[Ge-2];break;case 133:this.$=Re[Ge-1]+Re[Ge];break;case 181:this.$=Re[Ge];break;case 182:this.$=Re[Ge-1]+""+Re[Ge];break;case 184:this.$=Re[Ge-1]+""+Re[Ge];break;case 185:this.$={stmt:"dir",value:"TB"};break;case 186:this.$={stmt:"dir",value:"BT"};break;case 187:this.$={stmt:"dir",value:"RL"};break;case 188:this.$={stmt:"dir",value:"LR"};break;case 189:this.$={stmt:"dir",value:"TD"};break}},"anonymous"),table:[{3:1,4:2,9:t,10:r,12:n},{1:[3]},e(i,a,{5:6}),{4:7,9:t,10:r,12:n},{4:8,9:t,10:r,12:n},{13:[1,9],14:[1,10]},{1:[2,1],6:11,7:12,8:o,9:l,10:u,11:h,20:17,22:18,23:19,24:20,25:21,26:22,27:d,33:24,34:f,36:p,38:m,42:28,43:39,44:g,45:40,47:41,60:y,84:v,85:x,86:b,87:T,88:w,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A,121:I,122:D,123:L,124:B,125:O},e(i,[2,9]),e(i,[2,10]),e(i,[2,11]),{8:[1,55],9:[1,56],10:$,15:54,18:57},e(W,[2,3]),e(W,[2,4]),e(W,[2,5]),e(W,[2,6]),e(W,[2,7]),e(W,[2,8]),{8:V,9:z,11:H,21:59,41:60,72:64,75:[1,65],77:[1,67],78:[1,66]},{8:V,9:z,11:H,21:68},{8:V,9:z,11:H,21:69},{8:V,9:z,11:H,21:70},{8:V,9:z,11:H,21:71},{8:V,9:z,11:H,21:72},{8:V,9:z,10:[1,73],11:H,21:74},e(W,[2,36]),{35:[1,75]},{37:[1,76]},e(W,[2,39]),e(K,[2,50],{18:77,39:78,10:$,40:J}),{10:[1,80]},{10:[1,81]},{10:[1,82]},{10:[1,83]},{14:ne,44:Z,60:pe,80:[1,87],89:te,95:[1,84],97:[1,85],101:86,105:fe,106:ce,109:de,111:Ee,114:we,115:Fe,116:Ye,120:88},e(W,[2,185]),e(W,[2,186]),e(W,[2,187]),e(W,[2,188]),e(W,[2,189]),e(He,[2,51]),e(He,[2,54],{46:[1,100]}),e(ye,[2,72],{113:113,29:[1,101],44:g,48:[1,102],50:[1,103],52:[1,104],54:[1,105],56:[1,106],58:[1,107],60:y,63:[1,108],65:[1,109],67:[1,110],68:[1,111],70:[1,112],89:C,102:S,105:k,106:R,109:M,111:P,114:N,115:_,116:A}),e(Be,[2,181]),e(Be,[2,142]),e(Be,[2,143]),e(Be,[2,144]),e(Be,[2,145]),e(Be,[2,146]),e(Be,[2,147]),e(Be,[2,148]),e(Be,[2,149]),e(Be,[2,150]),e(Be,[2,151]),e(Be,[2,152]),e(i,[2,12]),e(i,[2,18]),e(i,[2,19]),{9:[1,114]},e(Ie,[2,26],{18:115,10:$}),e(W,[2,27]),{42:116,43:39,44:g,45:40,47:41,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A},e(W,[2,40]),e(W,[2,41]),e(W,[2,42]),e(Ne,[2,76],{73:117,62:[1,119],74:[1,118]}),{76:120,79:121,80:re,81:ue,116:le,119:he},{75:[1,126],77:[1,127]},e(Te,[2,83]),e(W,[2,28]),e(W,[2,29]),e(W,[2,30]),e(W,[2,31]),e(W,[2,32]),{10:oe,12:G,14:X,27:ee,28:128,32:q,44:ie,60:j,75:U,80:[1,130],81:[1,131],83:141,84:Y,85:Ae,86:me,87:Se,88:Ce,89:Me,90:_e,91:129,105:ze,109:Le,111:qe,114:ve,115:lt,116:Oe},e(mt,a,{5:154}),e(W,[2,37]),e(W,[2,38]),e(K,[2,48],{44:At}),e(K,[2,49],{18:156,10:$,40:Gt}),e(He,[2,44]),{44:g,47:158,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A},{102:[1,159],103:160,105:[1,161]},{44:g,47:162,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A},{44:g,47:163,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A},e(Ct,[2,107],{10:[1,164],96:[1,165]}),{80:[1,166]},e(Ct,[2,115],{120:168,10:[1,167],14:ne,44:Z,60:pe,89:te,105:fe,106:ce,109:de,111:Ee,114:we,115:Fe,116:Ye}),e(Ct,[2,117],{10:[1,169]}),e(Rt,[2,183]),e(Rt,[2,170]),e(Rt,[2,171]),e(Rt,[2,172]),e(Rt,[2,173]),e(Rt,[2,174]),e(Rt,[2,175]),e(Rt,[2,176]),e(Rt,[2,177]),e(Rt,[2,178]),e(Rt,[2,179]),e(Rt,[2,180]),{44:g,47:170,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A},{30:171,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{30:179,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{30:181,50:[1,180],67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{30:182,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{30:183,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{30:184,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{109:[1,185]},{30:186,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{30:187,65:[1,188],67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{30:189,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{30:190,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{30:191,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},e(Be,[2,182]),e(i,[2,20]),e(Ie,[2,25]),e(K,[2,46],{39:192,18:193,10:$,40:J}),e(Ne,[2,73],{10:[1,194]}),{10:[1,195]},{30:196,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{77:[1,197],79:198,116:le,119:he},e(tt,[2,79]),e(tt,[2,81]),e(tt,[2,82]),e(tt,[2,168]),e(tt,[2,169]),{76:199,79:121,80:re,81:ue,116:le,119:he},e(Te,[2,84]),{8:V,9:z,10:oe,11:H,12:G,14:X,21:201,27:ee,29:[1,200],32:q,44:ie,60:j,75:U,83:141,84:Y,85:Ae,86:me,87:Se,88:Ce,89:Me,90:_e,91:202,105:ze,109:Le,111:qe,114:ve,115:lt,116:Oe},e(Ze,[2,101]),e(Ze,[2,103]),e(Ze,[2,104]),e(Ze,[2,157]),e(Ze,[2,158]),e(Ze,[2,159]),e(Ze,[2,160]),e(Ze,[2,161]),e(Ze,[2,162]),e(Ze,[2,163]),e(Ze,[2,164]),e(Ze,[2,165]),e(Ze,[2,166]),e(Ze,[2,167]),e(Ze,[2,90]),e(Ze,[2,91]),e(Ze,[2,92]),e(Ze,[2,93]),e(Ze,[2,94]),e(Ze,[2,95]),e(Ze,[2,96]),e(Ze,[2,97]),e(Ze,[2,98]),e(Ze,[2,99]),e(Ze,[2,100]),{6:11,7:12,8:o,9:l,10:u,11:h,20:17,22:18,23:19,24:20,25:21,26:22,27:d,32:[1,203],33:24,34:f,36:p,38:m,42:28,43:39,44:g,45:40,47:41,60:y,84:v,85:x,86:b,87:T,88:w,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A,121:I,122:D,123:L,124:B,125:O},{10:$,18:204},{44:[1,205]},e(He,[2,43]),{10:[1,206],44:g,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:113,114:N,115:_,116:A},{10:[1,207]},{10:[1,208],106:[1,209]},e(dt,[2,128]),{10:[1,210],44:g,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:113,114:N,115:_,116:A},{10:[1,211],44:g,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:113,114:N,115:_,116:A},{80:[1,212]},e(Ct,[2,109],{10:[1,213]}),e(Ct,[2,111],{10:[1,214]}),{80:[1,215]},e(Rt,[2,184]),{80:[1,216],98:[1,217]},e(He,[2,55],{113:113,44:g,60:y,89:C,102:S,105:k,106:R,109:M,111:P,114:N,115:_,116:A}),{31:[1,218],67:gt,82:219,116:xt,117:Ke,118:De},e(Ut,[2,86]),e(Ut,[2,88]),e(Ut,[2,89]),e(Ut,[2,153]),e(Ut,[2,154]),e(Ut,[2,155]),e(Ut,[2,156]),{49:[1,220],67:gt,82:219,116:xt,117:Ke,118:De},{30:221,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{51:[1,222],67:gt,82:219,116:xt,117:Ke,118:De},{53:[1,223],67:gt,82:219,116:xt,117:Ke,118:De},{55:[1,224],67:gt,82:219,116:xt,117:Ke,118:De},{57:[1,225],67:gt,82:219,116:xt,117:Ke,118:De},{60:[1,226]},{64:[1,227],67:gt,82:219,116:xt,117:Ke,118:De},{66:[1,228],67:gt,82:219,116:xt,117:Ke,118:De},{30:229,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},{31:[1,230],67:gt,82:219,116:xt,117:Ke,118:De},{67:gt,69:[1,231],71:[1,232],82:219,116:xt,117:Ke,118:De},{67:gt,69:[1,234],71:[1,233],82:219,116:xt,117:Ke,118:De},e(K,[2,45],{18:156,10:$,40:Gt}),e(K,[2,47],{44:At}),e(Ne,[2,75]),e(Ne,[2,74]),{62:[1,235],67:gt,82:219,116:xt,117:Ke,118:De},e(Ne,[2,77]),e(tt,[2,80]),{77:[1,236],79:198,116:le,119:he},{30:237,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},e(mt,a,{5:238}),e(Ze,[2,102]),e(W,[2,35]),{43:239,44:g,45:40,47:41,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A},{10:$,18:240},{10:$t,60:Yt,84:Sr,92:241,105:lr,107:242,108:243,109:Wt,110:ar,111:et,112:pt},{10:$t,60:Yt,84:Sr,92:252,104:[1,253],105:lr,107:242,108:243,109:Wt,110:ar,111:et,112:pt},{10:$t,60:Yt,84:Sr,92:254,104:[1,255],105:lr,107:242,108:243,109:Wt,110:ar,111:et,112:pt},{105:[1,256]},{10:$t,60:Yt,84:Sr,92:257,105:lr,107:242,108:243,109:Wt,110:ar,111:et,112:pt},{44:g,47:258,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A},e(Ct,[2,108]),{80:[1,259]},{80:[1,260],98:[1,261]},e(Ct,[2,116]),e(Ct,[2,118],{10:[1,262]}),e(Ct,[2,119]),e(ye,[2,56]),e(Ut,[2,87]),e(ye,[2,57]),{51:[1,263],67:gt,82:219,116:xt,117:Ke,118:De},e(ye,[2,64]),e(ye,[2,59]),e(ye,[2,60]),e(ye,[2,61]),{109:[1,264]},e(ye,[2,63]),e(ye,[2,65]),{66:[1,265],67:gt,82:219,116:xt,117:Ke,118:De},e(ye,[2,67]),e(ye,[2,68]),e(ye,[2,70]),e(ye,[2,69]),e(ye,[2,71]),e([10,44,60,89,102,105,106,109,111,114,115,116],[2,85]),e(Ne,[2,78]),{31:[1,266],67:gt,82:219,116:xt,117:Ke,118:De},{6:11,7:12,8:o,9:l,10:u,11:h,20:17,22:18,23:19,24:20,25:21,26:22,27:d,32:[1,267],33:24,34:f,36:p,38:m,42:28,43:39,44:g,45:40,47:41,60:y,84:v,85:x,86:b,87:T,88:w,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A,121:I,122:D,123:L,124:B,125:O},e(He,[2,53]),{43:268,44:g,45:40,47:41,60:y,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A},e(Ct,[2,121],{106:kt}),e(vr,[2,130],{108:270,10:$t,60:Yt,84:Sr,105:lr,109:Wt,110:ar,111:et,112:pt}),e(tr,[2,132]),e(tr,[2,134]),e(tr,[2,135]),e(tr,[2,136]),e(tr,[2,137]),e(tr,[2,138]),e(tr,[2,139]),e(tr,[2,140]),e(tr,[2,141]),e(Ct,[2,122],{106:kt}),{10:[1,271]},e(Ct,[2,123],{106:kt}),{10:[1,272]},e(dt,[2,129]),e(Ct,[2,105],{106:kt}),e(Ct,[2,106],{113:113,44:g,60:y,89:C,102:S,105:k,106:R,109:M,111:P,114:N,115:_,116:A}),e(Ct,[2,110]),e(Ct,[2,112],{10:[1,273]}),e(Ct,[2,113]),{98:[1,274]},{51:[1,275]},{62:[1,276]},{66:[1,277]},{8:V,9:z,11:H,21:278},e(W,[2,34]),e(He,[2,52]),{10:$t,60:Yt,84:Sr,105:lr,107:279,108:243,109:Wt,110:ar,111:et,112:pt},e(tr,[2,133]),{14:ne,44:Z,60:pe,89:te,101:280,105:fe,106:ce,109:de,111:Ee,114:we,115:Fe,116:Ye,120:88},{14:ne,44:Z,60:pe,89:te,101:281,105:fe,106:ce,109:de,111:Ee,114:we,115:Fe,116:Ye,120:88},{98:[1,282]},e(Ct,[2,120]),e(ye,[2,58]),{30:283,67:gt,80:be,81:ct,82:172,116:xt,117:Ke,118:De},e(ye,[2,66]),e(mt,a,{5:284}),e(vr,[2,131],{108:270,10:$t,60:Yt,84:Sr,105:lr,109:Wt,110:ar,111:et,112:pt}),e(Ct,[2,126],{120:168,10:[1,285],14:ne,44:Z,60:pe,89:te,105:fe,106:ce,109:de,111:Ee,114:we,115:Fe,116:Ye}),e(Ct,[2,127],{120:168,10:[1,286],14:ne,44:Z,60:pe,89:te,105:fe,106:ce,109:de,111:Ee,114:we,115:Fe,116:Ye}),e(Ct,[2,114]),{31:[1,287],67:gt,82:219,116:xt,117:Ke,118:De},{6:11,7:12,8:o,9:l,10:u,11:h,20:17,22:18,23:19,24:20,25:21,26:22,27:d,32:[1,288],33:24,34:f,36:p,38:m,42:28,43:39,44:g,45:40,47:41,60:y,84:v,85:x,86:b,87:T,88:w,89:C,102:S,105:k,106:R,109:M,111:P,113:42,114:N,115:_,116:A,121:I,122:D,123:L,124:B,125:O},{10:$t,60:Yt,84:Sr,92:289,105:lr,107:242,108:243,109:Wt,110:ar,111:et,112:pt},{10:$t,60:Yt,84:Sr,92:290,105:lr,107:242,108:243,109:Wt,110:ar,111:et,112:pt},e(ye,[2,62]),e(W,[2,33]),e(Ct,[2,124],{106:kt}),e(Ct,[2,125],{106:kt})],defaultActions:{},parseError:s(function(Et,Ot){if(Ot.recoverable)this.trace(Et);else{var zt=new Error(Et);throw zt.hash=Ot,zt}},"parseError"),parse:s(function(Et){var Ot=this,zt=[0],wt=[],cr=[null],Re=[],ln=this.table,Ge="",_r=0,Yi=0,ll=0,Cd=2,Qu=1,O0=Re.slice.call(arguments,1),bi=Object.create(this.lexer),No={yy:{}};for(var Dc in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Dc)&&(No.yy[Dc]=this.yy[Dc]);bi.setInput(Et,No.yy),No.yy.lexer=bi,No.yy.parser=this,typeof bi.yylloc>"u"&&(bi.yylloc={});var cl=bi.yylloc;Re.push(cl);var Bt=bi.options&&bi.options.ranges;typeof No.yy.parseError=="function"?this.parseError=No.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function $e(Po){zt.length=zt.length-2*Po,cr.length=cr.length-Po,Re.length=Re.length-Po}s($e,"popStack");function nt(){var Po;return Po=wt.pop()||bi.lex()||Qu,typeof Po!="number"&&(Po instanceof Array&&(wt=Po,Po=wt.pop()),Po=Ot.symbols_[Po]||Po),Po}s(nt,"lex");for(var bt,rr,fr,dr,Ti,ji,Sa={},ca,Vi,wa,dw;;){if(fr=zt[zt.length-1],this.defaultActions[fr]?dr=this.defaultActions[fr]:((bt===null||typeof bt>"u")&&(bt=nt()),dr=ln[fr]&&ln[fr][bt]),typeof dr>"u"||!dr.length||!dr[0]){var cD="";dw=[];for(ca in ln[fr])this.terminals_[ca]&&ca>Cd&&dw.push("'"+this.terminals_[ca]+"'");bi.showPosition?cD="Parse error on line "+(_r+1)+`: +`+bi.showPosition()+` +Expecting `+dw.join(", ")+", got '"+(this.terminals_[bt]||bt)+"'":cD="Parse error on line "+(_r+1)+": Unexpected "+(bt==Qu?"end of input":"'"+(this.terminals_[bt]||bt)+"'"),this.parseError(cD,{text:bi.match,token:this.terminals_[bt]||bt,line:bi.yylineno,loc:cl,expected:dw})}if(dr[0]instanceof Array&&dr.length>1)throw new Error("Parse Error: multiple actions possible at state: "+fr+", token: "+bt);switch(dr[0]){case 1:zt.push(bt),cr.push(bi.yytext),Re.push(bi.yylloc),zt.push(dr[1]),bt=null,rr?(bt=rr,rr=null):(Yi=bi.yyleng,Ge=bi.yytext,_r=bi.yylineno,cl=bi.yylloc,ll>0&&ll--);break;case 2:if(Vi=this.productions_[dr[1]][1],Sa.$=cr[cr.length-Vi],Sa._$={first_line:Re[Re.length-(Vi||1)].first_line,last_line:Re[Re.length-1].last_line,first_column:Re[Re.length-(Vi||1)].first_column,last_column:Re[Re.length-1].last_column},Bt&&(Sa._$.range=[Re[Re.length-(Vi||1)].range[0],Re[Re.length-1].range[1]]),ji=this.performAction.apply(Sa,[Ge,Yi,_r,No.yy,dr[1],cr,Re].concat(O0)),typeof ji<"u")return ji;Vi&&(zt=zt.slice(0,-1*Vi*2),cr=cr.slice(0,-1*Vi),Re=Re.slice(0,-1*Vi)),zt.push(this.productions_[dr[1]][0]),cr.push(Sa.$),Re.push(Sa._$),wa=ln[zt[zt.length-2]][zt[zt.length-1]],zt.push(wa);break;case 3:return!0}}return!0},"parse")},on=(function(){var Pt={EOF:1,parseError:s(function(Ot,zt){if(this.yy.parser)this.yy.parser.parseError(Ot,zt);else throw new Error(Ot)},"parseError"),setInput:s(function(Et,Ot){return this.yy=Ot||this.yy||{},this._input=Et,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var Et=this._input[0];this.yytext+=Et,this.yyleng++,this.offset++,this.match+=Et,this.matched+=Et;var Ot=Et.match(/(?:\r\n?|\n).*/g);return Ot?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),Et},"input"),unput:s(function(Et){var Ot=Et.length,zt=Et.split(/(?:\r\n?|\n)/g);this._input=Et+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-Ot),this.offset-=Ot;var wt=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),zt.length-1&&(this.yylineno-=zt.length-1);var cr=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:zt?(zt.length===wt.length?this.yylloc.first_column:0)+wt[wt.length-zt.length].length-zt[0].length:this.yylloc.first_column-Ot},this.options.ranges&&(this.yylloc.range=[cr[0],cr[0]+this.yyleng-Ot]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(Et){this.unput(this.match.slice(Et))},"less"),pastInput:s(function(){var Et=this.matched.substr(0,this.matched.length-this.match.length);return(Et.length>20?"...":"")+Et.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var Et=this.match;return Et.length<20&&(Et+=this._input.substr(0,20-Et.length)),(Et.substr(0,20)+(Et.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var Et=this.pastInput(),Ot=new Array(Et.length+1).join("-");return Et+this.upcomingInput()+` +`+Ot+"^"},"showPosition"),test_match:s(function(Et,Ot){var zt,wt,cr;if(this.options.backtrack_lexer&&(cr={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(cr.yylloc.range=this.yylloc.range.slice(0))),wt=Et[0].match(/(?:\r\n?|\n).*/g),wt&&(this.yylineno+=wt.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:wt?wt[wt.length-1].length-wt[wt.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+Et[0].length},this.yytext+=Et[0],this.match+=Et[0],this.matches=Et,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(Et[0].length),this.matched+=Et[0],zt=this.performAction.call(this,this.yy,this,Ot,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),zt)return zt;if(this._backtrack){for(var Re in cr)this[Re]=cr[Re];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var Et,Ot,zt,wt;this._more||(this.yytext="",this.match="");for(var cr=this._currentRules(),Re=0;ReOt[0].length)){if(Ot=zt,wt=Re,this.options.backtrack_lexer){if(Et=this.test_match(zt,cr[Re]),Et!==!1)return Et;if(this._backtrack){Ot=!1;continue}else return!1}else if(!this.options.flex)break}return Ot?(Et=this.test_match(Ot,cr[wt]),Et!==!1?Et:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var Ot=this.next();return Ot||this.lex()},"lex"),begin:s(function(Ot){this.conditionStack.push(Ot)},"begin"),popState:s(function(){var Ot=this.conditionStack.length-1;return Ot>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(Ot){return Ot=this.conditionStack.length-1-Math.abs(Ot||0),Ot>=0?this.conditionStack[Ot]:"INITIAL"},"topState"),pushState:s(function(Ot){this.begin(Ot)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:s(function(Ot,zt,wt,cr){var Re=cr;switch(wt){case 0:return this.begin("acc_title"),34;break;case 1:return this.popState(),"acc_title_value";break;case 2:return this.begin("acc_descr"),36;break;case 3:return this.popState(),"acc_descr_value";break;case 4:this.begin("acc_descr_multiline");break;case 5:this.popState();break;case 6:return"acc_descr_multiline_value";case 7:return this.pushState("shapeData"),zt.yytext="",40;break;case 8:return this.pushState("shapeDataStr"),40;break;case 9:return this.popState(),40;break;case 10:let ln=/\n\s*/g;return zt.yytext=zt.yytext.replace(ln,"
    "),40;break;case 11:return 40;case 12:this.popState();break;case 13:this.begin("callbackname");break;case 14:this.popState();break;case 15:this.popState(),this.begin("callbackargs");break;case 16:return 95;case 17:this.popState();break;case 18:return 96;case 19:return"MD_STR";case 20:this.popState();break;case 21:this.begin("md_string");break;case 22:return"STR";case 23:this.popState();break;case 24:this.pushState("string");break;case 25:return 84;case 26:return 102;case 27:return 85;case 28:return 104;case 29:return 86;case 30:return 87;case 31:return 97;case 32:this.begin("click");break;case 33:this.popState();break;case 34:return 88;case 35:return Ot.lex.firstGraph()&&this.begin("dir"),12;break;case 36:return Ot.lex.firstGraph()&&this.begin("dir"),12;break;case 37:return Ot.lex.firstGraph()&&this.begin("dir"),12;break;case 38:return Ot.lex.firstGraph()&&this.begin("dir"),12;break;case 39:return 27;case 40:return 32;case 41:return 98;case 42:return 98;case 43:return 98;case 44:return 98;case 45:return this.popState(),13;break;case 46:return this.popState(),14;break;case 47:return this.popState(),14;break;case 48:return this.popState(),14;break;case 49:return this.popState(),14;break;case 50:return this.popState(),14;break;case 51:return this.popState(),14;break;case 52:return this.popState(),14;break;case 53:return this.popState(),14;break;case 54:return this.popState(),14;break;case 55:return this.popState(),14;break;case 56:return 121;case 57:return 122;case 58:return 123;case 59:return 124;case 60:return 125;case 61:return 78;case 62:return 105;case 63:return 111;case 64:return 46;case 65:return 60;case 66:return 44;case 67:return 8;case 68:return 106;case 69:return 115;case 70:return this.popState(),77;break;case 71:return this.pushState("edgeText"),75;break;case 72:return 119;case 73:return this.popState(),77;break;case 74:return this.pushState("thickEdgeText"),75;break;case 75:return 119;case 76:return this.popState(),77;break;case 77:return this.pushState("dottedEdgeText"),75;break;case 78:return 119;case 79:return 77;case 80:return this.popState(),53;break;case 81:return"TEXT";case 82:return this.pushState("ellipseText"),52;break;case 83:return this.popState(),55;break;case 84:return this.pushState("text"),54;break;case 85:return this.popState(),57;break;case 86:return this.pushState("text"),56;break;case 87:return 58;case 88:return this.pushState("text"),67;break;case 89:return this.popState(),64;break;case 90:return this.pushState("text"),63;break;case 91:return this.popState(),49;break;case 92:return this.pushState("text"),48;break;case 93:return this.popState(),69;break;case 94:return this.popState(),71;break;case 95:return 117;case 96:return this.pushState("trapText"),68;break;case 97:return this.pushState("trapText"),70;break;case 98:return 118;case 99:return 67;case 100:return 90;case 101:return"SEP";case 102:return 89;case 103:return 115;case 104:return 111;case 105:return 44;case 106:return 109;case 107:return 114;case 108:return 116;case 109:return this.popState(),62;break;case 110:return this.pushState("text"),62;break;case 111:return this.popState(),51;break;case 112:return this.pushState("text"),50;break;case 113:return this.popState(),31;break;case 114:return this.pushState("text"),29;break;case 115:return this.popState(),66;break;case 116:return this.pushState("text"),65;break;case 117:return"TEXT";case 118:return"QUOTE";case 119:return 9;case 120:return 10;case 121:return 11}},"anonymous"),rules:[/^(?:accTitle\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*\{\s*)/,/^(?:[\}])/,/^(?:[^\}]*)/,/^(?:@\{)/,/^(?:["])/,/^(?:["])/,/^(?:[^\"]+)/,/^(?:[^}^"]+)/,/^(?:\})/,/^(?:call[\s]+)/,/^(?:\([\s]*\))/,/^(?:\()/,/^(?:[^(]*)/,/^(?:\))/,/^(?:[^)]*)/,/^(?:[^`"]+)/,/^(?:[`]["])/,/^(?:["][`])/,/^(?:[^"]+)/,/^(?:["])/,/^(?:["])/,/^(?:style\b)/,/^(?:default\b)/,/^(?:linkStyle\b)/,/^(?:interpolate\b)/,/^(?:classDef\b)/,/^(?:class\b)/,/^(?:href[\s])/,/^(?:click[\s]+)/,/^(?:[\s\n])/,/^(?:[^\s\n]*)/,/^(?:flowchart-elk\b)/,/^(?:swimlane-beta\b)/,/^(?:graph\b)/,/^(?:flowchart\b)/,/^(?:subgraph\b)/,/^(?:end\b\s*)/,/^(?:_self\b)/,/^(?:_blank\b)/,/^(?:_parent\b)/,/^(?:_top\b)/,/^(?:(\r?\n)*\s*\n)/,/^(?:\s*LR\b)/,/^(?:\s*RL\b)/,/^(?:\s*TB\b)/,/^(?:\s*BT\b)/,/^(?:\s*TD\b)/,/^(?:\s*BR\b)/,/^(?:\s*<)/,/^(?:\s*>)/,/^(?:\s*\^)/,/^(?:\s*v\b)/,/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:.*direction\s+TD[^\n]*)/,/^(?:[^\s\"]+@(?=[^\{\"]))/,/^(?:[0-9]+)/,/^(?:#)/,/^(?::::)/,/^(?::)/,/^(?:&)/,/^(?:;)/,/^(?:,)/,/^(?:\*)/,/^(?:\s*[xo<]?--+[-xo>]\s*)/,/^(?:\s*[xo<]?--\s*)/,/^(?:[^-]|-(?!-)+)/,/^(?:\s*[xo<]?==+[=xo>]\s*)/,/^(?:\s*[xo<]?==\s*)/,/^(?:[^=]|=(?!))/,/^(?:\s*[xo<]?-?\.+-[xo>]?\s*)/,/^(?:\s*[xo<]?-\.\s*)/,/^(?:[^\.]|\.(?!))/,/^(?:\s*~~[\~]+\s*)/,/^(?:[-/\)][\)])/,/^(?:[^\(\)\[\]\{\}]|!\)+)/,/^(?:\(-)/,/^(?:\]\))/,/^(?:\(\[)/,/^(?:\]\])/,/^(?:\[\[)/,/^(?:\[\|)/,/^(?:>)/,/^(?:\)\])/,/^(?:\[\()/,/^(?:\)\)\))/,/^(?:\(\(\()/,/^(?:[\\(?=\])][\]])/,/^(?:\/(?=\])\])/,/^(?:\/(?!\])|\\(?!\])|[^\\\[\]\(\)\{\}\/]+)/,/^(?:\[\/)/,/^(?:\[\\)/,/^(?:<)/,/^(?:>)/,/^(?:\^)/,/^(?:\\\|)/,/^(?:v\b)/,/^(?:\*)/,/^(?:#)/,/^(?:&)/,/^(?:([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|-(?=[^\>\-\.])|(?!))+)/,/^(?:-)/,/^(?:[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|[\uFFD2-\uFFD7\uFFDA-\uFFDC])/,/^(?:\|)/,/^(?:\|)/,/^(?:\))/,/^(?:\()/,/^(?:\])/,/^(?:\[)/,/^(?:(\}))/,/^(?:\{)/,/^(?:[^\[\]\(\)\{\}\|\"]+)/,/^(?:")/,/^(?:(\r?\n)+)/,/^(?:\s)/,/^(?:$)/],conditions:{shapeDataEndBracket:{rules:[21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},shapeDataStr:{rules:[9,10,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},shapeData:{rules:[8,11,12,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},callbackargs:{rules:[17,18,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},callbackname:{rules:[14,15,16,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},href:{rules:[21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},click:{rules:[21,24,33,34,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},dottedEdgeText:{rules:[21,24,76,78,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},thickEdgeText:{rules:[21,24,73,75,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},edgeText:{rules:[21,24,70,72,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},trapText:{rules:[21,24,79,82,84,86,90,92,93,94,95,96,97,110,112,114,116],inclusive:!1},ellipseText:{rules:[21,24,79,80,81,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},text:{rules:[21,24,79,82,83,84,85,86,89,90,91,92,96,97,109,110,111,112,113,114,115,116,117],inclusive:!1},vertex:{rules:[21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},dir:{rules:[21,24,45,46,47,48,49,50,51,52,53,54,55,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},acc_descr_multiline:{rules:[5,6,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},acc_descr:{rules:[3,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},acc_title:{rules:[1,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},md_string:{rules:[19,20,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},string:{rules:[21,22,23,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},INITIAL:{rules:[0,2,4,7,13,21,24,25,26,27,28,29,30,31,32,35,36,37,38,39,40,41,42,43,44,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,73,74,76,77,79,82,84,86,87,88,90,92,96,97,98,99,100,101,102,103,104,105,106,107,108,110,112,114,116,118,119,120,121],inclusive:!0}}};return Pt})();Ur.lexer=on;function zr(){this.yy={}}return s(zr,"Parser"),zr.prototype=Ur,Ur.Parser=zr,new zr})();tG.parser=tG;rG=tG});var sCe,oCe,lCe=F(()=>{"use strict";aCe();sCe=Object.assign({},rG);sCe.parse=e=>{let t=e.replace(/}\s*\n/g,`} +`);return rG.parse(t)};oCe=sCe});var Ru,U1=F(()=>{"use strict";Ru=s(()=>` + /* Font Awesome icon styling - consolidated */ + .label-icon { + display: inline-block; + height: 1em; + overflow: visible; + vertical-align: -0.125em; + } + + .node .label-icon path { + fill: currentColor; + stroke: revert; + stroke-width: revert; + } +`,"getIconStyles")});var ngt,igt,dA,nG=F(()=>{"use strict";Xi();U1();ngt=s((e,t)=>{let r=Gp,n=r(e,"r"),i=r(e,"g"),a=r(e,"b");return Wi(n,i,a,t)},"fade"),igt=s(e=>`.label { + font-family: ${e.fontFamily}; + color: ${e.nodeTextColor||e.textColor}; + } + .cluster-label text { + fill: ${e.titleColor}; + } + .cluster-label span { + color: ${e.titleColor}; + } + .cluster-label span p { + background-color: transparent; + } + + .label text,span { + fill: ${e.nodeTextColor||e.textColor}; + color: ${e.nodeTextColor||e.textColor}; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: ${e.strokeWidth??1}px; + } + .rough-node .label text , .node .label text, .image-shape .label, .icon-shape .label { + text-anchor: middle; + } + + .node .katex path { + fill: #000; + stroke: #000; + stroke-width: 1px; + } + + .rough-node .label,.node .label, .image-shape .label, .icon-shape .label { + text-align: center; + } + .node.clickable { + cursor: pointer; + } + + + .root .anchor path { + fill: ${e.lineColor} !important; + stroke-width: 0; + stroke: ${e.lineColor}; + } + + .arrowheadPath { + fill: ${e.arrowheadColor}; + } + + .edgePaths .path { + stroke: ${e.lineColor}; + stroke-width: ${e.strokeWidth??2}px; + } + + .flowchart-link { + stroke: ${e.lineColor}; + fill: none; + } + + .edgeLabel { + background-color: ${e.edgeLabelBackground}; + p { + background-color: ${e.edgeLabelBackground}; + } + rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; + } + + /* For html labels only */ + .labelBkg { + background-color: ${ngt(e.edgeLabelBackground,.5)}; + // background-color: + } + + .cluster rect { + fill: ${e.clusterBkg}; + stroke: ${e.clusterBorder}; + stroke-width: 1px; + } + + .cluster text { + fill: ${e.titleColor}; + } + + .cluster span { + color: ${e.titleColor}; + } + /* .cluster div { + color: ${e.titleColor}; + } */ + + /* Collapsed subgraph node (@{ view: collapsed }) */ + .node .collapsed-indicator { + fill: ${e.clusterBorder}; + stroke: none; + opacity: 0.6; + } + + .node .collapsed-separator { + stroke: ${e.clusterBorder}; + stroke-width: 0.75px; + } + + div.mermaidTooltip { + position: absolute; + text-align: center; + max-width: 200px; + padding: 2px; + font-family: ${e.fontFamily}; + font-size: 12px; + background: ${e.tertiaryColor}; + border: 1px solid ${e.border2}; + border-radius: 2px; + pointer-events: none; + z-index: 100; + } + + .flowchartTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; + } + + rect.text { + fill: none; + stroke-width: 0; + } + + .icon-shape, .image-shape { + background-color: ${e.edgeLabelBackground}; + p { + background-color: ${e.edgeLabelBackground}; + padding: 2px; + } + .label rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; + } + ${Ru()} +`,"getStyles"),dA=igt});var pA={};sr(pA,{createFlowDiagram:()=>fA,diagram:()=>agt});var fA,agt,aC=F(()=>{"use strict";br();Jt();Sue();iCe();lCe();nG();fA=s(({defaultLayout:e,styles:t=dA}={})=>({parser:oCe,get db(){return new M4},renderer:nCe,styles:t,init:s(r=>{r.flowchart||(r.flowchart={});let n=Ew().layout??e??r.layout;n&&ib({layout:n}),r.flowchart.arrowMarkerAbsolute=r.arrowMarkerAbsolute,ib({flowchart:{arrowMarkerAbsolute:r.arrowMarkerAbsolute}})},"init")}),"createFlowDiagram"),agt=fA()});var dgt,fCe,pCe=F(()=>{"use strict";nG();dgt=s(e=>`${dA(e)} + .swimlane.cluster rect { + stroke: ${e.clusterBorder} !important; + } + [data-look="neo"].cluster rect { + filter: none; + } +`,"getStyles"),fCe=dgt});var mCe={};sr(mCe,{diagram:()=>fgt});var fgt,gCe=F(()=>{"use strict";aC();pCe();fgt=fA({defaultLayout:"swimlane",styles:fCe})});var iG,xCe,bCe=F(()=>{"use strict";iG=(function(){var e=s(function(Ne,re,ue,le){for(ue=ue||{},le=Ne.length;le--;ue[Ne[le]]=re);return ue},"o"),t=[6,9,21,23,25,27,34,37,38,39,40,41,43,46,47,51,53,54,55],r=[2,2],n=[1,7],i=[1,9],a=[1,10],o=[1,11],l=[1,12],u=[1,30],h=[1,23],d=[1,24],f=[1,25],p=[1,26],m=[1,27],g=[1,19],y=[1,28],v=[1,29],x=[1,20],b=[1,18],T=[1,21],w=[1,22],C=[2,6],S=[6,9,21,23,25,27,33,34,37,38,39,40,41,43,46,47,51,53,54,55],k=[1,36],R=[1,37],M=[1,38],P=[1,39],N=[1,40],_=[6,9,12,14,16,19,20,21,23,25,27,33,34,37,38,39,40,41,43,46,47,50,51,53,54,55,69,70,71,72,73],A=[1,46],I=[1,47],D=[1,57],L=[43,51,53,54,55,74,75],B=[1,70],O=[1,68],$=[1,65],W=[1,69],V=[1,71],z=[6,9,12,16,21,23,25,27,33,34,37,38,39,40,41,43,44,45,46,47,51,52,53,54,55,69,70,71,72,73],H=[1,78],K=[1,77],J=[1,76],ne=[69,70,71,72,73],Z=[1,91],pe=[6,9,45,50],te=[6,9,12,44,45,50,51,52],fe=[1,101],ce=[1,100],de=[1,99],Ee=[18,61],we=[1,110],Fe=[1,109],Ye=[20,43,51,53,54,55],He=[18,61,64,66],ye={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,ER_DIAGRAM:4,document:5,EOF:6,line:7,statement:8,NEWLINE:9,entityName:10,relSpec:11,COLON:12,role:13,STYLE_SEPARATOR:14,idList:15,BLOCK_START:16,attributes:17,BLOCK_STOP:18,SQS:19,SQE:20,title:21,title_value:22,acc_title:23,acc_title_value:24,acc_descr:25,acc_descr_value:26,acc_descr_multiline_value:27,direction:28,classDefStatement:29,classStatement:30,styleStatement:31,subgraphHeader:32,END:33,SUBGRAPH:34,separator:35,subgraphTitle:36,direction_tb:37,direction_bt:38,direction_rl:39,direction_lr:40,CLASSDEF:41,stylesOpt:42,UNICODE_TEXT:43,STYLE_TEXT:44,COMMA:45,CLASS:46,STYLE:47,style:48,styleComponent:49,SEMI:50,NUM:51,BRKT:52,ENTITY_NAME:53,DECIMAL_NUM:54,ENTITY_ONE:55,attribute:56,attributeType:57,attributeName:58,attributeKeyTypeList:59,attributeComment:60,ATTRIBUTE_WORD:61,"?":62,attributeKeyType:63,",":64,ATTRIBUTE_KEY:65,COMMENT:66,cardinality:67,relType:68,ZERO_OR_ONE:69,ZERO_OR_MORE:70,ONE_OR_MORE:71,ONLY_ONE:72,MD_PARENT:73,NON_IDENTIFYING:74,IDENTIFYING:75,WORD:76,$accept:0,$end:1},terminals_:{2:"error",4:"ER_DIAGRAM",6:"EOF",9:"NEWLINE",12:"COLON",14:"STYLE_SEPARATOR",16:"BLOCK_START",18:"BLOCK_STOP",19:"SQS",20:"SQE",21:"title",22:"title_value",23:"acc_title",24:"acc_title_value",25:"acc_descr",26:"acc_descr_value",27:"acc_descr_multiline_value",33:"END",34:"SUBGRAPH",37:"direction_tb",38:"direction_bt",39:"direction_rl",40:"direction_lr",41:"CLASSDEF",43:"UNICODE_TEXT",44:"STYLE_TEXT",45:"COMMA",46:"CLASS",47:"STYLE",50:"SEMI",51:"NUM",52:"BRKT",53:"ENTITY_NAME",54:"DECIMAL_NUM",55:"ENTITY_ONE",61:"ATTRIBUTE_WORD",62:"?",64:",",65:"ATTRIBUTE_KEY",66:"COMMENT",69:"ZERO_OR_ONE",70:"ZERO_OR_MORE",71:"ONE_OR_MORE",72:"ONLY_ONE",73:"MD_PARENT",74:"NON_IDENTIFYING",75:"IDENTIFYING",76:"WORD"},productions_:[0,[3,3],[5,0],[5,2],[7,1],[7,1],[7,1],[8,5],[8,9],[8,7],[8,7],[8,4],[8,6],[8,3],[8,5],[8,1],[8,3],[8,7],[8,9],[8,6],[8,8],[8,4],[8,6],[8,2],[8,2],[8,2],[8,1],[8,1],[8,1],[8,1],[8,1],[8,3],[32,3],[32,6],[36,1],[36,2],[28,1],[28,1],[28,1],[28,1],[29,4],[15,1],[15,1],[15,3],[15,3],[30,3],[31,4],[42,1],[42,3],[48,1],[48,2],[35,1],[35,1],[35,1],[49,1],[49,1],[49,1],[49,1],[10,1],[10,1],[10,1],[10,1],[10,1],[17,1],[17,2],[56,2],[56,3],[56,3],[56,4],[57,1],[57,2],[58,1],[59,1],[59,3],[63,1],[60,1],[11,3],[67,1],[67,1],[67,1],[67,1],[67,1],[68,1],[68,1],[13,1],[13,1],[13,1]],performAction:s(function(re,ue,le,he,Te,oe,G){var X=oe.length-1;switch(Te){case 1:break;case 2:this.$=[];break;case 3:this.$=oe[X-1].concat(oe[X]);break;case 4:this.$=oe[X];break;case 5:case 6:this.$=[];break;case 7:he.addEntity(oe[X-4]),he.addEntity(oe[X-2]),he.addRelationship(oe[X-4],oe[X],oe[X-2],oe[X-3]),this.$=[oe[X-4],oe[X-2]];break;case 8:he.addEntity(oe[X-8]),he.addEntity(oe[X-4]),he.addRelationship(oe[X-8],oe[X],oe[X-4],oe[X-5]),he.setClass([oe[X-8]],oe[X-6]),he.setClass([oe[X-4]],oe[X-2]),this.$=[oe[X-8],oe[X-4]];break;case 9:he.addEntity(oe[X-6]),he.addEntity(oe[X-2]),he.addRelationship(oe[X-6],oe[X],oe[X-2],oe[X-3]),he.setClass([oe[X-6]],oe[X-4]),this.$=[oe[X-6],oe[X-2]];break;case 10:he.addEntity(oe[X-6]),he.addEntity(oe[X-4]),he.addRelationship(oe[X-6],oe[X],oe[X-4],oe[X-5]),he.setClass([oe[X-4]],oe[X-2]),this.$=[oe[X-6],oe[X-4]];break;case 11:he.addEntity(oe[X-3]),he.addAttributes(oe[X-3],oe[X-1]),this.$=[oe[X-3]];break;case 12:he.addEntity(oe[X-5]),he.addAttributes(oe[X-5],oe[X-1]),he.setClass([oe[X-5]],oe[X-3]),this.$=[oe[X-5]];break;case 13:he.addEntity(oe[X-2]),this.$=[oe[X-2]];break;case 14:he.addEntity(oe[X-4]),he.setClass([oe[X-4]],oe[X-2]),this.$=[oe[X-4]];break;case 15:he.addEntity(oe[X]),this.$=[oe[X]];break;case 16:he.addEntity(oe[X-2]),he.setClass([oe[X-2]],oe[X]),this.$=[oe[X-2]];break;case 17:he.addEntity(oe[X-6],oe[X-4]),he.addAttributes(oe[X-6],oe[X-1]),this.$=[oe[X-6]];break;case 18:he.addEntity(oe[X-8],oe[X-6]),he.addAttributes(oe[X-8],oe[X-1]),he.setClass([oe[X-8]],oe[X-3]),this.$=[oe[X-8]];break;case 19:he.addEntity(oe[X-5],oe[X-3]),this.$=[oe[X-5]];break;case 20:he.addEntity(oe[X-7],oe[X-5]),he.setClass([oe[X-7]],oe[X-2]),this.$=[oe[X-7]];break;case 21:he.addEntity(oe[X-3],oe[X-1]);break;case 22:he.addEntity(oe[X-5],oe[X-3]),he.setClass([oe[X-5]],oe[X]);break;case 23:case 24:this.$=oe[X].trim(),he.setAccTitle(this.$);break;case 25:case 26:this.$=oe[X].trim(),he.setAccDescription(this.$);break;case 27:he.subgraphDepth?this.$=oe[X]:(he.setDirection(oe[X].value),this.$=[]);break;case 31:he.subgraphDepth=(he.subgraphDepth||1)-1,this.$=he.addSubGraph({text:oe[X-2].id},oe[X-1],{text:oe[X-2].text});break;case 32:he.subgraphDepth=(he.subgraphDepth||0)+1,this.$={id:oe[X-1],text:oe[X-1]};break;case 33:he.subgraphDepth=(he.subgraphDepth||0)+1,this.$={id:oe[X-4],text:oe[X-2]};break;case 34:case 59:case 60:case 61:case 62:case 86:this.$=oe[X];break;case 35:this.$=oe[X-1]+" "+oe[X];break;case 36:this.$={stmt:"dir",value:"TB"};break;case 37:this.$={stmt:"dir",value:"BT"};break;case 38:this.$={stmt:"dir",value:"RL"};break;case 39:this.$={stmt:"dir",value:"LR"};break;case 40:this.$=oe[X-3],he.addClass(oe[X-2],oe[X-1]);break;case 41:case 42:case 63:case 72:this.$=[oe[X]];break;case 43:case 44:this.$=oe[X-2].concat([oe[X]]);break;case 45:this.$=oe[X-2],he.setClass(oe[X-1],oe[X]);break;case 46:this.$=oe[X-3],he.addCssStyles(oe[X-2],oe[X-1]);break;case 47:this.$=[oe[X]];break;case 48:oe[X-2].push(oe[X]),this.$=oe[X-2];break;case 50:this.$=oe[X-1]+oe[X];break;case 58:case 84:case 85:this.$=oe[X].replace(/"/g,"");break;case 64:oe[X].push(oe[X-1]),this.$=oe[X];break;case 65:this.$={type:oe[X-1],name:oe[X]};break;case 66:this.$={type:oe[X-2],name:oe[X-1],keys:oe[X]};break;case 67:this.$={type:oe[X-2],name:oe[X-1],comment:oe[X]};break;case 68:this.$={type:oe[X-3],name:oe[X-2],keys:oe[X-1],comment:oe[X]};break;case 69:case 71:case 74:this.$=oe[X];break;case 70:this.$=oe[X-1]+oe[X];break;case 73:oe[X-2].push(oe[X]),this.$=oe[X-2];break;case 75:this.$=oe[X].replace(/"/g,"");break;case 76:this.$={cardA:oe[X],relType:oe[X-1],cardB:oe[X-2]};break;case 77:this.$=he.Cardinality.ZERO_OR_ONE;break;case 78:this.$=he.Cardinality.ZERO_OR_MORE;break;case 79:this.$=he.Cardinality.ONE_OR_MORE;break;case 80:this.$=he.Cardinality.ONLY_ONE;break;case 81:this.$=he.Cardinality.MD_PARENT;break;case 82:this.$=he.Identification.NON_IDENTIFYING;break;case 83:this.$=he.Identification.IDENTIFYING;break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},e(t,r,{5:3}),{6:[1,4],7:5,8:6,9:n,10:8,21:i,23:a,25:o,27:l,28:13,29:14,30:15,31:16,32:17,34:u,37:h,38:d,39:f,40:p,41:m,43:g,46:y,47:v,51:x,53:b,54:T,55:w},e(t,C,{1:[2,1]}),e(S,[2,3]),e(S,[2,4]),e(S,[2,5]),e(S,[2,15],{11:31,67:35,14:[1,32],16:[1,33],19:[1,34],69:k,70:R,71:M,72:P,73:N}),{22:[1,41]},{24:[1,42]},{26:[1,43]},e(S,[2,26]),e(S,[2,27]),e(S,[2,28]),e(S,[2,29]),e(S,[2,30]),e(S,r,{5:44}),e(_,[2,58]),e(_,[2,59]),e(_,[2,60]),e(_,[2,61]),e(_,[2,62]),e(S,[2,36]),e(S,[2,37]),e(S,[2,38]),e(S,[2,39]),{15:45,43:A,44:I},{15:48,43:A,44:I},{15:49,43:A,44:I},{10:50,43:g,51:x,53:b,54:T,55:w},{10:51,43:g,51:x,53:b,54:T,55:w},{15:52,43:A,44:I},{17:53,18:[1,54],56:55,57:56,61:D},{10:58,43:g,51:x,53:b,54:T,55:w},{68:59,74:[1,60],75:[1,61]},e(L,[2,77]),e(L,[2,78]),e(L,[2,79]),e(L,[2,80]),e(L,[2,81]),e(S,[2,23]),e(S,[2,24]),e(S,[2,25]),{6:[1,63],7:5,8:6,9:n,10:8,21:i,23:a,25:o,27:l,28:13,29:14,30:15,31:16,32:17,33:[1,62],34:u,37:h,38:d,39:f,40:p,41:m,43:g,46:y,47:v,51:x,53:b,54:T,55:w},{12:B,42:64,44:O,45:$,48:66,49:67,51:W,52:V},e(z,[2,41]),e(z,[2,42]),{15:72,43:A,44:I,45:$},{12:B,42:73,44:O,45:$,48:66,49:67,51:W,52:V},{6:H,9:K,19:[1,75],35:74,50:J},{12:[1,79],14:[1,80]},e(S,[2,16],{67:35,11:81,16:[1,82],45:$,69:k,70:R,71:M,72:P,73:N}),{18:[1,83]},e(S,[2,13]),{17:84,18:[2,63],56:55,57:56,61:D},{58:85,61:[1,86]},{61:[2,69],62:[1,87]},{20:[1,88]},{67:89,69:k,70:R,71:M,72:P,73:N},e(ne,[2,82]),e(ne,[2,83]),e(S,[2,31]),e(S,C),{6:H,9:K,35:90,45:Z,50:J},{43:[1,92],44:[1,93]},e(pe,[2,47],{49:94,12:B,44:O,51:W,52:V}),e(te,[2,49]),e(te,[2,54]),e(te,[2,55]),e(te,[2,56]),e(te,[2,57]),e(S,[2,45],{45:$}),{6:H,9:K,35:95,45:Z,50:J},e(S,[2,32]),{10:97,36:96,43:g,51:x,53:b,54:T,55:w},e(S,[2,51]),e(S,[2,52]),e(S,[2,53]),{13:98,43:fe,53:ce,76:de},{15:102,43:A,44:I},{10:103,43:g,51:x,53:b,54:T,55:w},{17:104,18:[1,105],56:55,57:56,61:D},e(S,[2,11]),{18:[2,64]},e(Ee,[2,65],{59:106,60:107,63:108,65:we,66:Fe}),e([18,61,65,66],[2,71]),{61:[2,70]},e(S,[2,21],{14:[1,112],16:[1,111]}),e([43,51,53,54,55],[2,76]),e(S,[2,40]),{12:B,44:O,48:113,49:67,51:W,52:V},e(z,[2,43]),e(z,[2,44]),e(te,[2,50]),e(S,[2,46]),{10:115,20:[1,114],43:g,51:x,53:b,54:T,55:w},e(Ye,[2,34]),e(S,[2,7]),e(S,[2,84]),e(S,[2,85]),e(S,[2,86]),{12:[1,116],45:$},{12:[1,118],14:[1,117]},{18:[1,119]},e(S,[2,14]),e(Ee,[2,66],{60:120,64:[1,121],66:Fe}),e(Ee,[2,67]),e(He,[2,72]),e(Ee,[2,75]),e(He,[2,74]),{17:122,18:[1,123],56:55,57:56,61:D},{15:124,43:A,44:I},e(pe,[2,48],{49:94,12:B,44:O,51:W,52:V}),{6:H,9:K,35:125,50:J},e(Ye,[2,35]),{13:126,43:fe,53:ce,76:de},{15:127,43:A,44:I},{13:128,43:fe,53:ce,76:de},e(S,[2,12]),e(Ee,[2,68]),{63:129,65:we},{18:[1,130]},e(S,[2,19]),e(S,[2,22],{16:[1,131],45:$}),e(S,[2,33]),e(S,[2,10]),{12:[1,132],45:$},e(S,[2,9]),e(He,[2,73]),e(S,[2,17]),{17:133,18:[1,134],56:55,57:56,61:D},{13:135,43:fe,53:ce,76:de},{18:[1,136]},e(S,[2,20]),e(S,[2,8]),e(S,[2,18])],defaultActions:{84:[2,64],87:[2,70]},parseError:s(function(re,ue){if(ue.recoverable)this.trace(re);else{var le=new Error(re);throw le.hash=ue,le}},"parseError"),parse:s(function(re){var ue=this,le=[0],he=[],Te=[null],oe=[],G=this.table,X="",ee=0,q=0,ie=0,j=2,U=1,Y=oe.slice.call(arguments,1),Ae=Object.create(this.lexer),me={yy:{}};for(var Se in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Se)&&(me.yy[Se]=this.yy[Se]);Ae.setInput(re,me.yy),me.yy.lexer=Ae,me.yy.parser=this,typeof Ae.yylloc>"u"&&(Ae.yylloc={});var Ce=Ae.yylloc;oe.push(Ce);var Me=Ae.options&&Ae.options.ranges;typeof me.yy.parseError=="function"?this.parseError=me.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function _e(ct){le.length=le.length-2*ct,Te.length=Te.length-ct,oe.length=oe.length-ct}s(_e,"popStack");function ze(){var ct;return ct=he.pop()||Ae.lex()||U,typeof ct!="number"&&(ct instanceof Array&&(he=ct,ct=he.pop()),ct=ue.symbols_[ct]||ct),ct}s(ze,"lex");for(var Le,qe,ve,lt,Oe,mt,At={},Gt,Ct,Rt,gt;;){if(ve=le[le.length-1],this.defaultActions[ve]?lt=this.defaultActions[ve]:((Le===null||typeof Le>"u")&&(Le=ze()),lt=G[ve]&&G[ve][Le]),typeof lt>"u"||!lt.length||!lt[0]){var be="";gt=[];for(Gt in G[ve])this.terminals_[Gt]&&Gt>j&>.push("'"+this.terminals_[Gt]+"'");Ae.showPosition?be="Parse error on line "+(ee+1)+`: +`+Ae.showPosition()+` +Expecting `+gt.join(", ")+", got '"+(this.terminals_[Le]||Le)+"'":be="Parse error on line "+(ee+1)+": Unexpected "+(Le==U?"end of input":"'"+(this.terminals_[Le]||Le)+"'"),this.parseError(be,{text:Ae.match,token:this.terminals_[Le]||Le,line:Ae.yylineno,loc:Ce,expected:gt})}if(lt[0]instanceof Array&<.length>1)throw new Error("Parse Error: multiple actions possible at state: "+ve+", token: "+Le);switch(lt[0]){case 1:le.push(Le),Te.push(Ae.yytext),oe.push(Ae.yylloc),le.push(lt[1]),Le=null,qe?(Le=qe,qe=null):(q=Ae.yyleng,X=Ae.yytext,ee=Ae.yylineno,Ce=Ae.yylloc,ie>0&&ie--);break;case 2:if(Ct=this.productions_[lt[1]][1],At.$=Te[Te.length-Ct],At._$={first_line:oe[oe.length-(Ct||1)].first_line,last_line:oe[oe.length-1].last_line,first_column:oe[oe.length-(Ct||1)].first_column,last_column:oe[oe.length-1].last_column},Me&&(At._$.range=[oe[oe.length-(Ct||1)].range[0],oe[oe.length-1].range[1]]),mt=this.performAction.apply(At,[X,q,ee,me.yy,lt[1],Te,oe].concat(Y)),typeof mt<"u")return mt;Ct&&(le=le.slice(0,-1*Ct*2),Te=Te.slice(0,-1*Ct),oe=oe.slice(0,-1*Ct)),le.push(this.productions_[lt[1]][0]),Te.push(At.$),oe.push(At._$),Rt=G[le[le.length-2]][le[le.length-1]],le.push(Rt);break;case 3:return!0}}return!0},"parse")},Be=(function(){var Ne={EOF:1,parseError:s(function(ue,le){if(this.yy.parser)this.yy.parser.parseError(ue,le);else throw new Error(ue)},"parseError"),setInput:s(function(re,ue){return this.yy=ue||this.yy||{},this._input=re,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var re=this._input[0];this.yytext+=re,this.yyleng++,this.offset++,this.match+=re,this.matched+=re;var ue=re.match(/(?:\r\n?|\n).*/g);return ue?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),re},"input"),unput:s(function(re){var ue=re.length,le=re.split(/(?:\r\n?|\n)/g);this._input=re+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-ue),this.offset-=ue;var he=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),le.length-1&&(this.yylineno-=le.length-1);var Te=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:le?(le.length===he.length?this.yylloc.first_column:0)+he[he.length-le.length].length-le[0].length:this.yylloc.first_column-ue},this.options.ranges&&(this.yylloc.range=[Te[0],Te[0]+this.yyleng-ue]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(re){this.unput(this.match.slice(re))},"less"),pastInput:s(function(){var re=this.matched.substr(0,this.matched.length-this.match.length);return(re.length>20?"...":"")+re.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var re=this.match;return re.length<20&&(re+=this._input.substr(0,20-re.length)),(re.substr(0,20)+(re.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var re=this.pastInput(),ue=new Array(re.length+1).join("-");return re+this.upcomingInput()+` +`+ue+"^"},"showPosition"),test_match:s(function(re,ue){var le,he,Te;if(this.options.backtrack_lexer&&(Te={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(Te.yylloc.range=this.yylloc.range.slice(0))),he=re[0].match(/(?:\r\n?|\n).*/g),he&&(this.yylineno+=he.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:he?he[he.length-1].length-he[he.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+re[0].length},this.yytext+=re[0],this.match+=re[0],this.matches=re,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(re[0].length),this.matched+=re[0],le=this.performAction.call(this,this.yy,this,ue,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),le)return le;if(this._backtrack){for(var oe in Te)this[oe]=Te[oe];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var re,ue,le,he;this._more||(this.yytext="",this.match="");for(var Te=this._currentRules(),oe=0;oeue[0].length)){if(ue=le,he=oe,this.options.backtrack_lexer){if(re=this.test_match(le,Te[oe]),re!==!1)return re;if(this._backtrack){ue=!1;continue}else return!1}else if(!this.options.flex)break}return ue?(re=this.test_match(ue,Te[he]),re!==!1?re:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var ue=this.next();return ue||this.lex()},"lex"),begin:s(function(ue){this.conditionStack.push(ue)},"begin"),popState:s(function(){var ue=this.conditionStack.length-1;return ue>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(ue){return ue=this.conditionStack.length-1-Math.abs(ue||0),ue>=0?this.conditionStack[ue]:"INITIAL"},"topState"),pushState:s(function(ue){this.begin(ue)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(ue,le,he,Te){var oe=Te;switch(he){case 0:return this.begin("acc_title"),23;break;case 1:return this.popState(),"acc_title_value";break;case 2:return this.begin("acc_descr"),25;break;case 3:return this.popState(),"acc_descr_value";break;case 4:this.begin("acc_descr_multiline");break;case 5:this.popState();break;case 6:return"acc_descr_multiline_value";case 7:return 37;case 8:return 38;case 9:return 39;case 10:return 40;case 11:break;case 12:return 9;case 13:return 53;case 14:return 76;case 15:return 4;case 16:return this.begin("block"),16;break;case 17:return 52;case 18:return 52;case 19:return 45;case 20:return 14;case 21:return 12;case 22:break;case 23:return 65;case 24:return 61;case 25:return 61;case 26:this.begin("block_bq");break;case 27:return 61;case 28:this.popState();break;case 29:return 66;case 30:break;case 31:return this.popState(),18;break;case 32:return le.yytext[0];case 33:return 19;case 34:return 20;case 35:return this.begin("style"),47;break;case 36:return this.popState(),9;break;case 37:break;case 38:return 12;case 39:return 45;case 40:return 52;case 41:return this.begin("style"),41;break;case 42:return 46;case 43:return 34;case 44:return 33;case 45:return 69;case 46:return 71;case 47:return 71;case 48:return 71;case 49:return 69;case 50:return 69;case 51:return 70;case 52:return 70;case 53:return 70;case 54:return 70;case 55:return 70;case 56:return 71;case 57:return 70;case 58:return 71;case 59:return 72;case 60:return 72;case 61:return 54;case 62:return 72;case 63:return 72;case 64:return 72;case 65:return 55;case 66:return 51;case 67:return 72;case 68:return 69;case 69:return 70;case 70:return 71;case 71:return 73;case 72:return 74;case 73:return 75;case 74:return 75;case 75:return 74;case 76:return 74;case 77:return 74;case 78:return 44;case 79:return 50;case 80:return 43;case 81:return le.yytext[0];case 82:return 6}},"anonymous"),rules:[/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:[ \t\r]+)/i,/^(?:[\n]+)/i,/^(?:"[^"%\r\n\v\b\\]+")/i,/^(?:"[^"]*")/i,/^(?:erDiagram\b)/i,/^(?:\{)/i,/^(?:#)/i,/^(?:#)/i,/^(?:,)/i,/^(?::::)/i,/^(?::)/i,/^(?:\s+)/i,/^(?:\b((?:PK)|(?:FK)|(?:UK))\b)/i,/^(?:([^\s]*)[~].*[~]([^\s]*))/i,/^(?:([\*A-Za-z_\u00C0-\uFFFF][A-Za-z0-9\-\_\[\]\(\)\.,\u00C0-\uFFFF\*]*))/i,/^(?:[`])/i,/^(?:[^`]+)/i,/^(?:[`])/i,/^(?:"[^"]*")/i,/^(?:[\n]+)/i,/^(?:\})/i,/^(?:.)/i,/^(?:\[)/i,/^(?:\])/i,/^(?:style\b)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?::)/i,/^(?:,)/i,/^(?:#)/i,/^(?:classDef\b)/i,/^(?:class\b)/i,/^(?:subgraph\b)/i,/^(?:end\b\s*)/i,/^(?:one or zero\b)/i,/^(?:one or more\b)/i,/^(?:one or many\b)/i,/^(?:1\+)/i,/^(?:\|o\b)/i,/^(?:zero or one\b)/i,/^(?:zero or more\b)/i,/^(?:zero or many\b)/i,/^(?:0\+)/i,/^(?:\}o\b)/i,/^(?:many\(0\))/i,/^(?:many\(1\))/i,/^(?:many\b)/i,/^(?:\}\|)/i,/^(?:one\b)/i,/^(?:only one\b)/i,/^(?:[0-9]+\.[0-9]+)/i,/^(?:1(?=\s+[A-Za-z_"']))/i,/^(?:1(?=\s+[0-9]))/i,/^(?:1(?=(--|\.\.|\.-|-\.)))/i,/^(?:1\b)/i,/^(?:[0-9]+)/i,/^(?:\|\|)/i,/^(?:o\|)/i,/^(?:o\{)/i,/^(?:\|\{)/i,/^(?:u(?=[\.\-\|]))/i,/^(?:\.\.)/i,/^(?:--)/i,/^(?:to\b)/i,/^(?:optionally to\b)/i,/^(?:\.-)/i,/^(?:-\.)/i,/^(?:([^\x00-\x7F]|\w|-|\*)+)/i,/^(?:;)/i,/^(?:([^\x00-\x7F]|\w|-|\*|\.)+)/i,/^(?:.)/i,/^(?:$)/i],conditions:{style:{rules:[36,37,38,39,40,78,79],inclusive:!1},acc_descr_multiline:{rules:[5,6],inclusive:!1},acc_descr:{rules:[3],inclusive:!1},acc_title:{rules:[1],inclusive:!1},block_bq:{rules:[27,28],inclusive:!1},block:{rules:[22,23,24,25,26,29,30,31,32],inclusive:!1},INITIAL:{rules:[0,2,4,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,33,34,35,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,80,81,82],inclusive:!0}}};return Ne})();ye.lexer=Be;function Ie(){this.yy={}}return s(Ie,"Parser"),Ie.prototype=ye,ye.Parser=Ie,new Ie})();iG.parser=iG;xCe=iG});var mA,TCe=F(()=>{"use strict";St();Jt();Yr();$n();er();mA=class{constructor(){this.entities=new Map;this.relationships=[];this.classes=new Map;this.subgraphDepth=0;this.subGraphs=[];this.subGraphLookup=new Map;this.subCount=0;this.direction="TB";this.Cardinality={ZERO_OR_ONE:"ZERO_OR_ONE",ZERO_OR_MORE:"ZERO_OR_MORE",ONE_OR_MORE:"ONE_OR_MORE",ONLY_ONE:"ONLY_ONE",MD_PARENT:"MD_PARENT"};this.Identification={NON_IDENTIFYING:"NON_IDENTIFYING",IDENTIFYING:"IDENTIFYING"};this.setAccTitle=Lr;this.getAccTitle=Ir;this.setAccDescription=Mr;this.getAccDescription=Nr;this.setDiagramTitle=Gr;this.getDiagramTitle=Pr;this.getConfig=s(()=>Pe().er,"getConfig");this.clear(),this.addEntity=this.addEntity.bind(this),this.addAttributes=this.addAttributes.bind(this),this.addRelationship=this.addRelationship.bind(this),this.setDirection=this.setDirection.bind(this),this.addCssStyles=this.addCssStyles.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.setAccTitle=this.setAccTitle.bind(this),this.setAccDescription=this.setAccDescription.bind(this),this.addSubGraph=this.addSubGraph.bind(this)}static{s(this,"ErDB")}addEntity(t,r=""){return this.entities.has(t)?!this.entities.get(t)?.alias&&r&&(this.entities.get(t).alias=r,se.info(`Add alias '${r}' to entity '${t}'`)):(this.entities.set(t,{id:`entity-${t}-${this.entities.size}`,label:t,attributes:[],alias:r,shape:"erBox",look:Pe().look??"default",cssClasses:"default",cssStyles:[],labelType:"markdown"}),se.info("Added new entity :",t)),this.entities.get(t)}getEntity(t){return this.entities.get(t)}getEntities(){return this.entities}getClasses(){return this.classes}addAttributes(t,r){let n=this.addEntity(t),i;for(i=r.length-1;i>=0;i--)r[i].keys||(r[i].keys=[]),r[i].comment||(r[i].comment=""),n.attributes.push(r[i]),se.debug("Added attribute ",r[i].name)}addRelationship(t,r,n,i){let a;if(this.subGraphLookup.has(t))a=t;else{let u=this.addEntity(t);if(!u)return;a=u.id}let o;if(this.subGraphLookup.has(n))o=n;else{let u=this.addEntity(n);if(!u)return;o=u.id}let l={entityA:a,roleA:r,entityB:o,relSpec:i};this.relationships.push(l),se.debug("Added new relationship :",l)}getRelationships(){return this.relationships}getDirection(){return this.direction}setDirection(t){this.direction=t}getCompiledStyles(t){let r=[];for(let n of t){let i=this.classes.get(n);i?.styles&&(r=[...r,...i.styles??[]].map(a=>a.trim())),i?.textStyles&&(r=[...r,...i.textStyles??[]].map(a=>a.trim()))}return r}addCssStyles(t,r){for(let n of t){let i=this.entities.get(n),a=this.subGraphLookup.get(n);if(r){if(i)for(let o of r)i.cssStyles.push(o);if(a){a.cssStyles||(a.cssStyles=[]);for(let o of r)a.cssStyles.push(o)}}}}addClass(t,r){t.forEach(n=>{let i=this.classes.get(n);i===void 0&&(i={id:n,styles:[],textStyles:[]},this.classes.set(n,i)),r&&r.forEach(function(a){if(/color/.exec(a)){let o=a.replace("fill","bgFill");i.textStyles.push(o)}i.styles.push(a)})})}addSubGraph(t,r,n){let i=t.text.trim(),a=n.text,l=s(f=>{let p=new Set,m;return{nodeList:f.filter(y=>{if(y?.stmt)return y.stmt==="dir"&&(m=y.value),!1;if(typeof y!="string")return!1;let v=y.trim();return!v||p.has(v)?!1:(p.add(v),!0)}),dir:m}},"uniq")(r.flat()),u=l.nodeList,h=l.dir;i=i??"subGraph"+this.subCount,a=a||"",a=this.sanitizeText(a),this.subCount=this.subCount+1;let d={id:i,nodes:u,title:a.trim(),classes:[],cssStyles:[],dir:h,labelType:this.sanitizeNodeLabelType(n?.type)};return se.info("Adding",d.id,d.nodes,d.dir),d.nodes=this.makeUniq(d,this.subGraphs).nodes,this.subGraphs.push(d),this.subGraphLookup.set(i,d),i}getSubGraphs(){return this.subGraphs}setClass(t,r){for(let n of t){let i=this.entities.get(n);if(i)for(let o of r)i.cssClasses+=" "+o;let a=this.subGraphLookup.get(n);if(a)for(let o of r)a.classes.push(o)}}subgraphNodeCache(t){let r=new Set;for(let n of t)for(let i of n.nodes)r.add(i);return r}makeUniq(t,r){let n=this.subgraphNodeCache(r),i=[];return t.nodes.forEach((a,o)=>{n.has(a)?se.warn(`Entity '${a}' already belongs to another subgraph and will be ignored`):i.push(t.nodes[o])}),{nodes:i}}sanitizeText(t){return Tt.sanitizeText(t,Pe())}sanitizeNodeLabelType(t){switch(t){case"markdown":case"string":case"text":return t;default:return"markdown"}}clear(){this.entities=new Map,this.classes=new Map,this.relationships=[],this.subGraphs=[],this.subGraphLookup=new Map,this.subCount=0,this.subgraphDepth=0,Tr()}getData(){let t=[],r=[],n=Pe(),i=this.getSubGraphs(),a=new Map,o=new Map;for(let d=i.length-1;d>=0;d--){let f=i[d];f.nodes.length>0&&o.set(f.id,!0);for(let p of f.nodes)a.set(p,f.id)}for(let d=i.length-1;d>=0;d--){let f=i[d];t.push({id:f.id,label:f.title,labelStyle:"",labelType:f.labelType,parentId:a.get(f.id),padding:8,cssCompiledStyles:this.getCompiledStyles(f.classes),cssStyles:f.cssStyles,cssClasses:f.classes.join(" "),shape:"rect",dir:f.dir,isGroup:!0,look:n.look})}let l=new Set(i.map(d=>d.id)),u=0;for(let d of this.entities.keys()){if(l.has(d))continue;let f=this.entities.get(d);f&&(f.cssCompiledStyles=this.getCompiledStyles(f.cssClasses.split(" ")),f.colorIndex=u++,t.push({...f,parentId:a.get(d),isGroup:!1}))}let h=0;for(let d of this.relationships){let f={id:Jc(d.entityA,d.entityB,{prefix:"id",counter:h++}),type:"normal",curve:"basis",start:d.entityA,end:d.entityB,label:d.roleA,labelpos:"c",thickness:"normal",classes:"relationshipLine",arrowTypeStart:d.relSpec.cardB.toLowerCase(),arrowTypeEnd:d.relSpec.cardA.toLowerCase(),pattern:d.relSpec.relType=="IDENTIFYING"?"solid":"dashed",look:n.look,labelType:"markdown"};r.push(f)}return{nodes:t,edges:r,other:{},config:n,direction:this.direction}}}});var aG={};sr(aG,{draw:()=>ygt});var ygt,CCe=F(()=>{"use strict";Jt();St();Rm();ep();tp();er();Wr();ygt=s(async function(e,t,r,n){se.info("REF0:"),se.info("Drawing er diagram (unified)",t);let{securityLevel:i,er:a,layout:o}=Pe(),l=n.db.getData(),u=xl(t,i);l.type=n.type,l.layoutAlgorithm=Au(o),l.config.flowchart.nodeSpacing=a?.nodeSpacing||140,l.config.flowchart.rankSpacing=a?.rankSpacing||80,l.direction=n.db.getDirection();let{config:h}=l,{look:d}=h;d==="neo"?l.markers=["only_one_neo","zero_or_one_neo","one_or_more_neo","zero_or_more_neo"]:l.markers=["only_one","zero_or_one","one_or_more","zero_or_more"],l.diagramId=t,await Dl(l,u),l.layoutAlgorithm==="elk"&&u.select(".edges").lower();let f=u.selectAll('[id*="-background"]');Array.from(f).length>0&&f.each(function(){let m=ut(this),y=m.attr("id").replace("-background",""),v=u.select(`#${CSS.escape(y)}`);if(!v.empty()){let x=v.attr("transform");m.attr("transform",x)}});let p=8;or.insertTitle(u,"erDiagramTitleText",a?.titleTopMargin??25,n.db.getDiagramTitle()),Co(u,p,"erDiagram",a?.useMaxWidth??!0)},"draw")});var SCe,gA,vgt,xgt,wCe,kCe=F(()=>{"use strict";Xi();SCe=s((e,t)=>{let r=Gp,n=r(e,"r"),i=r(e,"g"),a=r(e,"b");return Wi(n,i,a,t)},"fade"),gA=new Set(["redux-color","redux-dark-color"]),vgt=s(e=>{let{theme:t,look:r,bkgColorArray:n,borderColorArray:i}=e;if(!gA.has(t))return"";let a=n?.length>0,o="";for(let l=0;l{let{look:t,theme:r,erEdgeLabelBackground:n,strokeWidth:i}=e;return` + ${vgt(e)} + .entityBox { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + } + + .relationshipLabelBox { + fill: ${e.tertiaryColor}; + opacity: 0.7; + background-color: ${e.tertiaryColor}; + rect { + opacity: 0.5; + } + } + + .labelBkg { + background-color: ${gA.has(r)&&n?n:SCe(e.tertiaryColor,.5)}; + } + + .edgeLabel { + background-color: ${gA.has(r)&&n?n:e.edgeLabelBackground}; + } + .edgeLabel .label rect { + fill: ${gA.has(r)&&n?n:e.edgeLabelBackground}; + } + .edgeLabel .label text { + fill: ${e.textColor}; + } + + .edgeLabel .label { + fill: ${e.nodeBorder}; + font-size: 14px; + } + + .label { + font-family: ${e.fontFamily}; + color: ${e.nodeTextColor||e.textColor}; + } + + .edge-pattern-dashed { + stroke-dasharray: 8,8; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon + { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: ${t==="neo"?i:"1px"}; + } + + .relationshipLine { + stroke: ${e.lineColor}; + stroke-width: ${t==="neo"?i:"1px"}; + fill: none; + } + + .marker { + fill: none !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; + } + [data-look=neo].labelBkg { + background-color: ${SCe(e.tertiaryColor,.5)}; + } + + .cluster rect { + fill: ${e.clusterBkg??e.mainBkg}; + stroke: ${e.clusterBorder??e.nodeBorder}; + stroke-width: 1px; + } + + .cluster text { + fill: ${e.titleColor??e.textColor}; + } + + .cluster-label text { + fill: ${e.titleColor??e.textColor}; + } +`},"getStyles"),wCe=xgt});var ECe={};sr(ECe,{diagram:()=>bgt});var bgt,ACe=F(()=>{"use strict";bCe();TCe();CCe();kCe();bgt={parser:xCe,get db(){return new mA},renderer:aG,styles:wCe}});function sa(e){return typeof e=="object"&&e!==null&&typeof e.$type=="string"}function Ys(e){return typeof e=="object"&&e!==null&&typeof e.$refText=="string"&&"ref"in e}function $u(e){return typeof e=="object"&&e!==null&&typeof e.$refText=="string"&&"items"in e}function hW(e){return typeof e=="object"&&e!==null&&typeof e.name=="string"&&typeof e.type=="string"&&typeof e.path=="string"}function Tg(e){return typeof e=="object"&&e!==null&&typeof e.info=="object"&&typeof e.message=="string"}function Yh(e){return typeof e=="object"&&e!==null&&Array.isArray(e.content)}function Jg(e){return typeof e=="object"&&e!==null&&typeof e.tokenType=="object"}function r6(e){return Yh(e)&&typeof e.fullText=="string"}function Lwe(e){return typeof e=="string"?e:typeof e>"u"?"undefined":typeof e.toString=="function"?e.toString():Object.prototype.toString.call(e)}function HC(e){return!!e&&typeof e[Symbol.iterator]=="function"}function zn(...e){if(e.length===1){let t=e[0];if(t instanceof Ou)return t;if(HC(t))return new Ou(()=>t[Symbol.iterator](),r=>r.next());if(typeof t.length=="number")return new Ou(()=>({index:0}),r=>r.index1?new Ou(()=>({collIndex:0,arrIndex:0}),t=>{do{if(t.iterator){let r=t.iterator.next();if(!r.done)return r;t.iterator=void 0}if(t.array){if(t.arrIndex{sa(i)&&(i.$container=e,i.$containerProperty=r,i.$containerIndex=a,t.deep&&bv(i,t))}):sa(n)&&(n.$container=e,n.$containerProperty=r,t.deep&&bv(n,t)))}function e0(e,t){let r=e;for(;r;){if(t(r))return r;r=r.$container}}function Dwe(e,t){let r=e;for(;r;){if(t(r))return!0;r=r.$container}return!1}function Cc(e){let r=pv(e).$document;if(!r)throw new Error("AST node has no document.");return r}function pv(e){for(;e.$container;)e=e.$container;return e}function hR(e){return Ys(e)?e.ref?[e.ref]:[]:$u(e)?e.items.map(t=>t.ref):[]}function hS(e,t){if(!e)throw new Error("Node must be an AstNode.");let r=t?.range;return new Ou(()=>({keys:Object.keys(e),keyIndex:0,arrayIndex:0}),n=>{for(;n.keyIndexhS(r,t))}function Sc(e,t){if(e){if(t?.range&&!dR(e,t.range))return new xv(e,()=>[])}else throw new Error("Root node must be an AstNode.");return new xv(e,r=>hS(r,t),{includeRoot:!0})}function dR(e,t){if(!t)return!0;let r=e.$cstNode?.range;return r?BW(r,t):!1}function Tv(e){return new Ou(()=>({keys:Object.keys(e),keyIndex:0,arrayIndex:0}),t=>{for(;t.keyIndexYh(t)?t.content:[],{includeRoot:!0})}function Kwe(e){return Sv(e).filter(Jg)}function PW(e,t){for(;e.container;)if(e=e.container,e===t)return!0;return!1}function YC(e){return{start:{character:e.startColumn-1,line:e.startLine-1},end:{character:e.endColumn,line:e.endLine-1}}}function wv(e){if(!e)return;let{offset:t,end:r,range:n}=e;return{range:n,offset:t,end:r,length:r-t}}function OW(e,t){if(e.end.linet.end.line||e.start.line===t.end.line&&e.start.character>=t.end.character)return Pu.After;let r=e.start.line>t.start.line||e.start.line===t.start.line&&e.start.character>=t.start.character,n=e.end.linePu.After}function Zwe(e,t,r=$W){if(e){if(t>0){let n=t-e.offset,i=e.text.charAt(n);r.test(i)||t--}return c6(e,t)}}function FW(e,t){if(e){let r=VW(e,!0);if(r&&AR(r,t))return r;if(r6(e)){let n=e.content.findIndex(i=>!i.hidden);for(let i=n-1;i>=0;i--){let a=e.content[i];if(AR(a,t))return a}}}}function AR(e,t){return Jg(e)&&t.includes(e.tokenType.name)}function c6(e,t){if(Jg(e))return e;if(Yh(e)){let r=zW(e,t,!1);if(r)return c6(r,t)}}function GW(e,t){if(Jg(e))return e;if(Yh(e)){let r=zW(e,t,!0);if(r)return GW(r,t)}}function zW(e,t,r){let n=0,i=e.content.length-1,a;for(;n<=i;){let o=Math.floor((n+i)/2),l=e.content[o];if(l.offset<=t&&l.end>t)return l;l.end<=t?(a=r?l:void 0,n=o+1):i=o-1}return a}function VW(e,t=!0){for(;e.container;){let r=e.container,n=r.content.indexOf(e);for(;n>0;){n--;let i=r.content[n];if(t||!i.hidden)return i}e=r}}function Qwe(e,t=!0){for(;e.container;){let r=e.container,n=r.content.indexOf(e),i=r.content.length-1;for(;nt.test(r))}function Dv(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function YW(e,t){let r=jW(e),n=t.match(r);return!!n&&n[0].length>0}function jW(e){typeof e=="string"&&(e=new RegExp(e));let t=e,r=e.source,n=0;function i(){let a="",o;function l(h){a+=r.substr(n,h),n+=h}s(l,"appendRaw"),E(l,"appendRaw");function u(h){a+="(?:"+r.substr(n,h)+"|$)",n+=h}for(s(u,"appendOptional"),E(u,"appendOptional");n",n)-n+1);break;default:u(2);break}break;case"[":o=/\[(?:\\.|.)*?\]/g,o.lastIndex=n,o=o.exec(r)||[],u(o[0].length);break;case"|":case"^":case"$":case"*":case"+":case"?":l(1);break;case"{":o=/\{\d+,?\d*\}/g,o.lastIndex=n,o=o.exec(r),o?l(o[0].length):u(1);break;case"(":if(r[n+1]==="?")switch(r[n+2]){case":":a+="(?:",n+=3,a+=i()+"|$)";break;case"=":a+="(?=",n+=3,a+=i()+")";break;case"!":o=n,n+=3,i(),a+=r.substr(o,n-o);break;case"<":switch(r[n+3]){case"=":case"!":o=n,n+=4,i(),a+=r.substr(o,n-o);break;default:l(r.indexOf(">",n)-n+1),a+=i()+"|$)";break}break}else l(1),a+=i()+"|$)";break;case")":return++n,a;default:u(1);break}return a}return s(i,"process2"),E(i,"process"),new RegExp(i(),e.flags)}function XW(e){return e.rules.find(t=>Ks(t)&&t.entry)}function KW(e){return e.rules.filter(t=>Ol(t)&&t.hidden)}function f6(e,t){let r=new Set,n=XW(e);if(!n)return new Set(e.rules);let i=[n].concat(KW(e));for(let o of i)ZW(o,r,t);let a=new Set;for(let o of e.rules)(r.has(o.name)||Ol(o)&&o.hidden)&&a.add(o);return a}function ZW(e,t,r){t.add(e.name),nd(e).forEach(n=>{if(Kh(n)||r&&s6(n)){let i=n.rule.ref;i&&!t.has(i.name)&&ZW(i,t,r)}})}function oke(e){let t=new Set;return nd(e).forEach(r=>{r0(r)&&(Ks(r.type.ref)&&t.add(r.type.ref),dS(r.type.ref)&&Ks(r.type.ref.$container)&&t.add(r.type.ref.$container))}),t}function QW(e){if(e.terminal)return e.terminal;if(e.type.ref)return y6(e.type.ref)?.terminal}function JW(e){return e.hidden&&!d6(mS(e))}function eq(e,t){return!e||!t?[]:m6(e,t,e.astNode,!0)}function p6(e,t,r){if(!e||!t)return;let n=m6(e,t,e.astNode,!0);if(n.length!==0)return r!==void 0?r=Math.max(0,Math.min(r,n.length-1)):r=0,n[r]}function m6(e,t,r,n){if(!n){let i=e0(e.grammarSource,jh);if(i&&i.feature===t)return[e]}return Yh(e)&&e.astNode===r?e.content.flatMap(i=>m6(i,t,r,!1)):[]}function lke(e,t){return e?g6(e,t,e?.astNode):[]}function tq(e,t,r){if(!e)return;let n=g6(e,t,e?.astNode);if(n.length!==0)return r!==void 0?r=Math.max(0,Math.min(r,n.length-1)):r=0,n[r]}function g6(e,t,r){if(e.astNode!==r)return[];if(Xh(e.grammarSource)&&e.grammarSource.value===t)return[e];let n=Sv(e).iterator(),i,a=[];do if(i=n.next(),!i.done){let o=i.value;o.astNode===r?Xh(o.grammarSource)&&o.grammarSource.value===t&&a.push(o):n.prune()}while(!i.done);return a}function rq(e){let t=e.astNode;for(;t===e.container?.astNode;){let r=e0(e.grammarSource,jh);if(r)return r;e=e.container}}function y6(e){let t=e;return dS(t)&&(pp(t.$container)?t=t.$container.$container:t0(t.$container)?t=t.$container:bp(t.$container)),nq(e,t,new Map)}function nq(e,t,r){function n(i,a){let o;return e0(i,jh)||(o=nq(a,a,r)),r.set(e,o),o}if(s(n,"go"),E(n,"go"),r.has(e))return r.get(e);r.set(e,void 0);for(let i of nd(t)){if(jh(i)&&i.feature.toLowerCase()==="name")return r.set(e,i),i;if(Kh(i)&&Ks(i.rule.ref))return n(i,i.rule.ref);if(a6(i)&&i.typeRef?.ref)return n(i,i.typeRef.ref)}}function iq(e){let t=e.$container;if(n0(t)){let r=t.elements,n=r.indexOf(e);for(let i=n-1;i>=0;i--){let a=r[i];if(pp(a))return a;{let o=nd(r[i]).find(pp);if(o)return o}}}if(n6(t))return iq(t)}function cke(e,t){return e==="?"||e==="*"||n0(t)&&!!t.guardCondition}function uke(e){return e==="*"||e==="+"}function hke(e){return e==="+="}function fS(e){return aq(e,new Set)}function aq(e,t){if(t.has(e))return!0;t.add(e);for(let r of nd(e))if(Kh(r)){if(!r.rule.ref||Ks(r.rule.ref)&&!aq(r.rule.ref,t)||Cv(r.rule.ref))return!1}else{if(jh(r))return!1;if(pp(r))return!1}return!!e.definition}function dke(e){return LR(e.type,new Set)}function LR(e,t){if(t.has(e))return!0;if(t.add(e),gW(e))return!1;if(EW(e))return!1;if(DW(e))return e.types.every(r=>LR(r,t));if(a6(e)){if(e.primitiveType!==void 0)return!0;if(e.stringType!==void 0)return!0;if(e.typeRef!==void 0){let r=e.typeRef.ref;return o6(r)?LR(r.type,t):!1}else return!1}else return!1}function pS(e){if(!Ol(e)){if(e.inferredType)return e.inferredType.name;if(e.dataType)return e.dataType;if(e.returnType){let t=e.returnType.ref;if(t)return t.name}}}function jg(e){if(t0(e))return Ks(e)&&fS(e)?e.name:pS(e)??e.name;if(CW(e)||o6(e)||RW(e))return e.name;if(pp(e)){let t=sq(e);if(t)return t}else if(dS(e))return e.name;throw new Error("Cannot get name of Unknown Type")}function sq(e){if(e.inferredType)return e.inferredType.name;if(e.type?.ref)return jg(e.type.ref)}function fke(e){return Ol(e)?e.type?.name??"string":Ks(e)&&fS(e)?e.name:pS(e)??e.name}function oq(e){return Ol(e)?e.type?.name??"string":pS(e)??e.name}function mS(e){let t={s:!1,i:!1,u:!1},r=i0(e.definition,t),n=Object.entries(t).filter(([,i])=>i).map(([i])=>i).join("");return new RegExp(r,n)}function i0(e,t){if(_W(e))return pke(e);if(LW(e))return mke(e);if(vW(e))return vke(e);if(s6(e)){let r=e.rule.ref;if(!r)throw new Error("Missing rule reference.");return Fu(i0(r.definition),{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized})}else{if(SW(e))return yke(e);if(IW(e))return gke(e);if(AW(e)){let r=e.regex.lastIndexOf("/"),n=e.regex.substring(1,r),i=e.regex.substring(r+1);return t&&(t.i=i.includes("i"),t.s=i.includes("s"),t.u=i.includes("u")),Fu(n,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1})}else{if(MW(e))return Fu(lq,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized});throw new Error(`Invalid terminal element: ${e?.$type}, ${e?.$cstNode?.text}`)}}}function pke(e){return Fu(e.elements.map(t=>i0(t)).join("|"),{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1})}function mke(e){return Fu(e.elements.map(t=>i0(t)).join(""),{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1})}function gke(e){return Fu(`${lq}*?${i0(e.terminal)}`,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized})}function yke(e){return Fu(`(?!${i0(e.terminal)})${lq}*?`,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized})}function vke(e){return e.right?Fu(`[${qA(e.left)}-${qA(e.right)}]`,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1}):Fu(qA(e.left),{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1})}function qA(e){return Dv(e.value)}function Fu(e,t){return(t.parenthesized||t.lookahead||t.wrap!==!1)&&(e=`(${t.lookahead??(t.parenthesized?"":"?:")}${e})`),t.cardinality?`${e}${t.cardinality}`:e}function cq(e){let t=[],r=e.Grammar;for(let n of r.rules)Ol(n)&&JW(n)&&UW(mS(n))&&t.push(n.name);return{multilineCommentRules:t,nameRegexp:$W}}function Tke(e){var t=m0t.call(e,oC),r=e[oC];try{e[oC]=void 0;var n=!0}catch{}var i=g0t.call(e);return n&&(t?e[oC]=r:delete e[oC]),i}function Cke(e){return x0t.call(e)}function Ske(e){return e==null?e===void 0?C0t:T0t:ICe&&ICe in Object(e)?y0t(e):b0t(e)}function wke(e){return e!=null&&typeof e=="object"}function kke(e){return typeof e=="symbol"||Ec(e)&&Tp(e)==S0t}function Eke(e,t){for(var r=-1,n=e==null?0:e.length,i=Array(n);++r0){if(++t>=syt)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}function qke(e){return function(){return e}}function Hke(e,t){for(var r=-1,n=e==null?0:e.length;++r-1}function eEe(e,t){var r=typeof e;return t=t??vyt,!!t&&(r=="number"||r!="symbol"&&xyt.test(e))&&e>-1&&e%1==0&&e-1&&e%1==0&&e<=Syt}function lEe(e){return e!=null&&pq(e.length)&&!id(e)}function cEe(e,t,r){if(!Pl(r))return!1;var n=typeof t;return(n=="number"?Vu(r)&&x6(t,r.length):n=="string"&&t in r)?xS(r[t],e):!1}function uEe(e){return fq(function(t,r){var n=-1,i=r.length,a=i>1?r[i-1]:void 0,o=i>2?r[2]:void 0;for(a=e.length>3&&typeof a=="function"?(i--,a):void 0,o&&T6(r[0],r[1],o)&&(a=i<3?void 0:a,i=1),t=Object(t);++n-1}function zEe(e,t){var r=this.__data__,n=w6(r,e);return n<0?(++this.size,r.push([e,t])):r[n][1]=t,this}function l0(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t0&&r(l)?t>1?xq(l,t-1,r,n,i):vq(i,l):n||(i[i.length]=l)}return i}function n4e(e){var t=e==null?0:e.length;return t?bq(e,1):[]}function a4e(e,t,r){var n=-1,i=e.length;t<0&&(t=-t>i?0:i+t),r=r>i?i:r,r<0&&(r+=i),i=t>r?0:r-t>>>0,t>>>=0;for(var a=Array(i);++nl))return!1;var h=a.get(e),d=a.get(t);if(h&&d)return h==t&&d==e;var f=-1,p=!0,m=r&Qxt?new wq:void 0;for(a.set(e,t),a.set(t,e);++f=Hbt&&(a=kq,o=!1,t=new wq(t));e:for(;++i-1?i[a?t[o]:o]:void 0}}function z3e(e,t,r){var n=e==null?0:e.length;if(!n)return-1;var i=r==null?0:yS(r);return i<0&&(i=Qbt(n+i,0)),jke(e,Wu(t,3),i)}function V3e(e){return e&&e.length?e[0]:void 0}function W3e(e,t){var r=-1,n=Vu(e)?Array(e.length):[];return h0(e,function(i,a,o){n[++r]=t(i,a,o)}),n}function q3e(e,t){var r=gn(e)?gS:t2t;return r(e,Wu(t,3))}function H3e(e,t){return bq(gr(e,t),1)}function U3e(e,t){return e!=null&&o2t.call(e,t)}function Y3e(e,t){return e!=null&&h3e(e,t,l2t)}function j3e(e){return typeof e=="string"||!gn(e)&&Ec(e)&&Tp(e)==c2t}function X3e(e,t){return gS(t,function(r){return e[r]})}function K3e(e){return e==null?[]:u2t(e,el(e))}function Z3e(e,t,r,n){e=Vu(e)?e:xa(e),r=r&&!n?yS(r):0;var i=e.length;return r<0&&(r=h2t(i+r,0)),ko(e)?r<=i&&e.indexOf(t,r)>-1:!!i&&hq(e,t,r)>-1}function Q3e(e,t,r){var n=e==null?0:e.length;if(!n)return-1;var i=r==null?0:yS(r);return i<0&&(i=d2t(n+i,0)),hq(e,t,i)}function J3e(e){if(e==null)return!0;if(Vu(e)&&(gn(e)||typeof e=="string"||typeof e.splice=="function"||jC(e)||mq(e)||C6(e)))return!e.length;var t=kv(e);if(t==f2t||t==p2t)return!e.size;if(TS(e))return!kEe(e).length;for(var r in e)if(g2t.call(e,r))return!1;return!0}function e5e(e){return Ec(e)&&Tp(e)==y2t}function t5e(e){return e===void 0}function r5e(e){if(typeof e!="function")throw new TypeError(b2t);return function(){var t=arguments;switch(t.length){case 0:return!e.call(this);case 1:return!e.call(this,t[0]);case 2:return!e.call(this,t[0],t[1]);case 3:return!e.call(this,t[0],t[1],t[2])}return!e.apply(this,t)}}function n5e(e,t,r,n){if(!Pl(e))return e;t=_6(t,e);for(var i=-1,a=t.length,o=a-1,l=e;l!=null&&++i=_2t){var h=t?null:R2t(e);if(h)return Eq(h);o=!1,i=kq,u=new wq}else u=t?[]:l;e:for(;++nJC(n,t)):e instanceof Zs&&eo(t,e)?!1:e instanceof qu?(e instanceof Zs&&t.push(e),kc(e.definition,n=>JC(n,t))):!1}function v5e(e){return e instanceof Ro}function bc(e){if(e instanceof Zs)return"SUBRULE";if(e instanceof as)return"OPTION";if(e instanceof Ro)return"OR";if(e instanceof nl)return"AT_LEAST_ONE";if(e instanceof il)return"AT_LEAST_ONE_SEP";if(e instanceof Ao)return"MANY_SEP";if(e instanceof Gi)return"MANY";if(e instanceof fi)return"CONSUME";throw Error("non exhaustive match")}function Pz(e,t,r){return[new as({definition:[new fi({terminalType:e.separator})].concat(e.definition)})].concat(t,r)}function Nv(e){if(e instanceof Zs)return Nv(e.referencedRule);if(e instanceof fi)return T5e(e);if(y5e(e))return x5e(e);if(v5e(e))return b5e(e);throw Error("non exhaustive match")}function x5e(e){let t=[],r=e.definition,n=0,i=r.length>n,a,o=!0;for(;i&&o;)a=r[n],o=JC(a),t=t.concat(Nv(a)),n=n+1,i=r.length>n;return _q(t)}function b5e(e){let t=gr(e.definition,r=>Nv(r));return _q(wc(t))}function T5e(e){return[e.terminalType]}function S5e(e){let t={};return Br(e,r=>{let n=new D2t(r).startWalking();tl(t,n)}),t}function w5e(e,t){return e.name+t+C5e}function kS(e){let t=e.toString();if(HA.hasOwnProperty(t))return HA[t];{let r=I2t.pattern(t);return HA[t]=r,r}}function k5e(){HA={}}function A5e(e,t=!1){try{let r=kS(e);return PR(r.value,{},r.flags.ignoreCase)}catch(r){if(r.message===E5e)t&&Lq(`${NR} Unable to optimize: < ${e.toString()} > + Complement Sets cannot be automatically optimized. + This will disable the lexer's first char optimizations. + See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#COMPLEMENT for details.`);else{let n="";t&&(n=` + This will disable the lexer's first char optimizations. + See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#REGEXP_PARSING for details.`),MR(`${NR} + Failed parsing: < ${e.toString()} > + Using the @chevrotain/regexp-to-ast library + Please open an issue at: https://github.com/chevrotain/chevrotain/issues`+n)}}return[]}function PR(e,t,r){switch(e.type){case"Disjunction":for(let i=0;i{if(typeof u=="number")SC(u,t,r);else{let h=u;if(r===!0)for(let d=h.from;d<=h.to;d++)SC(d,t,r);else{for(let d=h.from;d<=h.to&&d=kC){let d=h.from>=kC?h.from:kC,f=h.to,p=Jh(d),m=Jh(f);for(let g=p;g<=m;g++)t[g]=g}}}});break;case"Group":PR(o.value,t,r);break;default:throw Error("Non Exhaustive Match")}let l=o.quantifier!==void 0&&o.quantifier.atLeast===0;if(o.type==="Group"&&OR(o)===!1||o.type!=="Group"&&l===!1)break}break;default:throw Error("non exhaustive match!")}return xa(t)}function SC(e,t,r){let n=Jh(e);t[n]=n,r===!0&&R5e(e,t)}function R5e(e,t){let r=String.fromCharCode(e),n=r.toUpperCase();if(n!==r){let i=Jh(n.charCodeAt(0));t[i]=i}else{let i=r.toLowerCase();if(i!==r){let a=Jh(i.charCodeAt(0));t[a]=a}}}function Oz(e,t){return Ev(e.value,r=>{if(typeof r=="number")return eo(t,r);{let n=r;return Ev(t,i=>n.from<=i&&i<=n.to)!==void 0}})}function OR(e){let t=e.quantifier;return t&&t.atLeast===0?!0:e.value?gn(e.value)?kc(e.value,OR):OR(e.value):!1}function M6(e,t){if(t instanceof RegExp){let r=kS(t),n=new M2t(e);return n.visit(r),n.found}else return Ev(t,r=>eo(e,r.charCodeAt(0)))!==void 0}function _5e(e,t){t=Rq(t,{debug:!1,safeMode:!1,positionTracking:"full",lineTerminatorCharacters:["\r",` +`],tracer:E((b,T)=>T(),"tracer")});let r=t.tracer;r("initCharCodeToOptimizedIndexMap",()=>{K5e()});let n;r("Reject Lexer.NA",()=>{n=D6(e,b=>b[Kg]===Xs.NA)});let i=!1,a;r("Transform Patterns",()=>{i=!1,a=gr(n,b=>{let T=b[Kg];if(Zh(T)){let w=T.source;return w.length===1&&w!=="^"&&w!=="$"&&w!=="."&&!T.ignoreCase?w:w.length===2&&w[0]==="\\"&&!eo(["d","D","s","S","t","r","n","t","0","c","b","B","f","v","w","W"],w[1])?w[1]:Bz(T)}else{if(id(T))return i=!0,{exec:T};if(typeof T=="object")return i=!0,T;if(typeof T=="string"){if(T.length===1)return T;{let w=T.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&"),C=new RegExp(w);return Bz(C)}}else throw Error("non exhaustive match")}})});let o,l,u,h,d;r("misc mapping",()=>{o=gr(n,b=>b.tokenTypeIdx),l=gr(n,b=>{let T=b.GROUP;if(T!==Xs.SKIPPED){if(ko(T))return T;if(Qh(T))return!1;throw Error("non exhaustive match")}}),u=gr(n,b=>{let T=b.LONGER_ALT;if(T)return gn(T)?gr(T,C=>uSe(n,C)):[uSe(n,T)]}),h=gr(n,b=>b.PUSH_MODE),d=gr(n,b=>Rr(b,"POP_MODE"))});let f;r("Line Terminator Handling",()=>{let b=Pq(t.lineTerminatorCharacters);f=gr(n,T=>!1),t.positionTracking!=="onlyOffset"&&(f=gr(n,T=>Rr(T,"LINE_BREAKS")?!!T.LINE_BREAKS:Nq(T,b)===!1&&M6(b,T.PATTERN)))});let p,m,g,y;r("Misc Mapping #2",()=>{p=gr(n,Mq),m=gr(a,j5e),g=rl(n,(b,T)=>{let w=T.GROUP;return ko(w)&&w!==Xs.SKIPPED&&(b[w]=[]),b},{}),y=gr(a,(b,T)=>({pattern:a[T],longerAlt:u[T],canLineTerminator:f[T],isCustom:p[T],short:m[T],group:l[T],push:h[T],pop:d[T],tokenTypeIdx:o[T],tokenType:n[T]}))});let v=!0,x=[];return t.safeMode||r("First Char Optimization",()=>{x=rl(n,(b,T,w)=>{if(typeof T.PATTERN=="string"){let C=T.PATTERN.charCodeAt(0),S=Jh(C);UA(b,S,y[w])}else if(gn(T.START_CHARS_HINT)){let C;Br(T.START_CHARS_HINT,S=>{let k=typeof S=="string"?S.charCodeAt(0):S,R=Jh(k);C!==R&&(C=R,UA(b,R,y[w]))})}else if(Zh(T.PATTERN))if(T.PATTERN.unicode)v=!1,t.ensureOptimizations&&MR(`${NR} Unable to analyze < ${T.PATTERN.toString()} > pattern. + The regexp unicode flag is not currently supported by the regexp-to-ast library. + This will disable the lexer's first char optimizations. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNICODE_OPTIMIZE`);else{let C=A5e(T.PATTERN,t.ensureOptimizations);ni(C)&&(v=!1),Br(C,S=>{UA(b,S,y[w])})}else t.ensureOptimizations&&MR(`${NR} TokenType: <${T.name}> is using a custom token pattern without providing parameter. + This will disable the lexer's first char optimizations. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_OPTIMIZE`),v=!1;return b},[])}),{emptyGroups:g,patternIdxToConfig:y,charCodeToPatternIdxToConfig:x,hasCustom:i,canBeOptimized:v}}function L5e(e,t){let r=[],n=I5e(e);r=r.concat(n.errors);let i=M5e(n.valid),a=i.valid;return r=r.concat(i.errors),r=r.concat(D5e(a)),r=r.concat(F5e(a)),r=r.concat(G5e(a,t)),r=r.concat(z5e(a)),r}function D5e(e){let t=[],r=Bl(e,n=>Zh(n[Kg]));return t=t.concat(N5e(r)),t=t.concat(O5e(r)),t=t.concat(B5e(r)),t=t.concat($5e(r)),t=t.concat(P5e(r)),t}function I5e(e){let t=Bl(e,i=>!Rr(i,Kg)),r=gr(t,i=>({message:"Token Type: ->"+i.name+"<- missing static 'PATTERN' property",type:zi.MISSING_PATTERN,tokenTypes:[i]})),n=L6(e,t);return{errors:r,valid:n}}function M5e(e){let t=Bl(e,i=>{let a=i[Kg];return!Zh(a)&&!id(a)&&!Rr(a,"exec")&&!ko(a)}),r=gr(t,i=>({message:"Token Type: ->"+i.name+"<- static 'PATTERN' can only be a RegExp, a Function matching the {CustomPatternMatcherFunc} type or an Object matching the {ICustomPattern} interface.",type:zi.INVALID_PATTERN,tokenTypes:[i]})),n=L6(e,t);return{errors:r,valid:n}}function N5e(e){class t extends h6{static{s(this,"EndAnchorFinder")}static{E(this,"EndAnchorFinder")}constructor(){super(...arguments),this.found=!1}visitEndAnchor(a){this.found=!0}}let r=Bl(e,i=>{let a=i.PATTERN;try{let o=kS(a),l=new t;return l.visit(o),l.found}catch{return N2t.test(a.source)}});return gr(r,i=>({message:`Unexpected RegExp Anchor Error: + Token Type: ->`+i.name+`<- static 'PATTERN' cannot contain end of input anchor '$' + See chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:zi.EOI_ANCHOR_FOUND,tokenTypes:[i]}))}function P5e(e){let t=Bl(e,n=>n.PATTERN.test(""));return gr(t,n=>({message:"Token Type: ->"+n.name+"<- static 'PATTERN' must not match an empty string",type:zi.EMPTY_MATCH_PATTERN,tokenTypes:[n]}))}function O5e(e){class t extends h6{static{s(this,"StartAnchorFinder")}static{E(this,"StartAnchorFinder")}constructor(){super(...arguments),this.found=!1}visitStartAnchor(a){this.found=!0}}let r=Bl(e,i=>{let a=i.PATTERN;try{let o=kS(a),l=new t;return l.visit(o),l.found}catch{return P2t.test(a.source)}});return gr(r,i=>({message:`Unexpected RegExp Anchor Error: + Token Type: ->`+i.name+`<- static 'PATTERN' cannot contain start of input anchor '^' + See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:zi.SOI_ANCHOR_FOUND,tokenTypes:[i]}))}function B5e(e){let t=Bl(e,n=>{let i=n[Kg];return i instanceof RegExp&&(i.multiline||i.global)});return gr(t,n=>({message:"Token Type: ->"+n.name+"<- static 'PATTERN' may NOT contain global('g') or multiline('m')",type:zi.UNSUPPORTED_FLAGS_FOUND,tokenTypes:[n]}))}function $5e(e){let t=[],r=gr(e,a=>rl(e,(o,l)=>(a.PATTERN.source===l.PATTERN.source&&!eo(t,l)&&l.PATTERN!==Xs.NA&&(t.push(l),o.push(l)),o),[]));r=wS(r);let n=Bl(r,a=>a.length>1);return gr(n,a=>{let o=gr(a,u=>u.name);return{message:`The same RegExp pattern ->${Ac(a).PATTERN}<-has been used in all of the following Token Types: ${o.join(", ")} <-`,type:zi.DUPLICATE_PATTERNS_FOUND,tokenTypes:a}})}function F5e(e){let t=Bl(e,n=>{if(!Rr(n,"GROUP"))return!1;let i=n.GROUP;return i!==Xs.SKIPPED&&i!==Xs.NA&&!ko(i)});return gr(t,n=>({message:"Token Type: ->"+n.name+"<- static 'GROUP' can only be Lexer.SKIPPED/Lexer.NA/A String",type:zi.INVALID_GROUP_TYPE_FOUND,tokenTypes:[n]}))}function G5e(e,t){let r=Bl(e,i=>i.PUSH_MODE!==void 0&&!eo(t,i.PUSH_MODE));return gr(r,i=>({message:`Token Type: ->${i.name}<- static 'PUSH_MODE' value cannot refer to a Lexer Mode ->${i.PUSH_MODE}<-which does not exist`,type:zi.PUSH_MODE_DOES_NOT_EXIST,tokenTypes:[i]}))}function z5e(e){let t=[],r=rl(e,(n,i,a)=>{let o=i.PATTERN;return o===Xs.NA||(ko(o)?n.push({str:o,idx:a,tokenType:i}):Zh(o)&&W5e(o)&&n.push({str:o.source,idx:a,tokenType:i})),n},[]);return Br(e,(n,i)=>{Br(r,({str:a,idx:o,tokenType:l})=>{if(i${l.name}<- can never be matched. +Because it appears AFTER the Token Type ->${n.name}<-in the lexer's definition. +See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNREACHABLE`;t.push({message:u,type:zi.UNREACHABLE_PATTERN,tokenTypes:[n,l]})}})}),t}function V5e(e,t){if(Zh(t)){if(q5e(t))return!1;let r=t.exec(e);return r!==null&&r.index===0}else{if(id(t))return t(e,0,[],{});if(Rr(t,"exec"))return t.exec(e,0,[],{});if(typeof t=="string")return t===e;throw Error("non exhaustive match")}}function W5e(e){return Ev([".","\\","[","]","|","^","$","(",")","?","*","+","{"],r=>e.source.indexOf(r)!==-1)===void 0}function q5e(e){return/(\(\?=)|(\(\?!)|(\(\?<=)|(\(\? property in its definition +`,type:zi.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE}),Rr(e,xA)||n.push({message:"A MultiMode Lexer cannot be initialized without a <"+xA+`> property in its definition +`,type:zi.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY}),Rr(e,xA)&&Rr(e,wC)&&!Rr(e.modes,e.defaultMode)&&n.push({message:`A MultiMode Lexer cannot be initialized with a ${wC}: <${e.defaultMode}>which does not exist +`,type:zi.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST}),Rr(e,xA)&&Br(e.modes,(i,a)=>{Br(i,(o,l)=>{if(Qh(o))n.push({message:`A Lexer cannot be initialized using an undefined Token Type. Mode:<${a}> at index: <${l}> +`,type:zi.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED});else if(Rr(o,"LONGER_ALT")){let u=gn(o.LONGER_ALT)?o.LONGER_ALT:[o.LONGER_ALT];Br(u,h=>{!Qh(h)&&!eo(i,h)&&n.push({message:`A MultiMode Lexer cannot be initialized with a longer_alt <${h.name}> on token <${o.name}> outside of mode <${a}> +`,type:zi.MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE})})}})}),n}function U5e(e,t,r){let n=[],i=!1,a=wS(wc(xa(e.modes))),o=D6(a,u=>u[Kg]===Xs.NA),l=Pq(r);return t&&Br(o,u=>{let h=Nq(u,l);if(h!==!1){let f={message:X5e(u,h),type:h.issue,tokenType:u};n.push(f)}else Rr(u,"LINE_BREAKS")?u.LINE_BREAKS===!0&&(i=!0):M6(l,u.PATTERN)&&(i=!0)}),t&&!i&&n.push({message:`Warning: No LINE_BREAKS Found. + This Lexer has been defined to track line and column information, + But none of the Token Types can be identified as matching a line terminator. + See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#LINE_BREAKS + for details.`,type:zi.NO_LINE_BREAKS_FLAGS}),n}function Y5e(e){let t={},r=el(e);return Br(r,n=>{let i=e[n];if(gn(i))t[n]=[];else throw Error("non exhaustive match")}),t}function Mq(e){let t=e.PATTERN;if(Zh(t))return!1;if(id(t))return!0;if(Rr(t,"exec"))return!0;if(ko(t))return!1;throw Error("non exhaustive match")}function j5e(e){return ko(e)&&e.length===1?e.charCodeAt(0):!1}function Nq(e,t){if(Rr(e,"LINE_BREAKS"))return!1;if(Zh(e.PATTERN)){try{M6(t,e.PATTERN)}catch(r){return{issue:zi.IDENTIFY_TERMINATOR,errMsg:r.message}}return!1}else{if(ko(e.PATTERN))return!1;if(Mq(e))return{issue:zi.CUSTOM_LINE_BREAK};throw Error("non exhaustive match")}}function X5e(e,t){if(t.issue===zi.IDENTIFY_TERMINATOR)return`Warning: unable to identify line terminator usage in pattern. + The problem is in the <${e.name}> Token Type + Root cause: ${t.errMsg}. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#IDENTIFY_TERMINATOR`;if(t.issue===zi.CUSTOM_LINE_BREAK)return`Warning: A Custom Token Pattern should specify the option. + The problem is in the <${e.name}> Token Type + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_LINE_BREAK`;throw Error("non exhaustive match")}function Pq(e){return gr(e,r=>ko(r)?r.charCodeAt(0):r)}function UA(e,t,r){e[t]===void 0?e[t]=[r]:e[t].push(r)}function Jh(e){return e255?255+~~(e/255):e}}function Pv(e,t){let r=e.tokenTypeIdx;return r===t.tokenTypeIdx?!0:t.isParent===!0&&t.categoryMatchesMap[r]===!0}function eS(e,t){return e.tokenTypeIdx===t.tokenTypeIdx}function Ov(e){let t=Q5e(e);J5e(t),tAe(t),eAe(t),Br(t,r=>{r.isParent=r.categoryMatches.length>0})}function Q5e(e){let t=ss(e),r=e,n=!0;for(;n;){r=wS(wc(gr(r,a=>a.CATEGORIES)));let i=L6(r,t);t=t.concat(i),ni(i)?n=!1:r=i}return t}function J5e(e){Br(e,t=>{Bq(t)||(Z5e[dSe]=t,t.tokenTypeIdx=dSe++),$z(t)&&!gn(t.CATEGORIES)&&(t.CATEGORIES=[t.CATEGORIES]),$z(t)||(t.CATEGORIES=[]),rAe(t)||(t.categoryMatches=[]),nAe(t)||(t.categoryMatchesMap={})})}function eAe(e){Br(e,t=>{t.categoryMatches=[],Br(t.categoryMatchesMap,(r,n)=>{t.categoryMatches.push(Z5e[n].tokenTypeIdx)})})}function tAe(e){Br(e,t=>{Oq([],t)})}function Oq(e,t){Br(e,r=>{t.categoryMatchesMap[r.tokenTypeIdx]=!0}),Br(t.CATEGORIES,r=>{let n=e.concat(t);eo(n,r)||Oq(n,r)})}function Bq(e){return Rr(e,"tokenTypeIdx")}function $z(e){return Rr(e,"CATEGORIES")}function rAe(e){return Rr(e,"categoryMatches")}function nAe(e){return Rr(e,"categoryMatchesMap")}function iAe(e){return Rr(e,"tokenTypeIdx")}function Ug(e){return $q(e)?e.LABEL:e.name}function $q(e){return ko(e.LABEL)&&e.LABEL!==""}function gv(e){return aAe(e)}function aAe(e){let t=e.pattern,r={};if(r.name=e.name,Qh(t)||(r.PATTERN=t),Rr(e,B2t))throw`The parent property is no longer supported. +See: https://github.com/chevrotain/chevrotain/issues/564#issuecomment-349062346 for details.`;return Rr(e,fSe)&&(r.CATEGORIES=e[fSe]),Ov([r]),Rr(e,pSe)&&(r.LABEL=e[pSe]),Rr(e,mSe)&&(r.GROUP=e[mSe]),Rr(e,ySe)&&(r.POP_MODE=e[ySe]),Rr(e,gSe)&&(r.PUSH_MODE=e[gSe]),Rr(e,vSe)&&(r.LONGER_ALT=e[vSe]),Rr(e,xSe)&&(r.LINE_BREAKS=e[xSe]),Rr(e,bSe)&&(r.START_CHARS_HINT=e[bSe]),r}function ES(e,t,r,n,i,a,o,l){return{image:t,startOffset:r,endOffset:n,startLine:i,endLine:a,startColumn:o,endColumn:l,tokenTypeIdx:e.tokenTypeIdx,tokenType:e}}function Fq(e,t){return Pv(e,t)}function sAe(e,t){let r=new F2t(e,t);return r.resolveRefs(),r.errors}function BR(e,t,r=[]){r=ss(r);let n=[],i=0;function a(l){return l.concat(is(e,i+1))}s(a,"remainingPathWith"),E(a,"remainingPathWith");function o(l){let u=BR(a(l),t,r);return n.concat(u)}for(s(o,"getAlternativesForProd"),E(o,"getAlternativesForProd");r.length{ni(u.definition)===!1&&(n=o(u.definition))}),n;if(l instanceof fi)r.push(l.terminalType);else throw Error("non exhaustive match")}i++}return n.push({partialPath:r,suffixDef:is(e,i)}),n}function Gq(e,t,r,n){let i="EXIT_NONE_TERMINAL",a=[i],o="EXIT_ALTERNATIVE",l=!1,u=t.length,h=u-n-1,d=[],f=[];for(f.push({idx:-1,def:e,ruleStack:[],occurrenceStack:[]});!ni(f);){let p=f.pop();if(p===o){l&&Xg(f).idx<=h&&f.pop();continue}let m=p.def,g=p.idx,y=p.ruleStack,v=p.occurrenceStack;if(ni(m))continue;let x=m[0];if(x===i){let b={idx:g,def:is(m),ruleStack:QC(y),occurrenceStack:QC(v)};f.push(b)}else if(x instanceof fi)if(g=0;b--){let T=x.definition[b],w={idx:g,def:T.definition.concat(is(m)),ruleStack:y,occurrenceStack:v};f.push(w),f.push(o)}else if(x instanceof Eo)f.push({idx:g,def:x.definition.concat(is(m)),ruleStack:y,occurrenceStack:v});else if(x instanceof Iv)f.push(oAe(x,g,y,v));else throw Error("non exhaustive match")}return d}function oAe(e,t,r,n){let i=ss(r);i.push(e.name);let a=ss(n);return a.push(1),{idx:t,def:e.definition,ruleStack:i,occurrenceStack:a}}function P6(e){if(e instanceof as||e==="Option")return Ri.OPTION;if(e instanceof Gi||e==="Repetition")return Ri.REPETITION;if(e instanceof nl||e==="RepetitionMandatory")return Ri.REPETITION_MANDATORY;if(e instanceof il||e==="RepetitionMandatoryWithSeparator")return Ri.REPETITION_MANDATORY_WITH_SEPARATOR;if(e instanceof Ao||e==="RepetitionWithSeparator")return Ri.REPETITION_WITH_SEPARATOR;if(e instanceof Ro||e==="Alternation")return Ri.ALTERNATION;throw Error("non exhaustive match")}function Gz(e){let{occurrence:t,rule:r,prodType:n,maxLookahead:i}=e,a=P6(n);return a===Ri.ALTERNATION?AS(t,r,i):RS(t,r,a,i)}function lAe(e,t,r,n,i,a){let o=AS(e,t,r),l=Vq(o)?eS:Pv;return a(o,n,l,i)}function cAe(e,t,r,n,i,a){let o=RS(e,t,i,r),l=Vq(o)?eS:Pv;return a(o[0],l,n)}function uAe(e,t,r,n){let i=e.length,a=kc(e,o=>kc(o,l=>l.length===1));if(t)return function(o){let l=gr(o,u=>u.GATE);for(let u=0;uwc(u)),l=rl(o,(u,h,d)=>(Br(h,f=>{Rr(u,f.tokenTypeIdx)||(u[f.tokenTypeIdx]=d),Br(f.categoryMatches,p=>{Rr(u,p)||(u[p]=d)})}),u),{});return function(){let u=this.LA(1);return l[u.tokenTypeIdx]}}else return function(){for(let o=0;oa.length===1),i=e.length;if(n&&!r){let a=wc(e);if(a.length===1&&ni(a[0].categoryMatches)){let l=a[0].tokenTypeIdx;return function(){return this.LA(1).tokenTypeIdx===l}}else{let o=rl(a,(l,u,h)=>(l[u.tokenTypeIdx]=!0,Br(u.categoryMatches,d=>{l[d]=!0}),l),[]);return function(){let l=this.LA(1);return o[l.tokenTypeIdx]===!0}}}else return function(){e:for(let a=0;aBR([o],1)),n=zz(r.length),i=gr(r,o=>{let l={};return Br(o,u=>{let h=jA(u.partialPath);Br(h,d=>{l[d]=!0})}),l}),a=r;for(let o=1;o<=t;o++){let l=a;a=zz(l.length);for(let u=0;u{let x=jA(v.partialPath);Br(x,b=>{i[u][b]=!0})})}}}}return n}function AS(e,t,r,n){let i=new dAe(e,Ri.ALTERNATION,n);return t.accept(i),zq(i.result,r)}function RS(e,t,r,n){let i=new dAe(e,r);t.accept(i);let a=i.result,l=new q2t(t,e,r).startWalking(),u=new Eo({definition:a}),h=new Eo({definition:l});return zq([u,h],n)}function $R(e,t){e:for(let r=0;r{let i=t[n];return r===i||i.categoryMatchesMap[r.tokenTypeIdx]})}function Vq(e){return kc(e,t=>kc(t,r=>kc(r,n=>ni(n.categoryMatches))))}function mAe(e){let t=e.lookaheadStrategy.validate({rules:e.rules,tokenTypes:e.tokenTypes,grammarName:e.grammarName});return gr(t,r=>Object.assign({type:Qs.CUSTOM_LOOKAHEAD_VALIDATION},r))}function gAe(e,t,r,n){let i=Ml(e,u=>yAe(u,r)),a=AAe(e,t,r),o=Ml(e,u=>SAe(u,r)),l=Ml(e,u=>xAe(u,e,n,r));return i.concat(a,o,l)}function yAe(e,t){let r=new H2t;e.accept(r);let n=r.allProductions,i=a2t(n,vAe),a=Rc(i,l=>l.length>1);return gr(xa(a),l=>{let u=Ac(l),h=t.buildDuplicateFoundError(e,l),d=bc(u),f={message:h,type:Qs.DUPLICATE_PRODUCTIONS,ruleName:e.name,dslName:d,occurrence:u.idx},p=Wq(u);return p&&(f.parameter=p),f})}function vAe(e){return`${bc(e)}_#_${e.idx}_#_${Wq(e)}`}function Wq(e){return e instanceof fi?e.terminalType.name:e instanceof Zs?e.nonTerminalName:""}function xAe(e,t,r,n){let i=[];if(rl(t,(o,l)=>l.name===e.name?o+1:o,0)>1){let o=n.buildDuplicateRuleNameError({topLevelRule:e,grammarName:r});i.push({message:o,type:Qs.DUPLICATE_RULE_NAME,ruleName:e.name})}return i}function bAe(e,t,r){let n=[],i;return eo(t,e)||(i=`Invalid rule override, rule: ->${e}<- cannot be overridden in the grammar: ->${r}<-as it is not defined in any of the super grammars `,n.push({message:i,type:Qs.INVALID_RULE_OVERRIDE,ruleName:e})),n}function qq(e,t,r,n=[]){let i=[],a=FC(t.definition);if(ni(a))return[];{let o=e.name;eo(a,e)&&i.push({message:r.buildLeftRecursionError({topLevelRule:e,leftRecursionPath:n}),type:Qs.LEFT_RECURSION,ruleName:o});let u=L6(a,n.concat([e])),h=Ml(u,d=>{let f=ss(n);return f.push(d),qq(e,d,r,f)});return i.concat(h)}}function FC(e){let t=[];if(ni(e))return t;let r=Ac(e);if(r instanceof Zs)t.push(r.referencedRule);else if(r instanceof Eo||r instanceof as||r instanceof nl||r instanceof il||r instanceof Ao||r instanceof Gi)t=t.concat(FC(r.definition));else if(r instanceof Ro)t=wc(gr(r.definition,a=>FC(a.definition)));else if(!(r instanceof fi))throw Error("non exhaustive match");let n=JC(r),i=e.length>1;if(n&&i){let a=is(e);return t.concat(FC(a))}else return t}function TAe(e,t){let r=new Hq;e.accept(r);let n=r.alternations;return Ml(n,a=>{let o=QC(a.definition);return Ml(o,(l,u)=>{let h=Gq([l],[],Pv,1);return ni(h)?[{message:t.buildEmptyAlternationError({topLevelRule:e,alternation:a,emptyChoiceIdx:u}),type:Qs.NONE_LAST_EMPTY_ALT,ruleName:e.name,occurrence:a.idx,alternative:u+1}]:[]})})}function CAe(e,t,r){let n=new Hq;e.accept(n);let i=n.alternations;return i=D6(i,o=>o.ignoreAmbiguities===!0),Ml(i,o=>{let l=o.idx,u=o.maxLookahead||t,h=AS(l,e,u,o),d=kAe(h,o,e,r),f=EAe(h,o,e,r);return d.concat(f)})}function SAe(e,t){let r=new Hq;e.accept(r);let n=r.alternations;return Ml(n,a=>a.definition.length>255?[{message:t.buildTooManyAlternativesError({topLevelRule:e,alternation:a}),type:Qs.TOO_MANY_ALTS,ruleName:e.name,occurrence:a.idx}]:[])}function wAe(e,t,r){let n=[];return Br(e,i=>{let a=new U2t;i.accept(a);let o=a.allProductions;Br(o,l=>{let u=P6(l),h=l.maxLookahead||t,d=l.idx,p=RS(d,i,u,h)[0];if(ni(wc(p))){let m=r.buildEmptyRepetitionError({topLevelRule:i,repetition:l});n.push({message:m,type:Qs.NO_NON_EMPTY_LOOKAHEAD,ruleName:i.name})}})}),n}function kAe(e,t,r,n){let i=[],a=rl(e,(l,u,h)=>(t.definition[h].ignoreAmbiguities===!0||Br(u,d=>{let f=[h];Br(e,(p,m)=>{h!==m&&$R(p,d)&&t.definition[m].ignoreAmbiguities!==!0&&f.push(m)}),f.length>1&&!$R(i,d)&&(i.push(d),l.push({alts:f,path:d}))}),l),[]);return gr(a,l=>{let u=gr(l.alts,d=>d+1);return{message:n.buildAlternationAmbiguityError({topLevelRule:r,alternation:t,ambiguityIndices:u,prefixPath:l.path}),type:Qs.AMBIGUOUS_ALTS,ruleName:r.name,occurrence:t.idx,alternatives:l.alts}})}function EAe(e,t,r,n){let i=rl(e,(o,l,u)=>{let h=gr(l,d=>({idx:u,path:d}));return o.concat(h)},[]);return wS(Ml(i,o=>{if(t.definition[o.idx].ignoreAmbiguities===!0)return[];let u=o.idx,h=o.path,d=Bl(i,p=>t.definition[p.idx].ignoreAmbiguities!==!0&&p.idx{let m=[p.idx+1,u+1],g=t.idx===0?"":t.idx;return{message:n.buildAlternationPrefixAmbiguityError({topLevelRule:r,alternation:t,ambiguityIndices:m,prefixPath:p.path}),type:Qs.AMBIGUOUS_PREFIX_ALTS,ruleName:r.name,occurrence:g,alternatives:m}})}))}function AAe(e,t,r){let n=[],i=gr(t,a=>a.name);return Br(e,a=>{let o=a.name;if(eo(i,o)){let l=r.buildNamespaceConflictError(a);n.push({message:l,type:Qs.CONFLICT_TOKENS_RULES_NAMESPACE,ruleName:o})}}),n}function RAe(e){let t=Rq(e,{errMsgProvider:$2t}),r={};return Br(e.rules,n=>{r[n.name]=n}),sAe(r,t.errMsgProvider)}function _Ae(e){return e=Rq(e,{errMsgProvider:Wg}),gAe(e.rules,e.tokenTypes,e.errMsgProvider,e.grammarName)}function tS(e){return eo(NAe,e.name)}function BAe(e,t,r,n,i,a,o){let l=this.getKeyForAutomaticLookahead(n,i),u=this.firstAfterRepMap[l];if(u===void 0){let p=this.getCurrRuleFullName(),m=this.getGAstProductions()[p];u=new a(m,i).startWalking(),this.firstAfterRepMap[l]=u}let h=u.token,d=u.occurrence,f=u.isEndOfRule;this.RULE_STACK.length===1&&f&&h===void 0&&(h=gp,d=1),!(h===void 0||d===void 0)&&this.shouldInRepetitionRecoveryBeTried(h,d,o)&&this.tryInRepetitionRecovery(e,t,r,h)}function KA(e,t,r){return r|t|e}function GAe(e){bA.reset(),e.accept(bA);let t=bA.dslMethods;return bA.reset(),t}function Hz(e,t){isNaN(e.startOffset)===!0?(e.startOffset=t.startOffset,e.endOffset=t.endOffset):e.endOffseto.msg);throw Error(`Errors Detected in CST Visitor <${this.constructor.name}>: + ${a.join(` + +`).replace(/\n/g,` + `)}`)}},"validateVisitor")};return r.prototype=n,r.prototype.constructor=r,r._RULE_NAMES=t,r}function HAe(e,t,r){let n=E(function(){},"derivedConstructor");Yq(n,e+"BaseSemanticsWithDefaults");let i=Object.create(r.prototype);return Br(t,a=>{i[a]=WAe}),n.prototype=i,n.prototype.constructor=n,n}function UAe(e,t){return YAe(e,t)}function YAe(e,t){let r=Bl(t,i=>id(e[i])===!1),n=gr(r,i=>({msg:`Missing visitor method: <${i}> on ${e.constructor.name} CST Visitor.`,type:Yz.MISSING_METHOD,methodName:i}));return wS(n)}function ev(e,t,r,n=!1){rS(r);let i=Xg(this.recordingProdStack),a=id(t)?t:t.DEF,o=new e({definition:[],idx:r});return n&&(o.separator=t.SEP),Rr(t,"MAX_LOOKAHEAD")&&(o.maxLookahead=t.MAX_LOOKAHEAD),this.recordingProdStack.push(o),a.call(this),i.definition.push(o),this.recordingProdStack.pop(),B6}function KAe(e,t){rS(t);let r=Xg(this.recordingProdStack),n=gn(e)===!1,i=n===!1?e:e.DEF,a=new Ro({definition:[],idx:t,ignoreAmbiguities:n&&e.IGNORE_AMBIGUITIES===!0});Rr(e,"MAX_LOOKAHEAD")&&(a.maxLookahead=e.MAX_LOOKAHEAD);let o=h5e(i,l=>id(l.GATE));return a.hasPredicates=o,r.definition.push(a),Br(i,l=>{let u=new Eo({definition:[]});a.definition.push(u),Rr(l,"IGNORE_AMBIGUITIES")?u.ignoreAmbiguities=l.IGNORE_AMBIGUITIES:Rr(l,"GATE")&&(u.ignoreAmbiguities=!0),this.recordingProdStack.push(u),l.ALT.call(this),this.recordingProdStack.pop()}),B6}function jz(e){return e===0?"":`${e}`}function rS(e){if(e<0||e>wSe){let t=new Error(`Invalid DSL Method idx value: <${e}> + Idx value must be a none negative value smaller than ${wSe+1}`);throw t.KNOWN_RECORDER_ERROR=!0,t}}function ZAe(e,t){t.forEach(r=>{let n=r.prototype;Object.getOwnPropertyNames(n).forEach(i=>{if(i==="constructor")return;let a=Object.getOwnPropertyDescriptor(n,i);a&&(a.get||a.set)?Object.defineProperty(e.prototype,i,a):e.prototype[i]=r.prototype[i]})})}function Xz(e=void 0){return function(){return e}}function JAe(e,t){for(var r=-1,n=e==null?0:e.length,i=Array(n);++r-1}function lRe(e,t){var r=this.__data__,n=$6(r,e);return n<0?(++this.size,r.push([e,t])):r[n][1]=t,this}function d0(e){var t=-1,r=e==null?0:e.length;for(this.clear();++tl))return!1;var h=a.get(e),d=a.get(t);if(h&&d)return h==t&&d==e;var f=-1,p=!0,m=r&kCt?new zRe:void 0;for(a.set(e,t),a.set(t,e);++f-1&&e%1==0&&e-1&&e%1==0&&e<=sSt}function h6e(e){return Av(e)&&Zq(e.length)&&!!di[Bv(e)]}function d6e(e){return function(t){return e(t)}}function p6e(e,t){var r=Js(e),n=!r&&V6(e),i=!r&&!n&&zR(e),a=!r&&!n&&!i&&Qq(e),o=r||n||i||a,l=o?XCt(e.length,String):[],u=l.length;for(var h in e)(t||BSt.call(e,h))&&!(o&&(h=="length"||i&&(h=="offset"||h=="parent")||a&&(h=="buffer"||h=="byteLength"||h=="byteOffset")||c6e(h,u)))&&l.push(h);return l}function m6e(e){var t=e&&e.constructor,r=typeof t=="function"&&t.prototype||FSt;return e===r}function y6e(e,t){return function(r){return e(t(r))}}function v6e(e){if(!g6e(e))return VSt(e);var t=[];for(var r in Object(e))qSt.call(e,r)&&r!="constructor"&&t.push(r);return t}function b6e(e){return e!=null&&Zq(e.length)&&!bRe(e)}function T6e(e){return W6(e)?$St(e):x6e(e)}function C6e(e){return WCt(e,Jq,jCt)}function S6e(e,t,r,n,i,a){var o=r&HSt,l=OSe(e),u=l.length,h=OSe(t),d=h.length;if(u!=d&&!o)return!1;for(var f=u;f--;){var p=l[f];if(!(o?p in t:YSt.call(t,p)))return!1}var m=a.get(e),g=a.get(t);if(m&&g)return m==t&&g==e;var y=!0;a.set(e,t),a.set(t,e);for(var v=o;++foH(e,t,o));return y0(e,t,n,r,...i)}function g_e(e,t,r){let n=ba(e,t,r,{type:yp});sd(e,n);let i=y0(e,t,n,r,Sp(e,t,r));return y_e(e,t,r,i)}function Sp(e,t,r){let n=Uwt(Uh(r.definition,i=>oH(e,t,i)),i=>i!==void 0);return n.length===1?n[0]:n.length===0?void 0:x_e(e,n)}function lH(e,t,r,n,i){let a=n.left,o=n.right,l=ba(e,t,r,{type:Zwt});sd(e,l);let u=ba(e,t,r,{type:o_e});return a.loopback=l,u.loopback=l,e.decisionMap[Zg(t,i?"RepetitionMandatoryWithSeparator":"RepetitionMandatory",r.idx)]=l,Hi(o,l),i===void 0?(Hi(l,a),Hi(l,u)):(Hi(l,u),Hi(l,i.left),Hi(i.right,a)),{left:a,right:u}}function cH(e,t,r,n,i){let a=n.left,o=n.right,l=ba(e,t,r,{type:Kwt});sd(e,l);let u=ba(e,t,r,{type:o_e}),h=ba(e,t,r,{type:Xwt});return l.loopback=h,u.loopback=h,Hi(l,a),Hi(l,u),Hi(o,h),i!==void 0?(Hi(h,u),Hi(h,i.left),Hi(i.right,a)):Hi(h,l),e.decisionMap[Zg(t,i?"RepetitionWithSeparator":"Repetition",r.idx)]=l,{left:l,right:u}}function y_e(e,t,r,n){let i=n.left,a=n.right;return Hi(i,a),e.decisionMap[Zg(t,"Option",r.idx)]=i,n}function sd(e,t){return e.decisionStates.push(t),t.decision=e.decisionStates.length-1,t.decision}function y0(e,t,r,n,...i){let a=ba(e,t,n,{type:jwt,start:r});r.end=a;for(let l of i)l!==void 0?(Hi(r,l.left),Hi(l.right,a)):Hi(r,a);let o={left:r,right:a};return e.decisionMap[Zg(t,v_e(n),n.idx)]=r,o}function v_e(e){if(e instanceof Ro)return"Alternation";if(e instanceof as)return"Option";if(e instanceof Gi)return"Repetition";if(e instanceof Ao)return"RepetitionWithSeparator";if(e instanceof nl)return"RepetitionMandatory";if(e instanceof il)return"RepetitionMandatoryWithSeparator";throw new Error("Invalid production type encountered")}function x_e(e,t){let r=t.length;for(let a=0;ar.stateNumber.toString()).join("_")}`}function S_e(e,t,r){for(var n=-1,i=e.length;++n0&&r(l)?t>1?hH(l,t-1,r,n,i):ZRe(i,l):n||(i[i.length]=l)}return i}function R_e(e,t){return A_e(Uh(e,t),1)}function __e(e,t,r,n){for(var i=e.length,a=r+(n?1:-1);n?a--:++a-1}function N_e(e,t,r){for(var n=-1,i=e==null?0:e.length;++n=fkt){var h=t?null:dkt(e);if(h)return Kq(h);o=!1,i=qRe,u=new zRe}else u=t?[]:l;e:for(;++n{let i=n.toString(),a=r[i];return a!==void 0||(a={atnStartState:e,decision:t,states:{}},r[i]=a),a}}function tV(e,t=!0){let r=new Set;for(let n of e){let i=new Set;for(let a of n){if(a===void 0){if(t)break;return!1}let o=[a.tokenTypeIdx].concat(a.categoryMatches);for(let l of o)if(r.has(l)){if(!i.has(l))return!1}else r.add(l),i.add(l)}}return!0}function j_e(e){let t=e.decisionStates.length,r=Array(t);for(let n=0;nUg(i)).join(", "),r=e.production.idx===0?"":e.production.idx,n=`Ambiguous Alternatives Detected: <${e.ambiguityIndices.join(", ")}> in <${J_e(e.production)}${r}> inside <${e.topLevelRule.name}> Rule, +<${t}> may appears as a prefix path in all these alternatives. +`;return n=n+`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#AMBIGUOUS_ALTERNATIVES +For Further details.`,n}function J_e(e){if(e instanceof Zs)return"SUBRULE";if(e instanceof as)return"OPTION";if(e instanceof Ro)return"OR";if(e instanceof nl)return"AT_LEAST_ONE";if(e instanceof il)return"AT_LEAST_ONE_SEP";if(e instanceof Ao)return"MANY_SEP";if(e instanceof Gi)return"MANY";if(e instanceof fi)return"CONSUME";throw Error("non exhaustive match")}function eLe(e,t,r){let n=rkt(t.configs.elements,a=>a.state.transitions),i=mkt(n.filter(a=>a instanceof aH).map(a=>a.tokenType),a=>a.tokenTypeIdx);return{actualToken:r,possibleTokenTypes:i,tokenPath:e}}function tLe(e,t){return e.edges[t.tokenTypeIdx]}function rLe(e,t,r){let n=new eV,i=[];for(let o of e.elements){if(r.is(o.alt)===!1)continue;if(o.state.type===_S){i.push(o);continue}let l=o.state.transitions.length;for(let u=0;u0&&!oLe(a))for(let o of i)a.add(o);return a}function nLe(e,t){if(e instanceof aH&&Fq(t,e.tokenType))return e.target}function iLe(e,t){let r;for(let n of e.elements)if(t.is(n.alt)===!0){if(r===void 0)r=n.alt;else if(r!==n.alt)return}return r}function dH(e){return{configs:e,edges:{},isAcceptState:!1,prediction:-1}}function rV(e,t,r,n){return n=fH(e,n),t.edges[r.tokenTypeIdx]=n,n}function fH(e,t){if(t===VR)return t;let r=t.configs.key,n=e.states[r];return n!==void 0?n:(t.configs.finalize(),e.states[r]=t,t)}function aLe(e){let t=new eV,r=e.transitions.length;for(let n=0;n0){let i=[...e.stack],o={state:i.pop(),alt:e.alt,stack:i};sS(o,t)}else t.add(e);return}r.epsilonOnlyTransitions||t.add(e);let n=r.transitions.length;for(let i=0;i1)return!0;return!1}function dLe(e){for(let t of Array.from(e.values()))if(Object.keys(t).length===1)return!0;return!1}function JA(e){return e.$type===qR}function Q6(e,t,r){return bLe({parser:t,tokens:r,ruleNames:new Map},e),t}function bLe(e,t){let r=f6(t,!1),n=zn(t.rules).filter(Ks).filter(a=>r.has(a));for(let a of n){let o={...e,consume:1,optional:1,subrule:1,many:1,or:1};e.parser.rule(a,vp(o,a.definition))}let i=zn(t.rules).filter(Cv).filter(a=>r.has(a));for(let a of i)e.parser.rule(a,TLe(e,a))}function TLe(e,t){let r=t.call.rule.ref;if(!r)throw new Error("Could not resolve reference to infix operator rule: "+t.call.rule.$refText);if(Ol(r))throw new Error("Cannot use terminal rule in infix expression");let n=t.operators.precedences.flatMap(m=>m.operators),i={$type:"Group",elements:[]},a={$container:i,$type:"Assignment",feature:"parts",operator:"+=",terminal:t.call},o={$container:i,$type:"Group",elements:[],cardinality:"*"};i.elements.push(a,o);let u={$container:o,$type:"Assignment",feature:"operators",operator:"+=",terminal:{$type:"Alternatives",elements:n}},h={...a,$container:o};o.elements.push(u,h);let f=n.map(m=>e.tokens[m.value]).map((m,g)=>({ALT:E(()=>e.parser.consume(g,m,u),"ALT")})),p;return m=>{p??(p=J6(e,r)),e.parser.subrule(0,p,!1,a,m),e.parser.many(0,{DEF:E(()=>{e.parser.alternatives(0,f),e.parser.subrule(1,p,!1,h,m)},"DEF")})}}function vp(e,t,r=!1){let n;if(Xh(t))n=RLe(e,t);else if(pp(t))n=CLe(e,t);else if(jh(t))n=vp(e,t.terminal);else if(r0(t))n=vH(e,t);else if(Kh(t))n=SLe(e,t);else if(i6(t))n=kLe(e,t);else if(l6(t))n=ELe(e,t);else if(n0(t))n=ALe(e,t);else if(TW(t)){let i=e.consume++;n=E(()=>e.parser.consume(i,gp,t),"method")}else throw new u6(t.$cstNode,`Unexpected element type: ${t.$type}`);return xH(e,r?void 0:oS(t),n,t.cardinality)}function CLe(e,t){let r=jg(t);return()=>e.parser.action(r,t)}function SLe(e,t){let r=t.rule.ref;if(t0(r)){let n=e.subrule++,i=Ks(r)&&r.fragment,a=t.arguments.length>0?wLe(r,t.arguments):()=>({}),o;return l=>{o??(o=J6(e,r)),e.parser.subrule(n,o,i,t,a(l))}}else if(Ol(r)){let n=e.consume++,i=HR(e,r.name);return()=>e.parser.consume(n,i,t)}else if(r)bp(r);else throw new u6(t.$cstNode,`Undefined rule: ${t.rule.$refText}`)}function wLe(e,t){if(t.some(n=>n.calledByName)){let n=t.map(i=>({parameterName:i.parameter?.ref?.name,predicate:Tc(i.value)}));return i=>{let a={};for(let{parameterName:o,predicate:l}of n)o&&(a[o]=l(i));return a}}else{let n=t.map(i=>Tc(i.value));return i=>{let a={};for(let o=0;ot(n)||r(n)}else if(xW(e)){let t=Tc(e.left),r=Tc(e.right);return n=>t(n)&&r(n)}else if(wW(e)){let t=Tc(e.value);return r=>!t(r)}else if(kW(e)){let t=e.parameter.ref.name;return r=>r!==void 0&&r[t]===!0}else if(yW(e)){let t=!!e.true;return()=>t}bp(e)}function kLe(e,t){if(t.elements.length===1)return vp(e,t.elements[0]);{let r=[];for(let i of t.elements){let a={ALT:vp(e,i,!0)},o=oS(i);o&&(a.GATE=Tc(o)),r.push(a)}let n=e.or++;return i=>e.parser.alternatives(n,r.map(a=>{let o={ALT:E(()=>a.ALT(i),"ALT")},l=a.GATE;return l&&(o.GATE=()=>l(i)),o}))}}function ELe(e,t){if(t.elements.length===1)return vp(e,t.elements[0]);let r=[];for(let l of t.elements){let u={ALT:vp(e,l,!0)},h=oS(l);h&&(u.GATE=Tc(h)),r.push(u)}let n=e.or++,i=E((l,u)=>{let h=u.getRuleStack().join("-");return`uGroup_${l}_${h}`},"idFunc"),a=E(l=>e.parser.alternatives(n,r.map((u,h)=>{let d={ALT:E(()=>!0,"ALT")},f=e.parser;d.ALT=()=>{if(u.ALT(l),!f.isRecording()){let m=i(n,f);f.unorderedGroups.get(m)||f.unorderedGroups.set(m,[]);let g=f.unorderedGroups.get(m);typeof g?.[h]>"u"&&(g[h]=!0)}};let p=u.GATE;return p?d.GATE=()=>p(l):d.GATE=()=>!f.unorderedGroups.get(i(n,f))?.[h],d})),"alternatives"),o=xH(e,oS(t),a,"*");return l=>{o(l),e.parser.isRecording()||e.parser.unorderedGroups.delete(i(n,e.parser))}}function ALe(e,t){let r=t.elements.map(n=>vp(e,n));return n=>r.forEach(i=>i(n))}function oS(e){if(n0(e))return e.guardCondition}function vH(e,t,r=t.terminal){if(r)if(Kh(r)&&Ks(r.rule.ref)){let n=r.rule.ref,i=e.subrule++,a;return o=>{a??(a=J6(e,n)),e.parser.subrule(i,a,!1,t,o)}}else if(Kh(r)&&Ol(r.rule.ref)){let n=e.consume++,i=HR(e,r.rule.ref.name);return()=>e.parser.consume(n,i,t)}else if(Xh(r)){let n=e.consume++,i=HR(e,r.value);return()=>e.parser.consume(n,i,t)}else throw new Error("Could not build cross reference parser");else{if(!t.type.ref)throw new Error("Could not resolve reference to type: "+t.type.$refText);let i=y6(t.type.ref)?.terminal;if(!i)throw new Error("Could not find name assignment for type: "+jg(t.type.ref));return vH(e,t,i)}}function RLe(e,t){let r=e.consume++,n=e.tokens[t.value];if(!n)throw new Error("Could not find token for keyword: "+t.value);return()=>e.parser.consume(r,n,t)}function xH(e,t,r,n){let i=t&&Tc(t);if(!n)if(i){let a=e.or++;return o=>e.parser.alternatives(a,[{ALT:E(()=>r(o),"ALT"),GATE:E(()=>i(o),"GATE")},{ALT:Xz(),GATE:E(()=>!i(o),"GATE")}])}else return r;if(n==="*"){let a=e.many++;return o=>e.parser.many(a,{DEF:E(()=>r(o),"DEF"),GATE:i?()=>i(o):void 0})}else if(n==="+"){let a=e.many++;if(i){let o=e.or++;return l=>e.parser.alternatives(o,[{ALT:E(()=>e.parser.atLeastOne(a,{DEF:E(()=>r(l),"DEF")}),"ALT"),GATE:E(()=>i(l),"GATE")},{ALT:Xz(),GATE:E(()=>!i(l),"GATE")}])}else return o=>e.parser.atLeastOne(a,{DEF:E(()=>r(o),"DEF")})}else if(n==="?"){let a=e.optional++;return o=>e.parser.optional(a,{DEF:E(()=>r(o),"DEF"),GATE:i?()=>i(o):void 0})}else bp(n)}function J6(e,t){let r=_Le(e,t),n=e.parser.getRule(r);if(!n)throw new Error(`Rule "${r}" not found."`);return n}function _Le(e,t){if(t0(t))return t.name;if(e.ruleNames.has(t))return e.ruleNames.get(t);{let r=t,n=r.$container,i=t.$type;for(;!Ks(n);)(n0(n)||i6(n)||l6(n))&&(i=n.elements.indexOf(r).toString()+":"+i),r=n,n=n.$container;return i=n.name+":"+i,e.ruleNames.set(t,i),i}}function HR(e,t){let r=e.tokens[t];if(!r)throw new Error(`Token "${t}" not found."`);return r}function bH(e){let t=e.Grammar,r=e.parser.Lexer,n=new vLe(e);return Q6(t,n,r.definition),n.finalize(),n}function TH(e){let t=CH(e);return t.finalize(),t}function CH(e){let t=e.Grammar,r=e.parser.Lexer,n=new gLe(e);return Q6(t,n,r.definition)}function t_(){return new Promise(e=>{typeof setImmediate>"u"?setTimeout(e,0):setImmediate(e)})}function r_(){return eR=performance.now(),new ri.CancellationTokenSource}function wH(e){LLe=e}function v0(e){return e===Bu}async function qa(e){if(e===ri.CancellationToken.None)return;let t=performance.now();if(t-eR>=LLe&&(eR=t,await t_(),eR=performance.now()),e.isCancellationRequested)throw Bu}function YR(e,t){if(e.length<=1)return e;let r=e.length/2|0,n=e.slice(0,r),i=e.slice(r);YR(n,t),YR(i,t);let a=0,o=0,l=0;for(;ar.line||t.line===r.line&&t.character>r.character?{start:r,end:t}:e}function DLe(e){let t=EH(e.range);return t!==e.range?{newText:e.newText,range:t}:e}function RH(e){return typeof e.name=="string"}function DH(e){return typeof e.$comment=="string"}function sV(e){return typeof e=="object"&&!!e&&("$ref"in e||"$error"in e)}function qg(e){return{code:e}}function IH(e){if(e.range)return e.range;let t;return typeof e.property=="string"?t=p6(e.node.$cstNode,e.property,e.index):typeof e.keyword=="string"&&(t=tq(e.node.$cstNode,e.keyword,e.index)),t??(t=e.node.$cstNode),t?t.range:{start:{line:0,character:0},end:{line:0,character:0}}}function zC(e){switch(e){case"error":return 1;case"warning":return 2;case"info":return 3;case"hint":return 4;default:throw new Error("Invalid diagnostic severity: "+e)}}function MH(e){switch(e){case"error":return qg(Il.LexingError);case"warning":return qg(Il.LexingWarning);case"info":return qg(Il.LexingInfo);case"hint":return qg(Il.LexingHint);default:throw new Error("Invalid diagnostic severity: "+e)}}function s_(e){return Array.isArray(e)&&(e.length===0||"name"in e[0])}function o_(e){return e&&"modes"in e&&"defaultMode"in e}function KR(e){return!s_(e)&&!o_(e)}function OH(e,t,r){let n,i;typeof e=="string"?(i=t,n=r):(i=e.range.start,n=t),i||(i=Dn.create(0,0));let a=$H(e),o=l_(n),l=tDe({lines:a,position:i,options:o});return iDe({index:0,tokens:l,position:i})}function BH(e,t){let r=l_(t),n=$H(e);if(n.length===0)return!1;let i=n[0],a=n[n.length-1],o=r.start,l=r.end;return!!o?.exec(i)&&!!l?.exec(a)}function $H(e){let t="";return typeof e=="string"?t=e:t=e.text,t.split(nke)}function tDe(e){let t=[],r=e.position.line,n=e.position.character;for(let i=0;i=l.length){if(t.length>0){let d=Dn.create(r,n);t.push({type:"break",content:"",range:hn.create(d,d)})}}else{QSe.lastIndex=u;let d=QSe.exec(l);if(d){let f=d[0],p=d[1],m=Dn.create(r,n+u),g=Dn.create(r,n+u+f.length);t.push({type:"tag",content:p,range:hn.create(m,g)}),u+=f.length,u=ZR(l,u)}if(u0&&t[t.length-1].type==="break"?t.slice(0,-1):t}function rDe(e,t,r,n){let i=[];if(e.length===0){let a=Dn.create(r,n),o=Dn.create(r,n+t.length);i.push({type:"text",content:t,range:hn.create(a,o)})}else{let a=0;for(let l of e){let u=l.index,h=t.substring(a,u);h.length>0&&i.push({type:"text",content:t.substring(a,u),range:hn.create(Dn.create(r,a+n),Dn.create(r,u+n))});let d=h.length+1,f=l[1];if(i.push({type:"inline-tag",content:f,range:hn.create(Dn.create(r,a+d+n),Dn.create(r,a+d+f.length+n))}),d+=f.length,l.length===4){d+=l[2].length;let p=l[3];i.push({type:"text",content:p,range:hn.create(Dn.create(r,a+d+n),Dn.create(r,a+d+p.length+n))})}else i.push({type:"text",content:"",range:hn.create(Dn.create(r,a+d+n),Dn.create(r,a+d+n))});a=u+l[0].length}let o=t.substring(a);o.length>0&&i.push({type:"text",content:o,range:hn.create(Dn.create(r,a+n),Dn.create(r,a+n+o.length))})}return i}function ZR(e,t){let r=e.substring(t).match(Mkt);return r?t+r.index:e.length}function nDe(e){let t=e.match(Nkt);if(t&&typeof t.index=="number")return t.index}function iDe(e){let t=Dn.create(e.position.line,e.position.character);if(e.tokens.length===0)return new JSe([],hn.create(t,t));let r=[];for(;e.index0){let o=ZR(t,n);i=t.substring(o),t=t.substring(0,n)}return(e==="linkcode"||e==="link"&&r.link==="code")&&(i=`\`${i}\``),r.renderLink?.(t,i)??cDe(t,i)}}function cDe(e,t){try{return Jo.parse(e,!0),`[${t}](${e})`}catch{return e}}function lV(e){return e.endsWith(` +`)?` +`:` + +`}function yn(e){return{documentation:{CommentProvider:E(t=>new dDe(t),"CommentProvider"),DocumentationProvider:E(t=>new hDe(t),"DocumentationProvider")},parser:{AsyncParser:E(t=>new fDe(t),"AsyncParser"),GrammarConfig:E(t=>cq(t),"GrammarConfig"),LangiumParser:E(t=>TH(t),"LangiumParser"),CompletionParser:E(t=>bH(t),"CompletionParser"),ValueConverter:E(()=>new SH,"ValueConverter"),TokenBuilder:E(()=>new e_,"TokenBuilder"),Lexer:E(t=>new PH(t),"Lexer"),ParserErrorMessageProvider:E(()=>new yH,"ParserErrorMessageProvider"),LexerErrorMessageProvider:E(()=>new eDe,"LexerErrorMessageProvider")},workspace:{AstNodeLocator:E(()=>new XLe,"AstNodeLocator"),AstNodeDescriptionProvider:E(t=>new YLe(t),"AstNodeDescriptionProvider"),ReferenceDescriptionProvider:E(t=>new jLe(t),"ReferenceDescriptionProvider")},references:{Linker:E(t=>new PLe(t),"Linker"),NameProvider:E(()=>new OLe,"NameProvider"),ScopeProvider:E(t=>new zLe(t),"ScopeProvider"),ScopeComputation:E(t=>new $Le(t),"ScopeComputation"),References:E(t=>new BLe(t),"References")},serializer:{Hydrator:E(t=>new mDe(t),"Hydrator"),JsonSerializer:E(t=>new VLe(t),"JsonSerializer")},validation:{DocumentValidator:E(t=>new ULe(t),"DocumentValidator"),ValidationRegistry:E(t=>new qLe(t),"ValidationRegistry")},shared:E(()=>e.shared,"shared")}}function vn(e){return{ServiceRegistry:E(t=>new WLe(t),"ServiceRegistry"),workspace:{LangiumDocuments:E(t=>new NLe(t),"LangiumDocuments"),LangiumDocumentFactory:E(t=>new MLe(t),"LangiumDocumentFactory"),DocumentBuilder:E(t=>new ZLe(t),"DocumentBuilder"),IndexManager:E(t=>new QLe(t),"IndexManager"),WorkspaceManager:E(t=>new JLe(t),"WorkspaceManager"),FileSystemProvider:E(t=>e.fileSystemProvider(t),"FileSystemProvider"),WorkspaceLock:E(()=>new pDe,"WorkspaceLock"),ConfigurationProvider:E(t=>new KLe(t),"ConfigurationProvider")},profilers:{}}}function $r(e,t,r,n,i,a,o,l,u){let h=[e,t,r,n,i,a,o,l,u].reduce(Rv,{});return WH(h)}function VH(e){if(e&&e[gDe])for(let t of Object.values(e))VH(t);return e}function WH(e,t){let r=new Proxy({},{deleteProperty:E(()=>!1,"deleteProperty"),set:E(()=>{throw new Error("Cannot set property on injected service container")},"set"),get:E((n,i)=>i===gDe?!0:uV(n,i,e,t||r),"get"),getOwnPropertyDescriptor:E((n,i)=>(uV(n,i,e,t||r),Object.getOwnPropertyDescriptor(n,i)),"getOwnPropertyDescriptor"),has:E((n,i)=>i in e,"has"),ownKeys:E(()=>[...Object.getOwnPropertyNames(e)],"ownKeys")});return r}function uV(e,t,r,n){if(t in e){if(e[t]instanceof Error)throw new Error("Construction failure. Please make sure that your dependencies are constructable. Cause: "+e[t]);if(e[t]===ewe)throw new Error('Cycle detected. Please make "'+String(t)+'" lazy. Visit https://langium.org/docs/reference/configuration-services/#resolving-cyclic-dependencies');return e[t]}else if(t in r){let i=r[t];e[t]=ewe;try{e[t]=typeof i=="function"?i(n):WH(i,n)}catch(a){throw e[t]=a instanceof Error?a:void 0,a}return e[t]}else return}function Rv(e,t){if(t){for(let[r,n]of Object.entries(t))if(n!=null)if(typeof n=="object"){let i=e[r];typeof i=="object"&&i!==null?e[r]=Rv(i,n):e[r]=Rv({},n)}else e[r]=n}return e}function xDe(){let e=$r(vn(Cn),Fkt),t=$r(yn({shared:e}),$kt);return e.ServiceRegistry.register(t),t}function Ha(e){let t=xDe(),r=t.serializer.JsonSerializer.deserialize(e);return t.shared.workspace.LangiumDocumentFactory.fromModel(r,Jo.parse(`memory:/${r.name??"grammar"}.langium`)),r}function TDe(e){return Ui.isInstance(e,xc.$type)}function CDe(e){return Ui.isInstance(e,VC.$type)}function SDe(e){return Ui.isInstance(e,Mg.$type)}function wDe(e){return Ui.isInstance(e,hp.$type)}function kDe(e){return Ui.isInstance(e,WC.$type)}function EDe(e){return Ui.isInstance(e,QR.$type)}function ADe(e){return e==="rmo"||e==="readmodel"||e==="ui"||e==="cmd"||e==="command"||e==="evt"||e==="event"||e==="pcr"||e==="processor"}function c_(e){return Ui.isInstance(e,Wh.$type)}function RDe(e){return Ui.isInstance(e,dp.$type)}function _De(e){return Ui.isInstance(e,dv.$type)}function LDe(e){return Ui.isInstance(e,Ng.$type)}function DDe(e){return Ui.isInstance(e,Pg.$type)}function IDe(e){return Ui.isInstance(e,Og.$type)}function MDe(e){return Ui.isInstance(e,fp.$type)}function NDe(e){return Ui.isInstance(e,qC.$type)}function PDe(e){return Ui.isInstance(e,Bg.$type)}function ODe(e){return Ui.isInstance(e,$g.$type)}function BDe(e){return Ui.isInstance(e,Fg.$type)}function $De(e){return Ui.isInstance(e,Gg.$type)}function FDe(e){return Ui.isInstance(e,fv.$type)}function GDe(e){return Ui.isInstance(e,zg.$type)}function zDe(e){return Ui.isInstance(e,Va.$type)}var wgt,lS,kgt,sW,Egt,Agt,E,Rgt,Hr,xp,uR,JR,oW,lW,e6,RG,PA,_G,mC,Dn,hn,gC,LG,OA,DG,IG,MG,NG,BA,PG,OG,BG,yC,hg,Lu,dg,ns,zh,vC,X1,K1,Z1,$A,sC,sG,bwe,$G,FG,xC,GG,FA,Q1,zG,VG,WG,qG,HG,UG,YG,jG,bC,XG,KG,ZG,QG,JG,ez,tz,rz,nz,iz,az,TC,sz,oz,lz,cz,uz,hz,dz,fz,pz,mz,gz,yz,vz,GA,zA,xz,bz,Tz,Cz,Sz,wz,kz,Ez,Twe,Az,_Ce,rt,cS,Qg,uS,_v,t6,Swe,kwe,_gt,Lgt,Ewe,Dgt,Igt,Mgt,Ngt,Rz,Pgt,Lv,LCe,_i,cW,Ogt,Bgt,$gt,Fgt,Ggt,zgt,Vgt,Wgt,qgt,Hgt,Ugt,Ygt,jgt,Xgt,Kgt,Zgt,Qgt,Jgt,e0t,t0t,r0t,n0t,i0t,a0t,s0t,_we,uW,dW,Ou,vv,Us,xv,UC,fW,Iwe,o0t,So,AC,iv,Qo,sp,RC,pR,mR,op,gR,lp,cp,_C,up,LC,yR,qh,vR,Cg,xR,Mu,DC,bR,av,sv,ov,Sg,TR,CR,lv,SR,vc,IC,wg,wR,kg,cv,kR,Eg,wo,Ag,Hh,Rg,MC,_g,Lg,ER,NC,Dg,Ig,uv,NW,Er,Pu,$W,WW,u6,HW,RR,_R,DCe,l0t,yA,c0t,rke,h6,nke,ike,u0t,Vg,ske,lq,h0t,xke,d0t,f0t,zu,p0t,Nl,bke,m0t,g0t,oC,y0t,v0t,x0t,b0t,T0t,C0t,ICe,Tp,Ec,S0t,v6,gS,w0t,gn,k0t,MCe,NCe,E0t,A0t,R0t,_0t,L0t,Pl,PCe,D0t,I0t,M0t,N0t,P0t,OCe,O0t,B0t,yS,vS,$0t,F0t,G0t,z0t,id,V0t,oG,BCe,W0t,q0t,H0t,a0,U0t,Y0t,j0t,X0t,K0t,Z0t,Q0t,J0t,eyt,s0,tyt,Dz,$Ce,ryt,nyt,iyt,Wa,ayt,syt,oyt,lyt,cyt,uyt,hyt,DR,dyt,fyt,pyt,myt,Uke,jke,gyt,yyt,hq,Jke,vyt,xyt,x6,dq,xS,byt,Tyt,b6,bS,FCe,Cyt,fq,Syt,pq,Vu,T6,wyt,kyt,TS,Eyt,Ayt,GCe,pEe,Ryt,_yt,Lyt,C6,Dyt,gEe,zCe,Iyt,VCe,Myt,Nyt,jC,Pyt,Oyt,Byt,$yt,Fyt,Gyt,zyt,Vyt,Wyt,qyt,Hyt,Uyt,Yyt,jyt,Xyt,Kyt,Zyt,Qyt,Jyt,e1t,t1t,r1t,n1t,i1t,hi,a1t,CS,xEe,PC,s1t,lG,o1t,mp,WCe,l1t,mq,c1t,u1t,TEe,SEe,h1t,d1t,f1t,p1t,kEe,el,m1t,g1t,y1t,tl,v1t,x1t,b1t,T1t,S6,C1t,S1t,gq,w1t,XC,k1t,E1t,A1t,R1t,_1t,L1t,D1t,I1t,M1t,N1t,P1t,qCe,O1t,w6,B1t,$1t,F1t,G1t,z1t,V1t,k6,W1t,KC,q1t,H1t,E6,U1t,Y1t,j1t,X1t,A6,K1t,Z1t,Q1t,J1t,evt,tvt,rvt,nvt,ivt,_6,avt,SS,yq,svt,vq,HCe,ovt,bq,wc,lvt,i4e,s4e,cvt,uvt,hvt,dvt,fvt,pvt,mvt,OC,gvt,yvt,m4e,UCe,vvt,YCe,jCe,xvt,Tq,x4e,bvt,Tvt,XCe,Cvt,Cq,Svt,wvt,kvt,T4e,Evt,w4e,Iz,A4e,Avt,Mz,Rvt,Nz,_vt,mv,KCe,Lvt,ZCe,QCe,JCe,eSe,Dvt,Ivt,Mvt,Nvt,Pvt,pg,kv,Ovt,Bvt,$vt,Fvt,IR,Sq,Gvt,zvt,Vvt,tSe,rSe,Wvt,qvt,Hvt,Uvt,Yvt,jvt,Xvt,Kvt,Zvt,Qvt,Jvt,ext,txt,rxt,nxt,ixt,axt,sxt,oxt,lxt,cxt,uxt,hxt,dxt,fxt,nSe,pxt,mxt,gxt,yxt,iSe,vxt,xxt,bxt,Txt,Cxt,$4e,Sxt,wxt,kxt,Ext,F4e,Axt,Rxt,_xt,G4e,Lxt,Dxt,Ixt,Mxt,Nxt,Pxt,Oxt,Bxt,$xt,Fxt,Gxt,zxt,Vxt,Wxt,qxt,Hxt,ti,Uxt,Yxt,ss,wS,jxt,Xxt,Kxt,wq,U4e,kq,Zxt,Qxt,X4e,Jxt,Eq,ebt,tbt,rbt,nbt,ibt,abt,sbt,obt,lbt,cbt,ubt,hbt,dbt,aSe,cG,fbt,pbt,mbt,gbt,ybt,vbt,sSe,oSe,vA,xbt,lSe,bbt,t3e,Tbt,Cbt,Sbt,i3e,wbt,o3e,kbt,Ebt,h3e,Abt,Rbt,_bt,Lbt,Dbt,Ibt,Mbt,Wu,Nbt,Pbt,Obt,Bbt,$bt,Fbt,Gbt,h0,zbt,Vbt,w3e,Wbt,qbt,Rq,cSe,A3e,Hbt,Ubt,Ybt,L6,Xg,is,QC,jbt,Br,Xbt,Kbt,kc,$3e,Bl,Zbt,Qbt,Jbt,e2t,Ev,Ac,t2t,gr,Ml,r2t,n2t,i2t,a2t,s2t,o2t,l2t,Rr,c2t,ko,u2t,xa,h2t,eo,d2t,uSe,f2t,p2t,m2t,g2t,ni,y2t,v2t,hSe,x2t,Zh,Qh,b2t,T2t,C2t,S2t,Rc,w2t,rl,D6,k2t,h5e,E2t,A2t,R2t,_2t,L2t,_q,qu,Zs,Iv,Eo,as,nl,il,Gi,Ao,Ro,fi,Mv,I6,C5e,D2t,HA,I2t,E5e,NR,M2t,Kg,wC,xA,N2t,P2t,O2t,kC,YA,dSe,Z5e,Fz,zi,EC,Xs,B2t,fSe,pSe,mSe,gSe,ySe,vSe,xSe,bSe,gp,hv,$2t,Wg,F2t,G2t,z2t,N6,V2t,TSe,W2t,CSe,Ri,q2t,dAe,H2t,Hq,U2t,LAe,DAe,IAe,MAe,NAe,O6,PAe,Y2t,j2t,X2t,uG,OAe,K2t,Z2t,Q2t,Cp,J2t,$Ae,FAe,Vz,Wz,qz,XA,J8r,Uq,eTt,tTt,bA,rTt,Yz,nTt,iTt,aTt,sTt,oTt,lTt,B6,SSe,wSe,jAe,XAe,cTt,uTt,hTt,FR,ed,GR,Qs,jq,dTt,eRe,fTt,nRe,$6,pTt,mTt,gTt,yTt,vTt,xTt,F6,bTt,TTt,CTt,STt,wTt,fRe,kTt,ETt,ad,ATt,Gu,pRe,RTt,_Tt,lC,LTt,DTt,ITt,MTt,NTt,PTt,kSe,Bv,Xq,OTt,BTt,$Tt,FTt,bRe,GTt,hG,ESe,zTt,VTt,WTt,f0,qTt,HTt,UTt,YTt,jTt,XTt,KTt,ZTt,QTt,$v,JTt,nS,eCt,iS,tCt,rCt,nCt,iCt,aCt,sCt,oCt,lCt,cCt,uCt,hCt,ASe,dCt,fCt,G6,pCt,mCt,gCt,yCt,z6,vCt,xCt,ZA,bCt,TCt,CCt,zRe,SCt,qRe,wCt,kCt,URe,ECt,RSe,ACt,Kq,RCt,_Ct,LCt,DCt,ICt,MCt,NCt,PCt,OCt,BCt,$Ct,FCt,GCt,_Se,dG,zCt,ZRe,VCt,Js,WCt,e6e,qCt,HCt,UCt,LSe,YCt,jCt,XCt,Av,KCt,DSe,a6e,ZCt,QCt,JCt,V6,eSt,o6e,ISe,tSt,MSe,rSt,nSt,zR,iSt,aSt,c6e,sSt,Zq,oSt,lSt,cSt,uSt,hSt,dSt,fSt,pSt,mSt,gSt,ySt,vSt,xSt,bSt,TSt,CSt,SSt,wSt,kSt,ESt,ASt,RSt,_St,LSt,di,DSt,ISt,f6e,GC,MSt,fG,NSt,NSe,PSe,PSt,Qq,OSt,BSt,$St,FSt,g6e,GSt,zSt,VSt,WSt,qSt,x6e,W6,Jq,OSe,HSt,USt,YSt,jSt,XSt,Kz,KSt,Zz,ZSt,yv,QSt,Qz,BSe,JSt,$Se,FSe,GSe,zSe,ewt,twt,rwt,nwt,iwt,mg,Jz,awt,VSe,WSe,TA,swt,qSe,owt,k6e,lwt,cwt,uwt,R6e,hwt,D6e,dwt,fwt,q6,pwt,mwt,tH,gwt,ywt,vwt,xwt,bwt,Twt,Cwt,Swt,wwt,HSe,USe,kwt,Ewt,$6e,Awt,U6,z6e,Rwt,_wt,Lwt,Dwt,Iwt,Mwt,Nwt,nH,Pwt,Owt,Bwt,Y6,$wt,Fwt,Gwt,zwt,Vwt,Wwt,j6,qwt,Uh,Hwt,Uwt,yp,Ywt,a_e,s_e,_S,jwt,Xwt,Kwt,Zwt,o_e,iH,aH,l_e,sH,VR,eV,Qwt,Jwt,ekt,YSe,tkt,A_e,rkt,nkt,ikt,akt,skt,okt,lkt,ckt,ukt,hkt,dkt,fkt,pkt,mkt,gkt,ykt,vkt,pG,xkt,bkt,Tkt,Ckt,Skt,wkt,kkt,jSe,Y_e,XSe,Ekt,fLe,pH,WR,Z6,Akt,mH,qR,KSe,mLe,gH,gLe,yLe,yH,vLe,Rkt,xLe,_kt,e_,SH,Nu,ri,eR,LLe,Bu,td,ZSe,UR,ILe,Jo,cC,js,AH,nn,MLe,NLe,gg,PLe,OLe,BLe,rd,jR,$Le,aV,Lkt,FLe,Dkt,n_,_H,i_,GLe,LH,zLe,VLe,WLe,XR,qLe,HLe,ULe,Il,YLe,jLe,XLe,a_,KLe,CA,Yg,ZLe,QLe,JLe,eDe,NH,PH,QSe,Ikt,Mkt,Nkt,JSe,mG,oV,uDe,hDe,dDe,fDe,Pkt,Okt,pDe,mDe,cV,gDe,ewe,hV,Hg,yDe,Bkt,qH,vDe,Cn,$kt,Fkt,Gkt,bDe,dV,fV,pV,mV,gV,yV,vV,xV,bV,TV,CV,SV,wV,kV,EV,eMr,AV,RV,rR,_V,LV,DV,yg,nR,IV,MV,SA,gG,wA,uC,yG,xc,kA,VC,twe,EA,vG,Mg,AA,cg,RA,hp,_A,rwe,Y1,WC,QR,NV,PV,OV,BV,$V,FV,GV,tv,np,zV,iR,VV,WV,aR,qV,HV,_u,vg,ip,hC,nwe,xG,LA,Wh,rp,bG,Du,iwe,DA,TG,dp,dC,dv,fC,CG,pC,IA,ug,Ng,MA,SG,Pg,Og,UV,YV,jV,XV,KV,sR,rv,oR,ZV,lR,fp,qC,wG,NA,ap,Bg,$g,QV,Fg,Iu,JV,eW,tW,Gg,cR,rW,nW,iW,aW,kG,j1,EG,xg,fv,zg,AG,bg,nv,Va,VDe,Ui,awe,zkt,swe,Vkt,owe,Wkt,lwe,qkt,cwe,Hkt,uwe,Ukt,hwe,Ykt,dwe,jkt,fwe,Xkt,pwe,Kkt,mwe,Zkt,gwe,Qkt,ywe,Jkt,vwe,eEt,xwe,tEt,rEt,nEt,iEt,aEt,sEt,oEt,lEt,cEt,uEt,hEt,dEt,fEt,pEt,mEt,gEt,In,HH,UH,YH,jH,XH,KH,ZH,QH,JH,eU,tU,rU,nU,iU,aU,yEt,vEt,xEt,bEt,oa,al,Bn,TEt,Sn=F(()=>{"use strict";wgt=Object.create,lS=Object.defineProperty,kgt=Object.getOwnPropertyDescriptor,sW=Object.getOwnPropertyNames,Egt=Object.getPrototypeOf,Agt=Object.prototype.hasOwnProperty,E=s((e,t)=>lS(e,"name",{value:t,configurable:!0}),"__name"),Rgt=s((e,t)=>s(function(){return e&&(t=(0,e[sW(e)[0]])(e=0)),t},"__init"),"__esm"),Hr=s((e,t)=>s(function(){return t||(0,e[sW(e)[0]])((t={exports:{}}).exports,t),t.exports},"__require"),"__commonJS"),xp=s((e,t)=>{for(var r in t)lS(e,r,{get:t[r],enumerable:!0})},"__export"),uR=s((e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of sW(t))!Agt.call(e,i)&&i!==r&&lS(e,i,{get:s(()=>t[i],"get"),enumerable:!(n=kgt(t,i))||n.enumerable});return e},"__copyProps"),JR=s((e,t,r)=>(uR(e,t,"default"),r&&uR(r,t,"default")),"__reExport"),oW=s((e,t,r)=>(r=e!=null?wgt(Egt(e)):{},uR(t||!e||!e.__esModule?lS(r,"default",{value:e,enumerable:!0}):r,e)),"__toESM"),lW=s(e=>uR(lS({},"__esModule",{value:!0}),e),"__toCommonJS"),e6={};xp(e6,{AnnotatedTextEdit:s(()=>zh,"AnnotatedTextEdit"),ChangeAnnotation:s(()=>dg,"ChangeAnnotation"),ChangeAnnotationIdentifier:s(()=>ns,"ChangeAnnotationIdentifier"),CodeAction:s(()=>oz,"CodeAction"),CodeActionContext:s(()=>sz,"CodeActionContext"),CodeActionKind:s(()=>az,"CodeActionKind"),CodeActionTriggerKind:s(()=>TC,"CodeActionTriggerKind"),CodeDescription:s(()=>BG,"CodeDescription"),CodeLens:s(()=>lz,"CodeLens"),Color:s(()=>OA,"Color"),ColorInformation:s(()=>DG,"ColorInformation"),ColorPresentation:s(()=>IG,"ColorPresentation"),Command:s(()=>hg,"Command"),CompletionItem:s(()=>YG,"CompletionItem"),CompletionItemKind:s(()=>zG,"CompletionItemKind"),CompletionItemLabelDetails:s(()=>UG,"CompletionItemLabelDetails"),CompletionItemTag:s(()=>WG,"CompletionItemTag"),CompletionList:s(()=>jG,"CompletionList"),CreateFile:s(()=>X1,"CreateFile"),DeleteFile:s(()=>Z1,"DeleteFile"),Diagnostic:s(()=>yC,"Diagnostic"),DiagnosticRelatedInformation:s(()=>BA,"DiagnosticRelatedInformation"),DiagnosticSeverity:s(()=>PG,"DiagnosticSeverity"),DiagnosticTag:s(()=>OG,"DiagnosticTag"),DocumentHighlight:s(()=>JG,"DocumentHighlight"),DocumentHighlightKind:s(()=>QG,"DocumentHighlightKind"),DocumentLink:s(()=>uz,"DocumentLink"),DocumentSymbol:s(()=>iz,"DocumentSymbol"),DocumentUri:s(()=>RG,"DocumentUri"),EOL:s(()=>Twe,"EOL"),FoldingRange:s(()=>NG,"FoldingRange"),FoldingRangeKind:s(()=>MG,"FoldingRangeKind"),FormattingOptions:s(()=>cz,"FormattingOptions"),Hover:s(()=>XG,"Hover"),InlayHint:s(()=>xz,"InlayHint"),InlayHintKind:s(()=>GA,"InlayHintKind"),InlayHintLabelPart:s(()=>zA,"InlayHintLabelPart"),InlineCompletionContext:s(()=>kz,"InlineCompletionContext"),InlineCompletionItem:s(()=>Tz,"InlineCompletionItem"),InlineCompletionList:s(()=>Cz,"InlineCompletionList"),InlineCompletionTriggerKind:s(()=>Sz,"InlineCompletionTriggerKind"),InlineValueContext:s(()=>vz,"InlineValueContext"),InlineValueEvaluatableExpression:s(()=>yz,"InlineValueEvaluatableExpression"),InlineValueText:s(()=>mz,"InlineValueText"),InlineValueVariableLookup:s(()=>gz,"InlineValueVariableLookup"),InsertReplaceEdit:s(()=>qG,"InsertReplaceEdit"),InsertTextFormat:s(()=>VG,"InsertTextFormat"),InsertTextMode:s(()=>HG,"InsertTextMode"),Location:s(()=>gC,"Location"),LocationLink:s(()=>LG,"LocationLink"),MarkedString:s(()=>bC,"MarkedString"),MarkupContent:s(()=>Q1,"MarkupContent"),MarkupKind:s(()=>FA,"MarkupKind"),OptionalVersionedTextDocumentIdentifier:s(()=>xC,"OptionalVersionedTextDocumentIdentifier"),ParameterInformation:s(()=>KG,"ParameterInformation"),Position:s(()=>Dn,"Position"),Range:s(()=>hn,"Range"),RenameFile:s(()=>K1,"RenameFile"),SelectedCompletionInfo:s(()=>wz,"SelectedCompletionInfo"),SelectionRange:s(()=>hz,"SelectionRange"),SemanticTokenModifiers:s(()=>fz,"SemanticTokenModifiers"),SemanticTokenTypes:s(()=>dz,"SemanticTokenTypes"),SemanticTokens:s(()=>pz,"SemanticTokens"),SignatureInformation:s(()=>ZG,"SignatureInformation"),StringValue:s(()=>bz,"StringValue"),SymbolInformation:s(()=>rz,"SymbolInformation"),SymbolKind:s(()=>ez,"SymbolKind"),SymbolTag:s(()=>tz,"SymbolTag"),TextDocument:s(()=>Az,"TextDocument"),TextDocumentEdit:s(()=>vC,"TextDocumentEdit"),TextDocumentIdentifier:s(()=>$G,"TextDocumentIdentifier"),TextDocumentItem:s(()=>GG,"TextDocumentItem"),TextEdit:s(()=>Lu,"TextEdit"),URI:s(()=>PA,"URI"),VersionedTextDocumentIdentifier:s(()=>FG,"VersionedTextDocumentIdentifier"),WorkspaceChange:s(()=>bwe,"WorkspaceChange"),WorkspaceEdit:s(()=>$A,"WorkspaceEdit"),WorkspaceFolder:s(()=>Ez,"WorkspaceFolder"),WorkspaceSymbol:s(()=>nz,"WorkspaceSymbol"),integer:s(()=>_G,"integer"),uinteger:s(()=>mC,"uinteger")});cS=Rgt({"../../node_modules/.pnpm/vscode-languageserver-types@3.17.5/node_modules/vscode-languageserver-types/lib/esm/main.js"(){"use strict";(function(e){function t(r){return typeof r=="string"}s(t,"is"),E(t,"is"),e.is=t})(RG||(RG={})),(function(e){function t(r){return typeof r=="string"}s(t,"is"),E(t,"is"),e.is=t})(PA||(PA={})),(function(e){e.MIN_VALUE=-2147483648,e.MAX_VALUE=2147483647;function t(r){return typeof r=="number"&&e.MIN_VALUE<=r&&r<=e.MAX_VALUE}s(t,"is"),E(t,"is"),e.is=t})(_G||(_G={})),(function(e){e.MIN_VALUE=0,e.MAX_VALUE=2147483647;function t(r){return typeof r=="number"&&e.MIN_VALUE<=r&&r<=e.MAX_VALUE}s(t,"is"),E(t,"is"),e.is=t})(mC||(mC={})),(function(e){function t(n,i){return n===Number.MAX_VALUE&&(n=mC.MAX_VALUE),i===Number.MAX_VALUE&&(i=mC.MAX_VALUE),{line:n,character:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&rt.uinteger(i.line)&&rt.uinteger(i.character)}s(r,"is"),E(r,"is"),e.is=r})(Dn||(Dn={})),(function(e){function t(n,i,a,o){if(rt.uinteger(n)&&rt.uinteger(i)&&rt.uinteger(a)&&rt.uinteger(o))return{start:Dn.create(n,i),end:Dn.create(a,o)};if(Dn.is(n)&&Dn.is(i))return{start:n,end:i};throw new Error(`Range#create called with invalid arguments[${n}, ${i}, ${a}, ${o}]`)}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&Dn.is(i.start)&&Dn.is(i.end)}s(r,"is"),E(r,"is"),e.is=r})(hn||(hn={})),(function(e){function t(n,i){return{uri:n,range:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&hn.is(i.range)&&(rt.string(i.uri)||rt.undefined(i.uri))}s(r,"is"),E(r,"is"),e.is=r})(gC||(gC={})),(function(e){function t(n,i,a,o){return{targetUri:n,targetRange:i,targetSelectionRange:a,originSelectionRange:o}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&hn.is(i.targetRange)&&rt.string(i.targetUri)&&hn.is(i.targetSelectionRange)&&(hn.is(i.originSelectionRange)||rt.undefined(i.originSelectionRange))}s(r,"is"),E(r,"is"),e.is=r})(LG||(LG={})),(function(e){function t(n,i,a,o){return{red:n,green:i,blue:a,alpha:o}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&rt.numberRange(i.red,0,1)&&rt.numberRange(i.green,0,1)&&rt.numberRange(i.blue,0,1)&&rt.numberRange(i.alpha,0,1)}s(r,"is"),E(r,"is"),e.is=r})(OA||(OA={})),(function(e){function t(n,i){return{range:n,color:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&hn.is(i.range)&&OA.is(i.color)}s(r,"is"),E(r,"is"),e.is=r})(DG||(DG={})),(function(e){function t(n,i,a){return{label:n,textEdit:i,additionalTextEdits:a}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&rt.string(i.label)&&(rt.undefined(i.textEdit)||Lu.is(i))&&(rt.undefined(i.additionalTextEdits)||rt.typedArray(i.additionalTextEdits,Lu.is))}s(r,"is"),E(r,"is"),e.is=r})(IG||(IG={})),(function(e){e.Comment="comment",e.Imports="imports",e.Region="region"})(MG||(MG={})),(function(e){function t(n,i,a,o,l,u){let h={startLine:n,endLine:i};return rt.defined(a)&&(h.startCharacter=a),rt.defined(o)&&(h.endCharacter=o),rt.defined(l)&&(h.kind=l),rt.defined(u)&&(h.collapsedText=u),h}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&rt.uinteger(i.startLine)&&rt.uinteger(i.startLine)&&(rt.undefined(i.startCharacter)||rt.uinteger(i.startCharacter))&&(rt.undefined(i.endCharacter)||rt.uinteger(i.endCharacter))&&(rt.undefined(i.kind)||rt.string(i.kind))}s(r,"is"),E(r,"is"),e.is=r})(NG||(NG={})),(function(e){function t(n,i){return{location:n,message:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&gC.is(i.location)&&rt.string(i.message)}s(r,"is"),E(r,"is"),e.is=r})(BA||(BA={})),(function(e){e.Error=1,e.Warning=2,e.Information=3,e.Hint=4})(PG||(PG={})),(function(e){e.Unnecessary=1,e.Deprecated=2})(OG||(OG={})),(function(e){function t(r){let n=r;return rt.objectLiteral(n)&&rt.string(n.href)}s(t,"is"),E(t,"is"),e.is=t})(BG||(BG={})),(function(e){function t(n,i,a,o,l,u){let h={range:n,message:i};return rt.defined(a)&&(h.severity=a),rt.defined(o)&&(h.code=o),rt.defined(l)&&(h.source=l),rt.defined(u)&&(h.relatedInformation=u),h}s(t,"create"),E(t,"create"),e.create=t;function r(n){var i;let a=n;return rt.defined(a)&&hn.is(a.range)&&rt.string(a.message)&&(rt.number(a.severity)||rt.undefined(a.severity))&&(rt.integer(a.code)||rt.string(a.code)||rt.undefined(a.code))&&(rt.undefined(a.codeDescription)||rt.string((i=a.codeDescription)===null||i===void 0?void 0:i.href))&&(rt.string(a.source)||rt.undefined(a.source))&&(rt.undefined(a.relatedInformation)||rt.typedArray(a.relatedInformation,BA.is))}s(r,"is"),E(r,"is"),e.is=r})(yC||(yC={})),(function(e){function t(n,i,...a){let o={title:n,command:i};return rt.defined(a)&&a.length>0&&(o.arguments=a),o}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&rt.string(i.title)&&rt.string(i.command)}s(r,"is"),E(r,"is"),e.is=r})(hg||(hg={})),(function(e){function t(a,o){return{range:a,newText:o}}s(t,"replace"),E(t,"replace"),e.replace=t;function r(a,o){return{range:{start:a,end:a},newText:o}}s(r,"insert"),E(r,"insert"),e.insert=r;function n(a){return{range:a,newText:""}}s(n,"del"),E(n,"del"),e.del=n;function i(a){let o=a;return rt.objectLiteral(o)&&rt.string(o.newText)&&hn.is(o.range)}s(i,"is"),E(i,"is"),e.is=i})(Lu||(Lu={})),(function(e){function t(n,i,a){let o={label:n};return i!==void 0&&(o.needsConfirmation=i),a!==void 0&&(o.description=a),o}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&rt.string(i.label)&&(rt.boolean(i.needsConfirmation)||i.needsConfirmation===void 0)&&(rt.string(i.description)||i.description===void 0)}s(r,"is"),E(r,"is"),e.is=r})(dg||(dg={})),(function(e){function t(r){let n=r;return rt.string(n)}s(t,"is"),E(t,"is"),e.is=t})(ns||(ns={})),(function(e){function t(a,o,l){return{range:a,newText:o,annotationId:l}}s(t,"replace"),E(t,"replace"),e.replace=t;function r(a,o,l){return{range:{start:a,end:a},newText:o,annotationId:l}}s(r,"insert"),E(r,"insert"),e.insert=r;function n(a,o){return{range:a,newText:"",annotationId:o}}s(n,"del"),E(n,"del"),e.del=n;function i(a){let o=a;return Lu.is(o)&&(dg.is(o.annotationId)||ns.is(o.annotationId))}s(i,"is"),E(i,"is"),e.is=i})(zh||(zh={})),(function(e){function t(n,i){return{textDocument:n,edits:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&xC.is(i.textDocument)&&Array.isArray(i.edits)}s(r,"is"),E(r,"is"),e.is=r})(vC||(vC={})),(function(e){function t(n,i,a){let o={kind:"create",uri:n};return i!==void 0&&(i.overwrite!==void 0||i.ignoreIfExists!==void 0)&&(o.options=i),a!==void 0&&(o.annotationId=a),o}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&i.kind==="create"&&rt.string(i.uri)&&(i.options===void 0||(i.options.overwrite===void 0||rt.boolean(i.options.overwrite))&&(i.options.ignoreIfExists===void 0||rt.boolean(i.options.ignoreIfExists)))&&(i.annotationId===void 0||ns.is(i.annotationId))}s(r,"is"),E(r,"is"),e.is=r})(X1||(X1={})),(function(e){function t(n,i,a,o){let l={kind:"rename",oldUri:n,newUri:i};return a!==void 0&&(a.overwrite!==void 0||a.ignoreIfExists!==void 0)&&(l.options=a),o!==void 0&&(l.annotationId=o),l}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&i.kind==="rename"&&rt.string(i.oldUri)&&rt.string(i.newUri)&&(i.options===void 0||(i.options.overwrite===void 0||rt.boolean(i.options.overwrite))&&(i.options.ignoreIfExists===void 0||rt.boolean(i.options.ignoreIfExists)))&&(i.annotationId===void 0||ns.is(i.annotationId))}s(r,"is"),E(r,"is"),e.is=r})(K1||(K1={})),(function(e){function t(n,i,a){let o={kind:"delete",uri:n};return i!==void 0&&(i.recursive!==void 0||i.ignoreIfNotExists!==void 0)&&(o.options=i),a!==void 0&&(o.annotationId=a),o}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&i.kind==="delete"&&rt.string(i.uri)&&(i.options===void 0||(i.options.recursive===void 0||rt.boolean(i.options.recursive))&&(i.options.ignoreIfNotExists===void 0||rt.boolean(i.options.ignoreIfNotExists)))&&(i.annotationId===void 0||ns.is(i.annotationId))}s(r,"is"),E(r,"is"),e.is=r})(Z1||(Z1={})),(function(e){function t(r){let n=r;return n&&(n.changes!==void 0||n.documentChanges!==void 0)&&(n.documentChanges===void 0||n.documentChanges.every(i=>rt.string(i.kind)?X1.is(i)||K1.is(i)||Z1.is(i):vC.is(i)))}s(t,"is"),E(t,"is"),e.is=t})($A||($A={})),sC=class{static{s(this,"TextEditChangeImpl")}static{E(this,"TextEditChangeImpl")}constructor(e,t){this.edits=e,this.changeAnnotations=t}insert(e,t,r){let n,i;if(r===void 0?n=Lu.insert(e,t):ns.is(r)?(i=r,n=zh.insert(e,t,r)):(this.assertChangeAnnotations(this.changeAnnotations),i=this.changeAnnotations.manage(r),n=zh.insert(e,t,i)),this.edits.push(n),i!==void 0)return i}replace(e,t,r){let n,i;if(r===void 0?n=Lu.replace(e,t):ns.is(r)?(i=r,n=zh.replace(e,t,r)):(this.assertChangeAnnotations(this.changeAnnotations),i=this.changeAnnotations.manage(r),n=zh.replace(e,t,i)),this.edits.push(n),i!==void 0)return i}delete(e,t){let r,n;if(t===void 0?r=Lu.del(e):ns.is(t)?(n=t,r=zh.del(e,t)):(this.assertChangeAnnotations(this.changeAnnotations),n=this.changeAnnotations.manage(t),r=zh.del(e,n)),this.edits.push(r),n!==void 0)return n}add(e){this.edits.push(e)}all(){return this.edits}clear(){this.edits.splice(0,this.edits.length)}assertChangeAnnotations(e){if(e===void 0)throw new Error("Text edit change is not configured to manage change annotations.")}},sG=class{static{s(this,"ChangeAnnotations")}static{E(this,"ChangeAnnotations")}constructor(e){this._annotations=e===void 0?Object.create(null):e,this._counter=0,this._size=0}all(){return this._annotations}get size(){return this._size}manage(e,t){let r;if(ns.is(e)?r=e:(r=this.nextId(),t=e),this._annotations[r]!==void 0)throw new Error(`Id ${r} is already in use.`);if(t===void 0)throw new Error(`No annotation provided for id ${r}`);return this._annotations[r]=t,this._size++,r}nextId(){return this._counter++,this._counter.toString()}},bwe=class{static{s(this,"WorkspaceChange")}static{E(this,"WorkspaceChange")}constructor(e){this._textEditChanges=Object.create(null),e!==void 0?(this._workspaceEdit=e,e.documentChanges?(this._changeAnnotations=new sG(e.changeAnnotations),e.changeAnnotations=this._changeAnnotations.all(),e.documentChanges.forEach(t=>{if(vC.is(t)){let r=new sC(t.edits,this._changeAnnotations);this._textEditChanges[t.textDocument.uri]=r}})):e.changes&&Object.keys(e.changes).forEach(t=>{let r=new sC(e.changes[t]);this._textEditChanges[t]=r})):this._workspaceEdit={}}get edit(){return this.initDocumentChanges(),this._changeAnnotations!==void 0&&(this._changeAnnotations.size===0?this._workspaceEdit.changeAnnotations=void 0:this._workspaceEdit.changeAnnotations=this._changeAnnotations.all()),this._workspaceEdit}getTextEditChange(e){if(xC.is(e)){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");let t={uri:e.uri,version:e.version},r=this._textEditChanges[t.uri];if(!r){let n=[],i={textDocument:t,edits:n};this._workspaceEdit.documentChanges.push(i),r=new sC(n,this._changeAnnotations),this._textEditChanges[t.uri]=r}return r}else{if(this.initChanges(),this._workspaceEdit.changes===void 0)throw new Error("Workspace edit is not configured for normal text edit changes.");let t=this._textEditChanges[e];if(!t){let r=[];this._workspaceEdit.changes[e]=r,t=new sC(r),this._textEditChanges[e]=t}return t}}initDocumentChanges(){this._workspaceEdit.documentChanges===void 0&&this._workspaceEdit.changes===void 0&&(this._changeAnnotations=new sG,this._workspaceEdit.documentChanges=[],this._workspaceEdit.changeAnnotations=this._changeAnnotations.all())}initChanges(){this._workspaceEdit.documentChanges===void 0&&this._workspaceEdit.changes===void 0&&(this._workspaceEdit.changes=Object.create(null))}createFile(e,t,r){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");let n;dg.is(t)||ns.is(t)?n=t:r=t;let i,a;if(n===void 0?i=X1.create(e,r):(a=ns.is(n)?n:this._changeAnnotations.manage(n),i=X1.create(e,r,a)),this._workspaceEdit.documentChanges.push(i),a!==void 0)return a}renameFile(e,t,r,n){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");let i;dg.is(r)||ns.is(r)?i=r:n=r;let a,o;if(i===void 0?a=K1.create(e,t,n):(o=ns.is(i)?i:this._changeAnnotations.manage(i),a=K1.create(e,t,n,o)),this._workspaceEdit.documentChanges.push(a),o!==void 0)return o}deleteFile(e,t,r){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");let n;dg.is(t)||ns.is(t)?n=t:r=t;let i,a;if(n===void 0?i=Z1.create(e,r):(a=ns.is(n)?n:this._changeAnnotations.manage(n),i=Z1.create(e,r,a)),this._workspaceEdit.documentChanges.push(i),a!==void 0)return a}},(function(e){function t(n){return{uri:n}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&rt.string(i.uri)}s(r,"is"),E(r,"is"),e.is=r})($G||($G={})),(function(e){function t(n,i){return{uri:n,version:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&rt.string(i.uri)&&rt.integer(i.version)}s(r,"is"),E(r,"is"),e.is=r})(FG||(FG={})),(function(e){function t(n,i){return{uri:n,version:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&rt.string(i.uri)&&(i.version===null||rt.integer(i.version))}s(r,"is"),E(r,"is"),e.is=r})(xC||(xC={})),(function(e){function t(n,i,a,o){return{uri:n,languageId:i,version:a,text:o}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&rt.string(i.uri)&&rt.string(i.languageId)&&rt.integer(i.version)&&rt.string(i.text)}s(r,"is"),E(r,"is"),e.is=r})(GG||(GG={})),(function(e){e.PlainText="plaintext",e.Markdown="markdown";function t(r){let n=r;return n===e.PlainText||n===e.Markdown}s(t,"is"),E(t,"is"),e.is=t})(FA||(FA={})),(function(e){function t(r){let n=r;return rt.objectLiteral(r)&&FA.is(n.kind)&&rt.string(n.value)}s(t,"is"),E(t,"is"),e.is=t})(Q1||(Q1={})),(function(e){e.Text=1,e.Method=2,e.Function=3,e.Constructor=4,e.Field=5,e.Variable=6,e.Class=7,e.Interface=8,e.Module=9,e.Property=10,e.Unit=11,e.Value=12,e.Enum=13,e.Keyword=14,e.Snippet=15,e.Color=16,e.File=17,e.Reference=18,e.Folder=19,e.EnumMember=20,e.Constant=21,e.Struct=22,e.Event=23,e.Operator=24,e.TypeParameter=25})(zG||(zG={})),(function(e){e.PlainText=1,e.Snippet=2})(VG||(VG={})),(function(e){e.Deprecated=1})(WG||(WG={})),(function(e){function t(n,i,a){return{newText:n,insert:i,replace:a}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&rt.string(i.newText)&&hn.is(i.insert)&&hn.is(i.replace)}s(r,"is"),E(r,"is"),e.is=r})(qG||(qG={})),(function(e){e.asIs=1,e.adjustIndentation=2})(HG||(HG={})),(function(e){function t(r){let n=r;return n&&(rt.string(n.detail)||n.detail===void 0)&&(rt.string(n.description)||n.description===void 0)}s(t,"is"),E(t,"is"),e.is=t})(UG||(UG={})),(function(e){function t(r){return{label:r}}s(t,"create"),E(t,"create"),e.create=t})(YG||(YG={})),(function(e){function t(r,n){return{items:r||[],isIncomplete:!!n}}s(t,"create"),E(t,"create"),e.create=t})(jG||(jG={})),(function(e){function t(n){return n.replace(/[\\`*_{}[\]()#+\-.!]/g,"\\$&")}s(t,"fromPlainText"),E(t,"fromPlainText"),e.fromPlainText=t;function r(n){let i=n;return rt.string(i)||rt.objectLiteral(i)&&rt.string(i.language)&&rt.string(i.value)}s(r,"is"),E(r,"is"),e.is=r})(bC||(bC={})),(function(e){function t(r){let n=r;return!!n&&rt.objectLiteral(n)&&(Q1.is(n.contents)||bC.is(n.contents)||rt.typedArray(n.contents,bC.is))&&(r.range===void 0||hn.is(r.range))}s(t,"is"),E(t,"is"),e.is=t})(XG||(XG={})),(function(e){function t(r,n){return n?{label:r,documentation:n}:{label:r}}s(t,"create"),E(t,"create"),e.create=t})(KG||(KG={})),(function(e){function t(r,n,...i){let a={label:r};return rt.defined(n)&&(a.documentation=n),rt.defined(i)?a.parameters=i:a.parameters=[],a}s(t,"create"),E(t,"create"),e.create=t})(ZG||(ZG={})),(function(e){e.Text=1,e.Read=2,e.Write=3})(QG||(QG={})),(function(e){function t(r,n){let i={range:r};return rt.number(n)&&(i.kind=n),i}s(t,"create"),E(t,"create"),e.create=t})(JG||(JG={})),(function(e){e.File=1,e.Module=2,e.Namespace=3,e.Package=4,e.Class=5,e.Method=6,e.Property=7,e.Field=8,e.Constructor=9,e.Enum=10,e.Interface=11,e.Function=12,e.Variable=13,e.Constant=14,e.String=15,e.Number=16,e.Boolean=17,e.Array=18,e.Object=19,e.Key=20,e.Null=21,e.EnumMember=22,e.Struct=23,e.Event=24,e.Operator=25,e.TypeParameter=26})(ez||(ez={})),(function(e){e.Deprecated=1})(tz||(tz={})),(function(e){function t(r,n,i,a,o){let l={name:r,kind:n,location:{uri:a,range:i}};return o&&(l.containerName=o),l}s(t,"create"),E(t,"create"),e.create=t})(rz||(rz={})),(function(e){function t(r,n,i,a){return a!==void 0?{name:r,kind:n,location:{uri:i,range:a}}:{name:r,kind:n,location:{uri:i}}}s(t,"create"),E(t,"create"),e.create=t})(nz||(nz={})),(function(e){function t(n,i,a,o,l,u){let h={name:n,detail:i,kind:a,range:o,selectionRange:l};return u!==void 0&&(h.children=u),h}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&rt.string(i.name)&&rt.number(i.kind)&&hn.is(i.range)&&hn.is(i.selectionRange)&&(i.detail===void 0||rt.string(i.detail))&&(i.deprecated===void 0||rt.boolean(i.deprecated))&&(i.children===void 0||Array.isArray(i.children))&&(i.tags===void 0||Array.isArray(i.tags))}s(r,"is"),E(r,"is"),e.is=r})(iz||(iz={})),(function(e){e.Empty="",e.QuickFix="quickfix",e.Refactor="refactor",e.RefactorExtract="refactor.extract",e.RefactorInline="refactor.inline",e.RefactorRewrite="refactor.rewrite",e.Source="source",e.SourceOrganizeImports="source.organizeImports",e.SourceFixAll="source.fixAll"})(az||(az={})),(function(e){e.Invoked=1,e.Automatic=2})(TC||(TC={})),(function(e){function t(n,i,a){let o={diagnostics:n};return i!=null&&(o.only=i),a!=null&&(o.triggerKind=a),o}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&rt.typedArray(i.diagnostics,yC.is)&&(i.only===void 0||rt.typedArray(i.only,rt.string))&&(i.triggerKind===void 0||i.triggerKind===TC.Invoked||i.triggerKind===TC.Automatic)}s(r,"is"),E(r,"is"),e.is=r})(sz||(sz={})),(function(e){function t(n,i,a){let o={title:n},l=!0;return typeof i=="string"?(l=!1,o.kind=i):hg.is(i)?o.command=i:o.edit=i,l&&a!==void 0&&(o.kind=a),o}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&rt.string(i.title)&&(i.diagnostics===void 0||rt.typedArray(i.diagnostics,yC.is))&&(i.kind===void 0||rt.string(i.kind))&&(i.edit!==void 0||i.command!==void 0)&&(i.command===void 0||hg.is(i.command))&&(i.isPreferred===void 0||rt.boolean(i.isPreferred))&&(i.edit===void 0||$A.is(i.edit))}s(r,"is"),E(r,"is"),e.is=r})(oz||(oz={})),(function(e){function t(n,i){let a={range:n};return rt.defined(i)&&(a.data=i),a}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&hn.is(i.range)&&(rt.undefined(i.command)||hg.is(i.command))}s(r,"is"),E(r,"is"),e.is=r})(lz||(lz={})),(function(e){function t(n,i){return{tabSize:n,insertSpaces:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&rt.uinteger(i.tabSize)&&rt.boolean(i.insertSpaces)}s(r,"is"),E(r,"is"),e.is=r})(cz||(cz={})),(function(e){function t(n,i,a){return{range:n,target:i,data:a}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&hn.is(i.range)&&(rt.undefined(i.target)||rt.string(i.target))}s(r,"is"),E(r,"is"),e.is=r})(uz||(uz={})),(function(e){function t(n,i){return{range:n,parent:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&hn.is(i.range)&&(i.parent===void 0||e.is(i.parent))}s(r,"is"),E(r,"is"),e.is=r})(hz||(hz={})),(function(e){e.namespace="namespace",e.type="type",e.class="class",e.enum="enum",e.interface="interface",e.struct="struct",e.typeParameter="typeParameter",e.parameter="parameter",e.variable="variable",e.property="property",e.enumMember="enumMember",e.event="event",e.function="function",e.method="method",e.macro="macro",e.keyword="keyword",e.modifier="modifier",e.comment="comment",e.string="string",e.number="number",e.regexp="regexp",e.operator="operator",e.decorator="decorator"})(dz||(dz={})),(function(e){e.declaration="declaration",e.definition="definition",e.readonly="readonly",e.static="static",e.deprecated="deprecated",e.abstract="abstract",e.async="async",e.modification="modification",e.documentation="documentation",e.defaultLibrary="defaultLibrary"})(fz||(fz={})),(function(e){function t(r){let n=r;return rt.objectLiteral(n)&&(n.resultId===void 0||typeof n.resultId=="string")&&Array.isArray(n.data)&&(n.data.length===0||typeof n.data[0]=="number")}s(t,"is"),E(t,"is"),e.is=t})(pz||(pz={})),(function(e){function t(n,i){return{range:n,text:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i!=null&&hn.is(i.range)&&rt.string(i.text)}s(r,"is"),E(r,"is"),e.is=r})(mz||(mz={})),(function(e){function t(n,i,a){return{range:n,variableName:i,caseSensitiveLookup:a}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i!=null&&hn.is(i.range)&&rt.boolean(i.caseSensitiveLookup)&&(rt.string(i.variableName)||i.variableName===void 0)}s(r,"is"),E(r,"is"),e.is=r})(gz||(gz={})),(function(e){function t(n,i){return{range:n,expression:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i!=null&&hn.is(i.range)&&(rt.string(i.expression)||i.expression===void 0)}s(r,"is"),E(r,"is"),e.is=r})(yz||(yz={})),(function(e){function t(n,i){return{frameId:n,stoppedLocation:i}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.defined(i)&&hn.is(n.stoppedLocation)}s(r,"is"),E(r,"is"),e.is=r})(vz||(vz={})),(function(e){e.Type=1,e.Parameter=2;function t(r){return r===1||r===2}s(t,"is"),E(t,"is"),e.is=t})(GA||(GA={})),(function(e){function t(n){return{value:n}}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&(i.tooltip===void 0||rt.string(i.tooltip)||Q1.is(i.tooltip))&&(i.location===void 0||gC.is(i.location))&&(i.command===void 0||hg.is(i.command))}s(r,"is"),E(r,"is"),e.is=r})(zA||(zA={})),(function(e){function t(n,i,a){let o={position:n,label:i};return a!==void 0&&(o.kind=a),o}s(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return rt.objectLiteral(i)&&Dn.is(i.position)&&(rt.string(i.label)||rt.typedArray(i.label,zA.is))&&(i.kind===void 0||GA.is(i.kind))&&i.textEdits===void 0||rt.typedArray(i.textEdits,Lu.is)&&(i.tooltip===void 0||rt.string(i.tooltip)||Q1.is(i.tooltip))&&(i.paddingLeft===void 0||rt.boolean(i.paddingLeft))&&(i.paddingRight===void 0||rt.boolean(i.paddingRight))}s(r,"is"),E(r,"is"),e.is=r})(xz||(xz={})),(function(e){function t(r){return{kind:"snippet",value:r}}s(t,"createSnippet"),E(t,"createSnippet"),e.createSnippet=t})(bz||(bz={})),(function(e){function t(r,n,i,a){return{insertText:r,filterText:n,range:i,command:a}}s(t,"create"),E(t,"create"),e.create=t})(Tz||(Tz={})),(function(e){function t(r){return{items:r}}s(t,"create"),E(t,"create"),e.create=t})(Cz||(Cz={})),(function(e){e.Invoked=0,e.Automatic=1})(Sz||(Sz={})),(function(e){function t(r,n){return{range:r,text:n}}s(t,"create"),E(t,"create"),e.create=t})(wz||(wz={})),(function(e){function t(r,n){return{triggerKind:r,selectedCompletionInfo:n}}s(t,"create"),E(t,"create"),e.create=t})(kz||(kz={})),(function(e){function t(r){let n=r;return rt.objectLiteral(n)&&PA.is(n.uri)&&rt.string(n.name)}s(t,"is"),E(t,"is"),e.is=t})(Ez||(Ez={})),Twe=[` +`,`\r +`,"\r"],(function(e){function t(a,o,l,u){return new _Ce(a,o,l,u)}s(t,"create"),E(t,"create"),e.create=t;function r(a){let o=a;return!!(rt.defined(o)&&rt.string(o.uri)&&(rt.undefined(o.languageId)||rt.string(o.languageId))&&rt.uinteger(o.lineCount)&&rt.func(o.getText)&&rt.func(o.positionAt)&&rt.func(o.offsetAt))}s(r,"is"),E(r,"is"),e.is=r;function n(a,o){let l=a.getText(),u=i(o,(d,f)=>{let p=d.range.start.line-f.range.start.line;return p===0?d.range.start.character-f.range.start.character:p}),h=l.length;for(let d=u.length-1;d>=0;d--){let f=u[d],p=a.offsetAt(f.range.start),m=a.offsetAt(f.range.end);if(m<=h)l=l.substring(0,p)+f.newText+l.substring(m,l.length);else throw new Error("Overlapping edit");h=p}return l}s(n,"applyEdits"),E(n,"applyEdits"),e.applyEdits=n;function i(a,o){if(a.length<=1)return a;let l=a.length/2|0,u=a.slice(0,l),h=a.slice(l);i(u,o),i(h,o);let d=0,f=0,p=0;for(;d0&&e.push(t.length),this._lineOffsets=e}return this._lineOffsets}positionAt(e){e=Math.max(Math.min(e,this._content.length),0);let t=this.getLineOffsets(),r=0,n=t.length;if(n===0)return Dn.create(0,e);for(;re?n=a:r=a+1}let i=r-1;return Dn.create(i,e-t[i])}offsetAt(e){let t=this.getLineOffsets();if(e.line>=t.length)return this._content.length;if(e.line<0)return 0;let r=t[e.line],n=e.line+1"u"}s(n,"undefined2"),E(n,"undefined"),e.undefined=n;function i(m){return m===!0||m===!1}s(i,"boolean"),E(i,"boolean"),e.boolean=i;function a(m){return t.call(m)==="[object String]"}s(a,"string"),E(a,"string"),e.string=a;function o(m){return t.call(m)==="[object Number]"}s(o,"number"),E(o,"number"),e.number=o;function l(m,g,y){return t.call(m)==="[object Number]"&&g<=m&&m<=y}s(l,"numberRange"),E(l,"numberRange"),e.numberRange=l;function u(m){return t.call(m)==="[object Number]"&&-2147483648<=m&&m<=2147483647}s(u,"integer2"),E(u,"integer"),e.integer=u;function h(m){return t.call(m)==="[object Number]"&&0<=m&&m<=2147483647}s(h,"uinteger2"),E(h,"uinteger"),e.uinteger=h;function d(m){return t.call(m)==="[object Function]"}s(d,"func"),E(d,"func"),e.func=d;function f(m){return m!==null&&typeof m=="object"}s(f,"objectLiteral"),E(f,"objectLiteral"),e.objectLiteral=f;function p(m,g){return Array.isArray(m)&&m.every(g)}s(p,"typedArray"),E(p,"typedArray"),e.typedArray=p})(rt||(rt={}))}}),Qg=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/ral.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t;function r(){if(t===void 0)throw new Error("No runtime abstraction layer installed");return t}s(r,"RAL"),E(r,"RAL"),(function(n){function i(a){if(a===void 0)throw new Error("No runtime abstraction layer provided");t=a}s(i,"install"),E(i,"install"),n.install=i})(r||(r={})),e.default=r}}),uS=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/is.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.stringArray=e.array=e.func=e.error=e.number=e.string=e.boolean=void 0;function t(u){return u===!0||u===!1}s(t,"boolean"),E(t,"boolean"),e.boolean=t;function r(u){return typeof u=="string"||u instanceof String}s(r,"string"),E(r,"string"),e.string=r;function n(u){return typeof u=="number"||u instanceof Number}s(n,"number"),E(n,"number"),e.number=n;function i(u){return u instanceof Error}s(i,"error"),E(i,"error"),e.error=i;function a(u){return typeof u=="function"}s(a,"func"),E(a,"func"),e.func=a;function o(u){return Array.isArray(u)}s(o,"array"),E(o,"array"),e.array=o;function l(u){return o(u)&&u.every(h=>r(h))}s(l,"stringArray"),E(l,"stringArray"),e.stringArray=l}}),_v=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/events.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Emitter=e.Event=void 0;var t=Qg(),r;(function(a){let o={dispose(){}};a.None=function(){return o}})(r||(e.Event=r={}));var n=class{static{s(this,"CallbackList")}static{E(this,"CallbackList")}add(a,o=null,l){this._callbacks||(this._callbacks=[],this._contexts=[]),this._callbacks.push(a),this._contexts.push(o),Array.isArray(l)&&l.push({dispose:E(()=>this.remove(a,o),"dispose")})}remove(a,o=null){if(!this._callbacks)return;let l=!1;for(let u=0,h=this._callbacks.length;u{this._callbacks||(this._callbacks=new n),this._options&&this._options.onFirstListenerAdd&&this._callbacks.isEmpty()&&this._options.onFirstListenerAdd(this),this._callbacks.add(o,l);let h={dispose:E(()=>{this._callbacks&&(this._callbacks.remove(o,l),h.dispose=Cwe._noop,this._options&&this._options.onLastListenerRemove&&this._callbacks.isEmpty()&&this._options.onLastListenerRemove(this))},"dispose")};return Array.isArray(u)&&u.push(h),h}),this._event}fire(o){this._callbacks&&this._callbacks.invoke.call(this._callbacks,o)}dispose(){this._callbacks&&(this._callbacks.dispose(),this._callbacks=void 0)}};e.Emitter=i,i._noop=function(){}}}),t6=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/cancellation.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CancellationTokenSource=e.CancellationToken=void 0;var t=Qg(),r=uS(),n=_v(),i;(function(u){u.None=Object.freeze({isCancellationRequested:!1,onCancellationRequested:n.Event.None}),u.Cancelled=Object.freeze({isCancellationRequested:!0,onCancellationRequested:n.Event.None});function h(d){let f=d;return f&&(f===u.None||f===u.Cancelled||r.boolean(f.isCancellationRequested)&&!!f.onCancellationRequested)}s(h,"is"),E(h,"is"),u.is=h})(i||(e.CancellationToken=i={}));var a=Object.freeze(function(u,h){let d=(0,t.default)().timer.setTimeout(u.bind(h),0);return{dispose(){d.dispose()}}}),o=class{static{s(this,"MutableToken")}static{E(this,"MutableToken")}constructor(){this._isCancelled=!1}cancel(){this._isCancelled||(this._isCancelled=!0,this._emitter&&(this._emitter.fire(void 0),this.dispose()))}get isCancellationRequested(){return this._isCancelled}get onCancellationRequested(){return this._isCancelled?a:(this._emitter||(this._emitter=new n.Emitter),this._emitter.event)}dispose(){this._emitter&&(this._emitter.dispose(),this._emitter=void 0)}},l=class{static{s(this,"CancellationTokenSource3")}static{E(this,"CancellationTokenSource")}get token(){return this._token||(this._token=new o),this._token}cancel(){this._token?this._token.cancel():this._token=i.Cancelled}dispose(){this._token?this._token instanceof o&&this._token.dispose():this._token=i.None}};e.CancellationTokenSource=l}}),Swe=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/messages.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Message=e.NotificationType9=e.NotificationType8=e.NotificationType7=e.NotificationType6=e.NotificationType5=e.NotificationType4=e.NotificationType3=e.NotificationType2=e.NotificationType1=e.NotificationType0=e.NotificationType=e.RequestType9=e.RequestType8=e.RequestType7=e.RequestType6=e.RequestType5=e.RequestType4=e.RequestType3=e.RequestType2=e.RequestType1=e.RequestType=e.RequestType0=e.AbstractMessageSignature=e.ParameterStructures=e.ResponseError=e.ErrorCodes=void 0;var t=uS(),r;(function(A){A.ParseError=-32700,A.InvalidRequest=-32600,A.MethodNotFound=-32601,A.InvalidParams=-32602,A.InternalError=-32603,A.jsonrpcReservedErrorRangeStart=-32099,A.serverErrorStart=-32099,A.MessageWriteError=-32099,A.MessageReadError=-32098,A.PendingResponseRejected=-32097,A.ConnectionInactive=-32096,A.ServerNotInitialized=-32002,A.UnknownErrorCode=-32001,A.jsonrpcReservedErrorRangeEnd=-32e3,A.serverErrorEnd=-32e3})(r||(e.ErrorCodes=r={}));var n=class wwe extends Error{static{s(this,"_ResponseError")}static{E(this,"ResponseError")}constructor(I,D,L){super(D),this.code=t.number(I)?I:r.UnknownErrorCode,this.data=L,Object.setPrototypeOf(this,wwe.prototype)}toJson(){let I={code:this.code,message:this.message};return this.data!==void 0&&(I.data=this.data),I}};e.ResponseError=n;var i=class VA{static{s(this,"_ParameterStructures")}static{E(this,"ParameterStructures")}constructor(I){this.kind=I}static is(I){return I===VA.auto||I===VA.byName||I===VA.byPosition}toString(){return this.kind}};e.ParameterStructures=i,i.auto=new i("auto"),i.byPosition=new i("byPosition"),i.byName=new i("byName");var a=class{static{s(this,"AbstractMessageSignature")}static{E(this,"AbstractMessageSignature")}constructor(A,I){this.method=A,this.numberOfParams=I}get parameterStructures(){return i.auto}};e.AbstractMessageSignature=a;var o=class extends a{static{s(this,"RequestType0")}static{E(this,"RequestType0")}constructor(A){super(A,0)}};e.RequestType0=o;var l=class extends a{static{s(this,"RequestType")}static{E(this,"RequestType")}constructor(A,I=i.auto){super(A,1),this._parameterStructures=I}get parameterStructures(){return this._parameterStructures}};e.RequestType=l;var u=class extends a{static{s(this,"RequestType1")}static{E(this,"RequestType1")}constructor(A,I=i.auto){super(A,1),this._parameterStructures=I}get parameterStructures(){return this._parameterStructures}};e.RequestType1=u;var h=class extends a{static{s(this,"RequestType2")}static{E(this,"RequestType2")}constructor(A){super(A,2)}};e.RequestType2=h;var d=class extends a{static{s(this,"RequestType3")}static{E(this,"RequestType3")}constructor(A){super(A,3)}};e.RequestType3=d;var f=class extends a{static{s(this,"RequestType4")}static{E(this,"RequestType4")}constructor(A){super(A,4)}};e.RequestType4=f;var p=class extends a{static{s(this,"RequestType5")}static{E(this,"RequestType5")}constructor(A){super(A,5)}};e.RequestType5=p;var m=class extends a{static{s(this,"RequestType6")}static{E(this,"RequestType6")}constructor(A){super(A,6)}};e.RequestType6=m;var g=class extends a{static{s(this,"RequestType7")}static{E(this,"RequestType7")}constructor(A){super(A,7)}};e.RequestType7=g;var y=class extends a{static{s(this,"RequestType8")}static{E(this,"RequestType8")}constructor(A){super(A,8)}};e.RequestType8=y;var v=class extends a{static{s(this,"RequestType9")}static{E(this,"RequestType9")}constructor(A){super(A,9)}};e.RequestType9=v;var x=class extends a{static{s(this,"NotificationType")}static{E(this,"NotificationType")}constructor(A,I=i.auto){super(A,1),this._parameterStructures=I}get parameterStructures(){return this._parameterStructures}};e.NotificationType=x;var b=class extends a{static{s(this,"NotificationType0")}static{E(this,"NotificationType0")}constructor(A){super(A,0)}};e.NotificationType0=b;var T=class extends a{static{s(this,"NotificationType1")}static{E(this,"NotificationType1")}constructor(A,I=i.auto){super(A,1),this._parameterStructures=I}get parameterStructures(){return this._parameterStructures}};e.NotificationType1=T;var w=class extends a{static{s(this,"NotificationType2")}static{E(this,"NotificationType2")}constructor(A){super(A,2)}};e.NotificationType2=w;var C=class extends a{static{s(this,"NotificationType3")}static{E(this,"NotificationType3")}constructor(A){super(A,3)}};e.NotificationType3=C;var S=class extends a{static{s(this,"NotificationType4")}static{E(this,"NotificationType4")}constructor(A){super(A,4)}};e.NotificationType4=S;var k=class extends a{static{s(this,"NotificationType5")}static{E(this,"NotificationType5")}constructor(A){super(A,5)}};e.NotificationType5=k;var R=class extends a{static{s(this,"NotificationType6")}static{E(this,"NotificationType6")}constructor(A){super(A,6)}};e.NotificationType6=R;var M=class extends a{static{s(this,"NotificationType7")}static{E(this,"NotificationType7")}constructor(A){super(A,7)}};e.NotificationType7=M;var P=class extends a{static{s(this,"NotificationType8")}static{E(this,"NotificationType8")}constructor(A){super(A,8)}};e.NotificationType8=P;var N=class extends a{static{s(this,"NotificationType9")}static{E(this,"NotificationType9")}constructor(A){super(A,9)}};e.NotificationType9=N;var _;(function(A){function I(B){let O=B;return O&&t.string(O.method)&&(t.string(O.id)||t.number(O.id))}s(I,"isRequest"),E(I,"isRequest"),A.isRequest=I;function D(B){let O=B;return O&&t.string(O.method)&&B.id===void 0}s(D,"isNotification"),E(D,"isNotification"),A.isNotification=D;function L(B){let O=B;return O&&(O.result!==void 0||!!O.error)&&(t.string(O.id)||t.number(O.id)||O.id===null)}s(L,"isResponse"),E(L,"isResponse"),A.isResponse=L})(_||(e.Message=_={}))}}),kwe=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/linkedMap.js"(e){"use strict";var t;Object.defineProperty(e,"__esModule",{value:!0}),e.LRUCache=e.LinkedMap=e.Touch=void 0;var r;(function(a){a.None=0,a.First=1,a.AsOld=a.First,a.Last=2,a.AsNew=a.Last})(r||(e.Touch=r={}));var n=class{static{s(this,"LinkedMap")}static{E(this,"LinkedMap")}constructor(){this[t]="LinkedMap",this._map=new Map,this._head=void 0,this._tail=void 0,this._size=0,this._state=0}clear(){this._map.clear(),this._head=void 0,this._tail=void 0,this._size=0,this._state++}isEmpty(){return!this._head&&!this._tail}get size(){return this._size}get first(){return this._head?.value}get last(){return this._tail?.value}has(a){return this._map.has(a)}get(a,o=r.None){let l=this._map.get(a);if(l)return o!==r.None&&this.touch(l,o),l.value}set(a,o,l=r.None){let u=this._map.get(a);if(u)u.value=o,l!==r.None&&this.touch(u,l);else{switch(u={key:a,value:o,next:void 0,previous:void 0},l){case r.None:this.addItemLast(u);break;case r.First:this.addItemFirst(u);break;case r.Last:this.addItemLast(u);break;default:this.addItemLast(u);break}this._map.set(a,u),this._size++}return this}delete(a){return!!this.remove(a)}remove(a){let o=this._map.get(a);if(o)return this._map.delete(a),this.removeItem(o),this._size--,o.value}shift(){if(!this._head&&!this._tail)return;if(!this._head||!this._tail)throw new Error("Invalid list");let a=this._head;return this._map.delete(a.key),this.removeItem(a),this._size--,a.value}forEach(a,o){let l=this._state,u=this._head;for(;u;){if(o?a.bind(o)(u.value,u.key,this):a(u.value,u.key,this),this._state!==l)throw new Error("LinkedMap got modified during iteration.");u=u.next}}keys(){let a=this._state,o=this._head,l={[Symbol.iterator]:()=>l,next:E(()=>{if(this._state!==a)throw new Error("LinkedMap got modified during iteration.");if(o){let u={value:o.key,done:!1};return o=o.next,u}else return{value:void 0,done:!0}},"next")};return l}values(){let a=this._state,o=this._head,l={[Symbol.iterator]:()=>l,next:E(()=>{if(this._state!==a)throw new Error("LinkedMap got modified during iteration.");if(o){let u={value:o.value,done:!1};return o=o.next,u}else return{value:void 0,done:!0}},"next")};return l}entries(){let a=this._state,o=this._head,l={[Symbol.iterator]:()=>l,next:E(()=>{if(this._state!==a)throw new Error("LinkedMap got modified during iteration.");if(o){let u={value:[o.key,o.value],done:!1};return o=o.next,u}else return{value:void 0,done:!0}},"next")};return l}[(t=Symbol.toStringTag,Symbol.iterator)](){return this.entries()}trimOld(a){if(a>=this.size)return;if(a===0){this.clear();return}let o=this._head,l=this.size;for(;o&&l>a;)this._map.delete(o.key),o=o.next,l--;this._head=o,this._size=l,o&&(o.previous=void 0),this._state++}addItemFirst(a){if(!this._head&&!this._tail)this._tail=a;else if(this._head)a.next=this._head,this._head.previous=a;else throw new Error("Invalid list");this._head=a,this._state++}addItemLast(a){if(!this._head&&!this._tail)this._head=a;else if(this._tail)a.previous=this._tail,this._tail.next=a;else throw new Error("Invalid list");this._tail=a,this._state++}removeItem(a){if(a===this._head&&a===this._tail)this._head=void 0,this._tail=void 0;else if(a===this._head){if(!a.next)throw new Error("Invalid list");a.next.previous=void 0,this._head=a.next}else if(a===this._tail){if(!a.previous)throw new Error("Invalid list");a.previous.next=void 0,this._tail=a.previous}else{let o=a.next,l=a.previous;if(!o||!l)throw new Error("Invalid list");o.previous=l,l.next=o}a.next=void 0,a.previous=void 0,this._state++}touch(a,o){if(!this._head||!this._tail)throw new Error("Invalid list");if(!(o!==r.First&&o!==r.Last)){if(o===r.First){if(a===this._head)return;let l=a.next,u=a.previous;a===this._tail?(u.next=void 0,this._tail=u):(l.previous=u,u.next=l),a.previous=void 0,a.next=this._head,this._head.previous=a,this._head=a,this._state++}else if(o===r.Last){if(a===this._tail)return;let l=a.next,u=a.previous;a===this._head?(l.previous=void 0,this._head=l):(l.previous=u,u.next=l),a.next=void 0,a.previous=this._tail,this._tail.next=a,this._tail=a,this._state++}}}toJSON(){let a=[];return this.forEach((o,l)=>{a.push([l,o])}),a}fromJSON(a){this.clear();for(let[o,l]of a)this.set(o,l)}};e.LinkedMap=n;var i=class extends n{static{s(this,"LRUCache")}static{E(this,"LRUCache")}constructor(a,o=1){super(),this._limit=a,this._ratio=Math.min(Math.max(0,o),1)}get limit(){return this._limit}set limit(a){this._limit=a,this.checkTrim()}get ratio(){return this._ratio}set ratio(a){this._ratio=Math.min(Math.max(0,a),1),this.checkTrim()}get(a,o=r.AsNew){return super.get(a,o)}peek(a){return super.get(a,r.None)}set(a,o){return super.set(a,o,r.Last),this.checkTrim(),this}checkTrim(){this.size>this._limit&&this.trimOld(Math.round(this._limit*this._ratio))}};e.LRUCache=i}}),_gt=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/disposable.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Disposable=void 0;var t;(function(r){function n(i){return{dispose:i}}s(n,"create"),E(n,"create"),r.create=n})(t||(e.Disposable=t={}))}}),Lgt=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/sharedArrayCancellation.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SharedArrayReceiverStrategy=e.SharedArraySenderStrategy=void 0;var t=t6(),r;(function(l){l.Continue=0,l.Cancelled=1})(r||(r={}));var n=class{static{s(this,"SharedArraySenderStrategy")}static{E(this,"SharedArraySenderStrategy")}constructor(){this.buffers=new Map}enableCancellation(l){if(l.id===null)return;let u=new SharedArrayBuffer(4),h=new Int32Array(u,0,1);h[0]=r.Continue,this.buffers.set(l.id,u),l.$cancellationData=u}async sendCancellation(l,u){let h=this.buffers.get(u);if(h===void 0)return;let d=new Int32Array(h,0,1);Atomics.store(d,0,r.Cancelled)}cleanup(l){this.buffers.delete(l)}dispose(){this.buffers.clear()}};e.SharedArraySenderStrategy=n;var i=class{static{s(this,"SharedArrayBufferCancellationToken")}static{E(this,"SharedArrayBufferCancellationToken")}constructor(l){this.data=new Int32Array(l,0,1)}get isCancellationRequested(){return Atomics.load(this.data,0)===r.Cancelled}get onCancellationRequested(){throw new Error("Cancellation over SharedArrayBuffer doesn't support cancellation events")}},a=class{static{s(this,"SharedArrayBufferCancellationTokenSource")}static{E(this,"SharedArrayBufferCancellationTokenSource")}constructor(l){this.token=new i(l)}cancel(){}dispose(){}},o=class{static{s(this,"SharedArrayReceiverStrategy")}static{E(this,"SharedArrayReceiverStrategy")}constructor(){this.kind="request"}createCancellationTokenSource(l){let u=l.$cancellationData;return u===void 0?new t.CancellationTokenSource:new a(u)}};e.SharedArrayReceiverStrategy=o}}),Ewe=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/semaphore.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Semaphore=void 0;var t=Qg(),r=class{static{s(this,"Semaphore")}static{E(this,"Semaphore")}constructor(n=1){if(n<=0)throw new Error("Capacity must be greater than 0");this._capacity=n,this._active=0,this._waiting=[]}lock(n){return new Promise((i,a)=>{this._waiting.push({thunk:n,resolve:i,reject:a}),this.runNext()})}get active(){return this._active}runNext(){this._waiting.length===0||this._active===this._capacity||(0,t.default)().timer.setImmediate(()=>this.doRunNext())}doRunNext(){if(this._waiting.length===0||this._active===this._capacity)return;let n=this._waiting.shift();if(this._active++,this._active>this._capacity)throw new Error("To many thunks active");try{let i=n.thunk();i instanceof Promise?i.then(a=>{this._active--,n.resolve(a),this.runNext()},a=>{this._active--,n.reject(a),this.runNext()}):(this._active--,n.resolve(i),this.runNext())}catch(i){this._active--,n.reject(i),this.runNext()}}};e.Semaphore=r}}),Dgt=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/messageReader.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ReadableStreamMessageReader=e.AbstractMessageReader=e.MessageReader=void 0;var t=Qg(),r=uS(),n=_v(),i=Ewe(),a;(function(h){function d(f){let p=f;return p&&r.func(p.listen)&&r.func(p.dispose)&&r.func(p.onError)&&r.func(p.onClose)&&r.func(p.onPartialMessage)}s(d,"is"),E(d,"is"),h.is=d})(a||(e.MessageReader=a={}));var o=class{static{s(this,"AbstractMessageReader")}static{E(this,"AbstractMessageReader")}constructor(){this.errorEmitter=new n.Emitter,this.closeEmitter=new n.Emitter,this.partialMessageEmitter=new n.Emitter}dispose(){this.errorEmitter.dispose(),this.closeEmitter.dispose()}get onError(){return this.errorEmitter.event}fireError(h){this.errorEmitter.fire(this.asError(h))}get onClose(){return this.closeEmitter.event}fireClose(){this.closeEmitter.fire(void 0)}get onPartialMessage(){return this.partialMessageEmitter.event}firePartialMessage(h){this.partialMessageEmitter.fire(h)}asError(h){return h instanceof Error?h:new Error(`Reader received error. Reason: ${r.string(h.message)?h.message:"unknown"}`)}};e.AbstractMessageReader=o;var l;(function(h){function d(f){let p,m,g,y=new Map,v,x=new Map;if(f===void 0||typeof f=="string")p=f??"utf-8";else{if(p=f.charset??"utf-8",f.contentDecoder!==void 0&&(g=f.contentDecoder,y.set(g.name,g)),f.contentDecoders!==void 0)for(let b of f.contentDecoders)y.set(b.name,b);if(f.contentTypeDecoder!==void 0&&(v=f.contentTypeDecoder,x.set(v.name,v)),f.contentTypeDecoders!==void 0)for(let b of f.contentTypeDecoders)x.set(b.name,b)}return v===void 0&&(v=(0,t.default)().applicationJson.decoder,x.set(v.name,v)),{charset:p,contentDecoder:g,contentDecoders:y,contentTypeDecoder:v,contentTypeDecoders:x}}s(d,"fromOptions"),E(d,"fromOptions"),h.fromOptions=d})(l||(l={}));var u=class extends o{static{s(this,"ReadableStreamMessageReader")}static{E(this,"ReadableStreamMessageReader")}constructor(h,d){super(),this.readable=h,this.options=l.fromOptions(d),this.buffer=(0,t.default)().messageBuffer.create(this.options.charset),this._partialMessageTimeout=1e4,this.nextMessageLength=-1,this.messageToken=0,this.readSemaphore=new i.Semaphore(1)}set partialMessageTimeout(h){this._partialMessageTimeout=h}get partialMessageTimeout(){return this._partialMessageTimeout}listen(h){this.nextMessageLength=-1,this.messageToken=0,this.partialMessageTimer=void 0,this.callback=h;let d=this.readable.onData(f=>{this.onData(f)});return this.readable.onError(f=>this.fireError(f)),this.readable.onClose(()=>this.fireClose()),d}onData(h){try{for(this.buffer.append(h);;){if(this.nextMessageLength===-1){let f=this.buffer.tryReadHeaders(!0);if(!f)return;let p=f.get("content-length");if(!p){this.fireError(new Error(`Header must provide a Content-Length property. +${JSON.stringify(Object.fromEntries(f))}`));return}let m=parseInt(p);if(isNaN(m)){this.fireError(new Error(`Content-Length value must be a number. Got ${p}`));return}this.nextMessageLength=m}let d=this.buffer.tryReadBody(this.nextMessageLength);if(d===void 0){this.setPartialMessageTimer();return}this.clearPartialMessageTimer(),this.nextMessageLength=-1,this.readSemaphore.lock(async()=>{let f=this.options.contentDecoder!==void 0?await this.options.contentDecoder.decode(d):d,p=await this.options.contentTypeDecoder.decode(f,this.options);this.callback(p)}).catch(f=>{this.fireError(f)})}}catch(d){this.fireError(d)}}clearPartialMessageTimer(){this.partialMessageTimer&&(this.partialMessageTimer.dispose(),this.partialMessageTimer=void 0)}setPartialMessageTimer(){this.clearPartialMessageTimer(),!(this._partialMessageTimeout<=0)&&(this.partialMessageTimer=(0,t.default)().timer.setTimeout((h,d)=>{this.partialMessageTimer=void 0,h===this.messageToken&&(this.firePartialMessage({messageToken:h,waitingTime:d}),this.setPartialMessageTimer())},this._partialMessageTimeout,this.messageToken,this._partialMessageTimeout))}};e.ReadableStreamMessageReader=u}}),Igt=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/messageWriter.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WriteableStreamMessageWriter=e.AbstractMessageWriter=e.MessageWriter=void 0;var t=Qg(),r=uS(),n=Ewe(),i=_v(),a="Content-Length: ",o=`\r +`,l;(function(f){function p(m){let g=m;return g&&r.func(g.dispose)&&r.func(g.onClose)&&r.func(g.onError)&&r.func(g.write)}s(p,"is"),E(p,"is"),f.is=p})(l||(e.MessageWriter=l={}));var u=class{static{s(this,"AbstractMessageWriter")}static{E(this,"AbstractMessageWriter")}constructor(){this.errorEmitter=new i.Emitter,this.closeEmitter=new i.Emitter}dispose(){this.errorEmitter.dispose(),this.closeEmitter.dispose()}get onError(){return this.errorEmitter.event}fireError(f,p,m){this.errorEmitter.fire([this.asError(f),p,m])}get onClose(){return this.closeEmitter.event}fireClose(){this.closeEmitter.fire(void 0)}asError(f){return f instanceof Error?f:new Error(`Writer received error. Reason: ${r.string(f.message)?f.message:"unknown"}`)}};e.AbstractMessageWriter=u;var h;(function(f){function p(m){return m===void 0||typeof m=="string"?{charset:m??"utf-8",contentTypeEncoder:(0,t.default)().applicationJson.encoder}:{charset:m.charset??"utf-8",contentEncoder:m.contentEncoder,contentTypeEncoder:m.contentTypeEncoder??(0,t.default)().applicationJson.encoder}}s(p,"fromOptions"),E(p,"fromOptions"),f.fromOptions=p})(h||(h={}));var d=class extends u{static{s(this,"WriteableStreamMessageWriter")}static{E(this,"WriteableStreamMessageWriter")}constructor(f,p){super(),this.writable=f,this.options=h.fromOptions(p),this.errorCount=0,this.writeSemaphore=new n.Semaphore(1),this.writable.onError(m=>this.fireError(m)),this.writable.onClose(()=>this.fireClose())}async write(f){return this.writeSemaphore.lock(async()=>this.options.contentTypeEncoder.encode(f,this.options).then(m=>this.options.contentEncoder!==void 0?this.options.contentEncoder.encode(m):m).then(m=>{let g=[];return g.push(a,m.byteLength.toString(),o),g.push(o),this.doWrite(f,g,m)},m=>{throw this.fireError(m),m}))}async doWrite(f,p,m){try{return await this.writable.write(p.join(""),"ascii"),this.writable.write(m)}catch(g){return this.handleError(g,f),Promise.reject(g)}}handleError(f,p){this.errorCount++,this.fireError(f,p,this.errorCount)}end(){this.writable.end()}};e.WriteableStreamMessageWriter=d}}),Mgt=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/messageBuffer.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractMessageBuffer=void 0;var t=13,r=10,n=`\r +`,i=class{static{s(this,"AbstractMessageBuffer")}static{E(this,"AbstractMessageBuffer")}constructor(a="utf-8"){this._encoding=a,this._chunks=[],this._totalLength=0}get encoding(){return this._encoding}append(a){let o=typeof a=="string"?this.fromString(a,this._encoding):a;this._chunks.push(o),this._totalLength+=o.byteLength}tryReadHeaders(a=!1){if(this._chunks.length===0)return;let o=0,l=0,u=0,h=0;e:for(;lthis._totalLength)throw new Error("Cannot read so many bytes!");if(this._chunks[0].byteLength===a){let h=this._chunks[0];return this._chunks.shift(),this._totalLength-=a,this.asNative(h)}if(this._chunks[0].byteLength>a){let h=this._chunks[0],d=this.asNative(h,a);return this._chunks[0]=h.slice(a),this._totalLength-=a,d}let o=this.allocNative(a),l=0,u=0;for(;a>0;){let h=this._chunks[u];if(h.byteLength>a){let d=h.slice(0,a);o.set(d,l),l+=a,this._chunks[u]=h.slice(a),this._totalLength-=a,a-=a}else o.set(h,l),l+=h.byteLength,this._chunks.shift(),this._totalLength-=h.byteLength,a-=h.byteLength}return o}};e.AbstractMessageBuffer=i}}),Ngt=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/connection.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.createMessageConnection=e.ConnectionOptions=e.MessageStrategy=e.CancellationStrategy=e.CancellationSenderStrategy=e.CancellationReceiverStrategy=e.RequestCancellationReceiverStrategy=e.IdCancellationReceiverStrategy=e.ConnectionStrategy=e.ConnectionError=e.ConnectionErrors=e.LogTraceNotification=e.SetTraceNotification=e.TraceFormat=e.TraceValues=e.Trace=e.NullLogger=e.ProgressType=e.ProgressToken=void 0;var t=Qg(),r=uS(),n=Swe(),i=kwe(),a=_v(),o=t6(),l;(function(A){A.type=new n.NotificationType("$/cancelRequest")})(l||(l={}));var u;(function(A){function I(D){return typeof D=="string"||typeof D=="number"}s(I,"is"),E(I,"is"),A.is=I})(u||(e.ProgressToken=u={}));var h;(function(A){A.type=new n.NotificationType("$/progress")})(h||(h={}));var d=class{static{s(this,"ProgressType")}static{E(this,"ProgressType")}constructor(){}};e.ProgressType=d;var f;(function(A){function I(D){return r.func(D)}s(I,"is"),E(I,"is"),A.is=I})(f||(f={})),e.NullLogger=Object.freeze({error:E(()=>{},"error"),warn:E(()=>{},"warn"),info:E(()=>{},"info"),log:E(()=>{},"log")});var p;(function(A){A[A.Off=0]="Off",A[A.Messages=1]="Messages",A[A.Compact=2]="Compact",A[A.Verbose=3]="Verbose"})(p||(e.Trace=p={}));var m;(function(A){A.Off="off",A.Messages="messages",A.Compact="compact",A.Verbose="verbose"})(m||(e.TraceValues=m={})),(function(A){function I(L){if(!r.string(L))return A.Off;switch(L=L.toLowerCase(),L){case"off":return A.Off;case"messages":return A.Messages;case"compact":return A.Compact;case"verbose":return A.Verbose;default:return A.Off}}s(I,"fromString"),E(I,"fromString"),A.fromString=I;function D(L){switch(L){case A.Off:return"off";case A.Messages:return"messages";case A.Compact:return"compact";case A.Verbose:return"verbose";default:return"off"}}s(D,"toString4"),E(D,"toString"),A.toString=D})(p||(e.Trace=p={}));var g;(function(A){A.Text="text",A.JSON="json"})(g||(e.TraceFormat=g={})),(function(A){function I(D){return r.string(D)?(D=D.toLowerCase(),D==="json"?A.JSON:A.Text):A.Text}s(I,"fromString"),E(I,"fromString"),A.fromString=I})(g||(e.TraceFormat=g={}));var y;(function(A){A.type=new n.NotificationType("$/setTrace")})(y||(e.SetTraceNotification=y={}));var v;(function(A){A.type=new n.NotificationType("$/logTrace")})(v||(e.LogTraceNotification=v={}));var x;(function(A){A[A.Closed=1]="Closed",A[A.Disposed=2]="Disposed",A[A.AlreadyListening=3]="AlreadyListening"})(x||(e.ConnectionErrors=x={}));var b=class Awe extends Error{static{s(this,"_ConnectionError")}static{E(this,"ConnectionError")}constructor(I,D){super(D),this.code=I,Object.setPrototypeOf(this,Awe.prototype)}};e.ConnectionError=b;var T;(function(A){function I(D){let L=D;return L&&r.func(L.cancelUndispatched)}s(I,"is"),E(I,"is"),A.is=I})(T||(e.ConnectionStrategy=T={}));var w;(function(A){function I(D){let L=D;return L&&(L.kind===void 0||L.kind==="id")&&r.func(L.createCancellationTokenSource)&&(L.dispose===void 0||r.func(L.dispose))}s(I,"is"),E(I,"is"),A.is=I})(w||(e.IdCancellationReceiverStrategy=w={}));var C;(function(A){function I(D){let L=D;return L&&L.kind==="request"&&r.func(L.createCancellationTokenSource)&&(L.dispose===void 0||r.func(L.dispose))}s(I,"is"),E(I,"is"),A.is=I})(C||(e.RequestCancellationReceiverStrategy=C={}));var S;(function(A){A.Message=Object.freeze({createCancellationTokenSource(D){return new o.CancellationTokenSource}});function I(D){return w.is(D)||C.is(D)}s(I,"is"),E(I,"is"),A.is=I})(S||(e.CancellationReceiverStrategy=S={}));var k;(function(A){A.Message=Object.freeze({sendCancellation(D,L){return D.sendNotification(l.type,{id:L})},cleanup(D){}});function I(D){let L=D;return L&&r.func(L.sendCancellation)&&r.func(L.cleanup)}s(I,"is"),E(I,"is"),A.is=I})(k||(e.CancellationSenderStrategy=k={}));var R;(function(A){A.Message=Object.freeze({receiver:S.Message,sender:k.Message});function I(D){let L=D;return L&&S.is(L.receiver)&&k.is(L.sender)}s(I,"is"),E(I,"is"),A.is=I})(R||(e.CancellationStrategy=R={}));var M;(function(A){function I(D){let L=D;return L&&r.func(L.handleMessage)}s(I,"is"),E(I,"is"),A.is=I})(M||(e.MessageStrategy=M={}));var P;(function(A){function I(D){let L=D;return L&&(R.is(L.cancellationStrategy)||T.is(L.connectionStrategy)||M.is(L.messageStrategy))}s(I,"is"),E(I,"is"),A.is=I})(P||(e.ConnectionOptions=P={}));var N;(function(A){A[A.New=1]="New",A[A.Listening=2]="Listening",A[A.Closed=3]="Closed",A[A.Disposed=4]="Disposed"})(N||(N={}));function _(A,I,D,L){let B=D!==void 0?D:e.NullLogger,O=0,$=0,W=0,V="2.0",z,H=new Map,K,J=new Map,ne=new Map,Z,pe=new i.LinkedMap,te=new Map,fe=new Set,ce=new Map,de=p.Off,Ee=g.Text,we,Fe=N.New,Ye=new a.Emitter,He=new a.Emitter,ye=new a.Emitter,Be=new a.Emitter,Ie=new a.Emitter,Ne=L&&L.cancellationStrategy?L.cancellationStrategy:R.Message;function re(De){if(De===null)throw new Error("Can't send requests with id null since the response can't be correlated.");return"req-"+De.toString()}s(re,"createRequestQueueKey"),E(re,"createRequestQueueKey");function ue(De){return De===null?"res-unknown-"+(++W).toString():"res-"+De.toString()}s(ue,"createResponseQueueKey"),E(ue,"createResponseQueueKey");function le(){return"not-"+(++$).toString()}s(le,"createNotificationQueueKey"),E(le,"createNotificationQueueKey");function he(De,tt){n.Message.isRequest(tt)?De.set(re(tt.id),tt):n.Message.isResponse(tt)?De.set(ue(tt.id),tt):De.set(le(),tt)}s(he,"addMessageToQueue"),E(he,"addMessageToQueue");function Te(De){}s(Te,"cancelUndispatched"),E(Te,"cancelUndispatched");function oe(){return Fe===N.Listening}s(oe,"isListening"),E(oe,"isListening");function G(){return Fe===N.Closed}s(G,"isClosed"),E(G,"isClosed");function X(){return Fe===N.Disposed}s(X,"isDisposed"),E(X,"isDisposed");function ee(){(Fe===N.New||Fe===N.Listening)&&(Fe=N.Closed,He.fire(void 0))}s(ee,"closeHandler"),E(ee,"closeHandler");function q(De){Ye.fire([De,void 0,void 0])}s(q,"readErrorHandler"),E(q,"readErrorHandler");function ie(De){Ye.fire(De)}s(ie,"writeErrorHandler"),E(ie,"writeErrorHandler"),A.onClose(ee),A.onError(q),I.onClose(ee),I.onError(ie);function j(){Z||pe.size===0||(Z=(0,t.default)().timer.setImmediate(()=>{Z=void 0,Y()}))}s(j,"triggerMessageQueue"),E(j,"triggerMessageQueue");function U(De){n.Message.isRequest(De)?me(De):n.Message.isNotification(De)?Ce(De):n.Message.isResponse(De)?Se(De):Me(De)}s(U,"handleMessage"),E(U,"handleMessage");function Y(){if(pe.size===0)return;let De=pe.shift();try{let tt=L?.messageStrategy;M.is(tt)?tt.handleMessage(De,U):U(De)}finally{j()}}s(Y,"processMessageQueue"),E(Y,"processMessageQueue");let Ae=E(De=>{try{if(n.Message.isNotification(De)&&De.method===l.type.method){let tt=De.params.id,Ze=re(tt),dt=pe.get(Ze);if(n.Message.isRequest(dt)){let $t=L?.connectionStrategy,Yt=$t&&$t.cancelUndispatched?$t.cancelUndispatched(dt,Te):void 0;if(Yt&&(Yt.error!==void 0||Yt.result!==void 0)){pe.delete(Ze),ce.delete(tt),Yt.id=dt.id,qe(Yt,De.method,Date.now()),I.write(Yt).catch(()=>B.error("Sending response for canceled message failed."));return}}let Ut=ce.get(tt);if(Ut!==void 0){Ut.cancel(),lt(De);return}else fe.add(tt)}he(pe,De)}finally{j()}},"callback");function me(De){if(X())return;function tt(lr,Wt,ar){let et={jsonrpc:V,id:De.id};lr instanceof n.ResponseError?et.error=lr.toJson():et.result=lr===void 0?null:lr,qe(et,Wt,ar),I.write(et).catch(()=>B.error("Sending response failed."))}s(tt,"reply"),E(tt,"reply");function Ze(lr,Wt,ar){let et={jsonrpc:V,id:De.id,error:lr.toJson()};qe(et,Wt,ar),I.write(et).catch(()=>B.error("Sending response failed."))}s(Ze,"replyError"),E(Ze,"replyError");function dt(lr,Wt,ar){lr===void 0&&(lr=null);let et={jsonrpc:V,id:De.id,result:lr};qe(et,Wt,ar),I.write(et).catch(()=>B.error("Sending response failed."))}s(dt,"replySuccess"),E(dt,"replySuccess"),ve(De);let Ut=H.get(De.method),$t,Yt;Ut&&($t=Ut.type,Yt=Ut.handler);let Sr=Date.now();if(Yt||z){let lr=De.id??String(Date.now()),Wt=w.is(Ne.receiver)?Ne.receiver.createCancellationTokenSource(lr):Ne.receiver.createCancellationTokenSource(De);De.id!==null&&fe.has(De.id)&&Wt.cancel(),De.id!==null&&ce.set(lr,Wt);try{let ar;if(Yt)if(De.params===void 0){if($t!==void 0&&$t.numberOfParams!==0){Ze(new n.ResponseError(n.ErrorCodes.InvalidParams,`Request ${De.method} defines ${$t.numberOfParams} params but received none.`),De.method,Sr);return}ar=Yt(Wt.token)}else if(Array.isArray(De.params)){if($t!==void 0&&$t.parameterStructures===n.ParameterStructures.byName){Ze(new n.ResponseError(n.ErrorCodes.InvalidParams,`Request ${De.method} defines parameters by name but received parameters by position`),De.method,Sr);return}ar=Yt(...De.params,Wt.token)}else{if($t!==void 0&&$t.parameterStructures===n.ParameterStructures.byPosition){Ze(new n.ResponseError(n.ErrorCodes.InvalidParams,`Request ${De.method} defines parameters by position but received parameters by name`),De.method,Sr);return}ar=Yt(De.params,Wt.token)}else z&&(ar=z(De.method,De.params,Wt.token));let et=ar;ar?et.then?et.then(pt=>{ce.delete(lr),tt(pt,De.method,Sr)},pt=>{ce.delete(lr),pt instanceof n.ResponseError?Ze(pt,De.method,Sr):pt&&r.string(pt.message)?Ze(new n.ResponseError(n.ErrorCodes.InternalError,`Request ${De.method} failed with message: ${pt.message}`),De.method,Sr):Ze(new n.ResponseError(n.ErrorCodes.InternalError,`Request ${De.method} failed unexpectedly without providing any details.`),De.method,Sr)}):(ce.delete(lr),tt(ar,De.method,Sr)):(ce.delete(lr),dt(ar,De.method,Sr))}catch(ar){ce.delete(lr),ar instanceof n.ResponseError?tt(ar,De.method,Sr):ar&&r.string(ar.message)?Ze(new n.ResponseError(n.ErrorCodes.InternalError,`Request ${De.method} failed with message: ${ar.message}`),De.method,Sr):Ze(new n.ResponseError(n.ErrorCodes.InternalError,`Request ${De.method} failed unexpectedly without providing any details.`),De.method,Sr)}}else Ze(new n.ResponseError(n.ErrorCodes.MethodNotFound,`Unhandled method ${De.method}`),De.method,Sr)}s(me,"handleRequest"),E(me,"handleRequest");function Se(De){if(!X())if(De.id===null)De.error?B.error(`Received response message without id: Error is: +${JSON.stringify(De.error,void 0,4)}`):B.error("Received response message without id. No further error information provided.");else{let tt=De.id,Ze=te.get(tt);if(Oe(De,Ze),Ze!==void 0){te.delete(tt);try{if(De.error){let dt=De.error;Ze.reject(new n.ResponseError(dt.code,dt.message,dt.data))}else if(De.result!==void 0)Ze.resolve(De.result);else throw new Error("Should never happen.")}catch(dt){dt.message?B.error(`Response handler '${Ze.method}' failed with message: ${dt.message}`):B.error(`Response handler '${Ze.method}' failed unexpectedly.`)}}}}s(Se,"handleResponse"),E(Se,"handleResponse");function Ce(De){if(X())return;let tt,Ze;if(De.method===l.type.method){let dt=De.params.id;fe.delete(dt),lt(De);return}else{let dt=J.get(De.method);dt&&(Ze=dt.handler,tt=dt.type)}if(Ze||K)try{if(lt(De),Ze)if(De.params===void 0)tt!==void 0&&tt.numberOfParams!==0&&tt.parameterStructures!==n.ParameterStructures.byName&&B.error(`Notification ${De.method} defines ${tt.numberOfParams} params but received none.`),Ze();else if(Array.isArray(De.params)){let dt=De.params;De.method===h.type.method&&dt.length===2&&u.is(dt[0])?Ze({token:dt[0],value:dt[1]}):(tt!==void 0&&(tt.parameterStructures===n.ParameterStructures.byName&&B.error(`Notification ${De.method} defines parameters by name but received parameters by position`),tt.numberOfParams!==De.params.length&&B.error(`Notification ${De.method} defines ${tt.numberOfParams} params but received ${dt.length} arguments`)),Ze(...dt))}else tt!==void 0&&tt.parameterStructures===n.ParameterStructures.byPosition&&B.error(`Notification ${De.method} defines parameters by position but received parameters by name`),Ze(De.params);else K&&K(De.method,De.params)}catch(dt){dt.message?B.error(`Notification handler '${De.method}' failed with message: ${dt.message}`):B.error(`Notification handler '${De.method}' failed unexpectedly.`)}else ye.fire(De)}s(Ce,"handleNotification"),E(Ce,"handleNotification");function Me(De){if(!De){B.error("Received empty message.");return}B.error(`Received message which is neither a response nor a notification message: +${JSON.stringify(De,null,4)}`);let tt=De;if(r.string(tt.id)||r.number(tt.id)){let Ze=tt.id,dt=te.get(Ze);dt&&dt.reject(new Error("The received response has neither a result nor an error property."))}}s(Me,"handleInvalidMessage"),E(Me,"handleInvalidMessage");function _e(De){if(De!=null)switch(de){case p.Verbose:return JSON.stringify(De,null,4);case p.Compact:return JSON.stringify(De);default:return}}s(_e,"stringifyTrace"),E(_e,"stringifyTrace");function ze(De){if(!(de===p.Off||!we))if(Ee===g.Text){let tt;(de===p.Verbose||de===p.Compact)&&De.params&&(tt=`Params: ${_e(De.params)} + +`),we.log(`Sending request '${De.method} - (${De.id})'.`,tt)}else mt("send-request",De)}s(ze,"traceSendingRequest"),E(ze,"traceSendingRequest");function Le(De){if(!(de===p.Off||!we))if(Ee===g.Text){let tt;(de===p.Verbose||de===p.Compact)&&(De.params?tt=`Params: ${_e(De.params)} + +`:tt=`No parameters provided. + +`),we.log(`Sending notification '${De.method}'.`,tt)}else mt("send-notification",De)}s(Le,"traceSendingNotification"),E(Le,"traceSendingNotification");function qe(De,tt,Ze){if(!(de===p.Off||!we))if(Ee===g.Text){let dt;(de===p.Verbose||de===p.Compact)&&(De.error&&De.error.data?dt=`Error data: ${_e(De.error.data)} + +`:De.result?dt=`Result: ${_e(De.result)} + +`:De.error===void 0&&(dt=`No result returned. + +`)),we.log(`Sending response '${tt} - (${De.id})'. Processing request took ${Date.now()-Ze}ms`,dt)}else mt("send-response",De)}s(qe,"traceSendingResponse"),E(qe,"traceSendingResponse");function ve(De){if(!(de===p.Off||!we))if(Ee===g.Text){let tt;(de===p.Verbose||de===p.Compact)&&De.params&&(tt=`Params: ${_e(De.params)} + +`),we.log(`Received request '${De.method} - (${De.id})'.`,tt)}else mt("receive-request",De)}s(ve,"traceReceivedRequest"),E(ve,"traceReceivedRequest");function lt(De){if(!(de===p.Off||!we||De.method===v.type.method))if(Ee===g.Text){let tt;(de===p.Verbose||de===p.Compact)&&(De.params?tt=`Params: ${_e(De.params)} + +`:tt=`No parameters provided. + +`),we.log(`Received notification '${De.method}'.`,tt)}else mt("receive-notification",De)}s(lt,"traceReceivedNotification"),E(lt,"traceReceivedNotification");function Oe(De,tt){if(!(de===p.Off||!we))if(Ee===g.Text){let Ze;if((de===p.Verbose||de===p.Compact)&&(De.error&&De.error.data?Ze=`Error data: ${_e(De.error.data)} + +`:De.result?Ze=`Result: ${_e(De.result)} + +`:De.error===void 0&&(Ze=`No result returned. + +`)),tt){let dt=De.error?` Request failed: ${De.error.message} (${De.error.code}).`:"";we.log(`Received response '${tt.method} - (${De.id})' in ${Date.now()-tt.timerStart}ms.${dt}`,Ze)}else we.log(`Received response ${De.id} without active response promise.`,Ze)}else mt("receive-response",De)}s(Oe,"traceReceivedResponse"),E(Oe,"traceReceivedResponse");function mt(De,tt){if(!we||de===p.Off)return;let Ze={isLSPMessage:!0,type:De,message:tt,timestamp:Date.now()};we.log(Ze)}s(mt,"logLSPMessage"),E(mt,"logLSPMessage");function At(){if(G())throw new b(x.Closed,"Connection is closed.");if(X())throw new b(x.Disposed,"Connection is disposed.")}s(At,"throwIfClosedOrDisposed"),E(At,"throwIfClosedOrDisposed");function Gt(){if(oe())throw new b(x.AlreadyListening,"Connection is already listening")}s(Gt,"throwIfListening"),E(Gt,"throwIfListening");function Ct(){if(!oe())throw new Error("Call listen() first.")}s(Ct,"throwIfNotListening"),E(Ct,"throwIfNotListening");function Rt(De){return De===void 0?null:De}s(Rt,"undefinedToNull"),E(Rt,"undefinedToNull");function gt(De){if(De!==null)return De}s(gt,"nullToUndefined"),E(gt,"nullToUndefined");function be(De){return De!=null&&!Array.isArray(De)&&typeof De=="object"}s(be,"isNamedParam"),E(be,"isNamedParam");function ct(De,tt){switch(De){case n.ParameterStructures.auto:return be(tt)?gt(tt):[Rt(tt)];case n.ParameterStructures.byName:if(!be(tt))throw new Error("Received parameters by name but param is not an object literal.");return gt(tt);case n.ParameterStructures.byPosition:return[Rt(tt)];default:throw new Error(`Unknown parameter structure ${De.toString()}`)}}s(ct,"computeSingleParam"),E(ct,"computeSingleParam");function xt(De,tt){let Ze,dt=De.numberOfParams;switch(dt){case 0:Ze=void 0;break;case 1:Ze=ct(De.parameterStructures,tt[0]);break;default:Ze=[];for(let Ut=0;Ut{At();let Ze,dt;if(r.string(De)){Ze=De;let $t=tt[0],Yt=0,Sr=n.ParameterStructures.auto;n.ParameterStructures.is($t)&&(Yt=1,Sr=$t);let lr=tt.length,Wt=lr-Yt;switch(Wt){case 0:dt=void 0;break;case 1:dt=ct(Sr,tt[Yt]);break;default:if(Sr===n.ParameterStructures.byName)throw new Error(`Received ${Wt} parameters for 'by Name' notification parameter structure.`);dt=tt.slice(Yt,lr).map(ar=>Rt(ar));break}}else{let $t=tt;Ze=De.method,dt=xt(De,$t)}let Ut={jsonrpc:V,method:Ze,params:dt};return Le(Ut),I.write(Ut).catch($t=>{throw B.error("Sending notification failed."),$t})},"sendNotification"),onNotification:E((De,tt)=>{At();let Ze;return r.func(De)?K=De:tt&&(r.string(De)?(Ze=De,J.set(De,{type:void 0,handler:tt})):(Ze=De.method,J.set(De.method,{type:De,handler:tt}))),{dispose:E(()=>{Ze!==void 0?J.delete(Ze):K=void 0},"dispose")}},"onNotification"),onProgress:E((De,tt,Ze)=>{if(ne.has(tt))throw new Error(`Progress handler for token ${tt} already registered`);return ne.set(tt,Ze),{dispose:E(()=>{ne.delete(tt)},"dispose")}},"onProgress"),sendProgress:E((De,tt,Ze)=>Ke.sendNotification(h.type,{token:tt,value:Ze}),"sendProgress"),onUnhandledProgress:Be.event,sendRequest:E((De,...tt)=>{At(),Ct();let Ze,dt,Ut;if(r.string(De)){Ze=De;let lr=tt[0],Wt=tt[tt.length-1],ar=0,et=n.ParameterStructures.auto;n.ParameterStructures.is(lr)&&(ar=1,et=lr);let pt=tt.length;o.CancellationToken.is(Wt)&&(pt=pt-1,Ut=Wt);let kt=pt-ar;switch(kt){case 0:dt=void 0;break;case 1:dt=ct(et,tt[ar]);break;default:if(et===n.ParameterStructures.byName)throw new Error(`Received ${kt} parameters for 'by Name' request parameter structure.`);dt=tt.slice(ar,pt).map(vr=>Rt(vr));break}}else{let lr=tt;Ze=De.method,dt=xt(De,lr);let Wt=De.numberOfParams;Ut=o.CancellationToken.is(lr[Wt])?lr[Wt]:void 0}let $t=O++,Yt;Ut&&(Yt=Ut.onCancellationRequested(()=>{let lr=Ne.sender.sendCancellation(Ke,$t);return lr===void 0?(B.log(`Received no promise from cancellation strategy when cancelling id ${$t}`),Promise.resolve()):lr.catch(()=>{B.log(`Sending cancellation messages for id ${$t} failed`)})}));let Sr={jsonrpc:V,id:$t,method:Ze,params:dt};return ze(Sr),typeof Ne.sender.enableCancellation=="function"&&Ne.sender.enableCancellation(Sr),new Promise(async(lr,Wt)=>{let ar=E(kt=>{lr(kt),Ne.sender.cleanup($t),Yt?.dispose()},"resolveWithCleanup"),et=E(kt=>{Wt(kt),Ne.sender.cleanup($t),Yt?.dispose()},"rejectWithCleanup"),pt={method:Ze,timerStart:Date.now(),resolve:ar,reject:et};try{await I.write(Sr),te.set($t,pt)}catch(kt){throw B.error("Sending request failed."),pt.reject(new n.ResponseError(n.ErrorCodes.MessageWriteError,kt.message?kt.message:"Unknown reason")),kt}})},"sendRequest"),onRequest:E((De,tt)=>{At();let Ze=null;return f.is(De)?(Ze=void 0,z=De):r.string(De)?(Ze=null,tt!==void 0&&(Ze=De,H.set(De,{handler:tt,type:void 0}))):tt!==void 0&&(Ze=De.method,H.set(De.method,{type:De,handler:tt})),{dispose:E(()=>{Ze!==null&&(Ze!==void 0?H.delete(Ze):z=void 0)},"dispose")}},"onRequest"),hasPendingResponse:E(()=>te.size>0,"hasPendingResponse"),trace:E(async(De,tt,Ze)=>{let dt=!1,Ut=g.Text;Ze!==void 0&&(r.boolean(Ze)?dt=Ze:(dt=Ze.sendNotification||!1,Ut=Ze.traceFormat||g.Text)),de=De,Ee=Ut,de===p.Off?we=void 0:we=tt,dt&&!G()&&!X()&&await Ke.sendNotification(y.type,{value:p.toString(De)})},"trace"),onError:Ye.event,onClose:He.event,onUnhandledNotification:ye.event,onDispose:Ie.event,end:E(()=>{I.end()},"end"),dispose:E(()=>{if(X())return;Fe=N.Disposed,Ie.fire(void 0);let De=new n.ResponseError(n.ErrorCodes.PendingResponseRejected,"Pending response rejected since connection got disposed");for(let tt of te.values())tt.reject(De);te=new Map,ce=new Map,fe=new Set,pe=new i.LinkedMap,r.func(I.dispose)&&I.dispose(),r.func(A.dispose)&&A.dispose()},"dispose"),listen:E(()=>{At(),Gt(),Fe=N.Listening,A.listen(Ae)},"listen"),inspect:E(()=>{(0,t.default)().console.log("inspect")},"inspect")};return Ke.onNotification(v.type,De=>{if(de===p.Off||!we)return;let tt=de===p.Verbose||de===p.Compact;we.log(De.message,tt?De.verbose:void 0)}),Ke.onNotification(h.type,De=>{let tt=ne.get(De.token);tt?tt(De.value):Be.fire(De)}),Ke}s(_,"createMessageConnection"),E(_,"createMessageConnection"),e.createMessageConnection=_}}),Rz=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/api.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ProgressType=e.ProgressToken=e.createMessageConnection=e.NullLogger=e.ConnectionOptions=e.ConnectionStrategy=e.AbstractMessageBuffer=e.WriteableStreamMessageWriter=e.AbstractMessageWriter=e.MessageWriter=e.ReadableStreamMessageReader=e.AbstractMessageReader=e.MessageReader=e.SharedArrayReceiverStrategy=e.SharedArraySenderStrategy=e.CancellationToken=e.CancellationTokenSource=e.Emitter=e.Event=e.Disposable=e.LRUCache=e.Touch=e.LinkedMap=e.ParameterStructures=e.NotificationType9=e.NotificationType8=e.NotificationType7=e.NotificationType6=e.NotificationType5=e.NotificationType4=e.NotificationType3=e.NotificationType2=e.NotificationType1=e.NotificationType0=e.NotificationType=e.ErrorCodes=e.ResponseError=e.RequestType9=e.RequestType8=e.RequestType7=e.RequestType6=e.RequestType5=e.RequestType4=e.RequestType3=e.RequestType2=e.RequestType1=e.RequestType0=e.RequestType=e.Message=e.RAL=void 0,e.MessageStrategy=e.CancellationStrategy=e.CancellationSenderStrategy=e.CancellationReceiverStrategy=e.ConnectionError=e.ConnectionErrors=e.LogTraceNotification=e.SetTraceNotification=e.TraceFormat=e.TraceValues=e.Trace=void 0;var t=Swe();Object.defineProperty(e,"Message",{enumerable:!0,get:E(function(){return t.Message},"get")}),Object.defineProperty(e,"RequestType",{enumerable:!0,get:E(function(){return t.RequestType},"get")}),Object.defineProperty(e,"RequestType0",{enumerable:!0,get:E(function(){return t.RequestType0},"get")}),Object.defineProperty(e,"RequestType1",{enumerable:!0,get:E(function(){return t.RequestType1},"get")}),Object.defineProperty(e,"RequestType2",{enumerable:!0,get:E(function(){return t.RequestType2},"get")}),Object.defineProperty(e,"RequestType3",{enumerable:!0,get:E(function(){return t.RequestType3},"get")}),Object.defineProperty(e,"RequestType4",{enumerable:!0,get:E(function(){return t.RequestType4},"get")}),Object.defineProperty(e,"RequestType5",{enumerable:!0,get:E(function(){return t.RequestType5},"get")}),Object.defineProperty(e,"RequestType6",{enumerable:!0,get:E(function(){return t.RequestType6},"get")}),Object.defineProperty(e,"RequestType7",{enumerable:!0,get:E(function(){return t.RequestType7},"get")}),Object.defineProperty(e,"RequestType8",{enumerable:!0,get:E(function(){return t.RequestType8},"get")}),Object.defineProperty(e,"RequestType9",{enumerable:!0,get:E(function(){return t.RequestType9},"get")}),Object.defineProperty(e,"ResponseError",{enumerable:!0,get:E(function(){return t.ResponseError},"get")}),Object.defineProperty(e,"ErrorCodes",{enumerable:!0,get:E(function(){return t.ErrorCodes},"get")}),Object.defineProperty(e,"NotificationType",{enumerable:!0,get:E(function(){return t.NotificationType},"get")}),Object.defineProperty(e,"NotificationType0",{enumerable:!0,get:E(function(){return t.NotificationType0},"get")}),Object.defineProperty(e,"NotificationType1",{enumerable:!0,get:E(function(){return t.NotificationType1},"get")}),Object.defineProperty(e,"NotificationType2",{enumerable:!0,get:E(function(){return t.NotificationType2},"get")}),Object.defineProperty(e,"NotificationType3",{enumerable:!0,get:E(function(){return t.NotificationType3},"get")}),Object.defineProperty(e,"NotificationType4",{enumerable:!0,get:E(function(){return t.NotificationType4},"get")}),Object.defineProperty(e,"NotificationType5",{enumerable:!0,get:E(function(){return t.NotificationType5},"get")}),Object.defineProperty(e,"NotificationType6",{enumerable:!0,get:E(function(){return t.NotificationType6},"get")}),Object.defineProperty(e,"NotificationType7",{enumerable:!0,get:E(function(){return t.NotificationType7},"get")}),Object.defineProperty(e,"NotificationType8",{enumerable:!0,get:E(function(){return t.NotificationType8},"get")}),Object.defineProperty(e,"NotificationType9",{enumerable:!0,get:E(function(){return t.NotificationType9},"get")}),Object.defineProperty(e,"ParameterStructures",{enumerable:!0,get:E(function(){return t.ParameterStructures},"get")});var r=kwe();Object.defineProperty(e,"LinkedMap",{enumerable:!0,get:E(function(){return r.LinkedMap},"get")}),Object.defineProperty(e,"LRUCache",{enumerable:!0,get:E(function(){return r.LRUCache},"get")}),Object.defineProperty(e,"Touch",{enumerable:!0,get:E(function(){return r.Touch},"get")});var n=_gt();Object.defineProperty(e,"Disposable",{enumerable:!0,get:E(function(){return n.Disposable},"get")});var i=_v();Object.defineProperty(e,"Event",{enumerable:!0,get:E(function(){return i.Event},"get")}),Object.defineProperty(e,"Emitter",{enumerable:!0,get:E(function(){return i.Emitter},"get")});var a=t6();Object.defineProperty(e,"CancellationTokenSource",{enumerable:!0,get:E(function(){return a.CancellationTokenSource},"get")}),Object.defineProperty(e,"CancellationToken",{enumerable:!0,get:E(function(){return a.CancellationToken},"get")});var o=Lgt();Object.defineProperty(e,"SharedArraySenderStrategy",{enumerable:!0,get:E(function(){return o.SharedArraySenderStrategy},"get")}),Object.defineProperty(e,"SharedArrayReceiverStrategy",{enumerable:!0,get:E(function(){return o.SharedArrayReceiverStrategy},"get")});var l=Dgt();Object.defineProperty(e,"MessageReader",{enumerable:!0,get:E(function(){return l.MessageReader},"get")}),Object.defineProperty(e,"AbstractMessageReader",{enumerable:!0,get:E(function(){return l.AbstractMessageReader},"get")}),Object.defineProperty(e,"ReadableStreamMessageReader",{enumerable:!0,get:E(function(){return l.ReadableStreamMessageReader},"get")});var u=Igt();Object.defineProperty(e,"MessageWriter",{enumerable:!0,get:E(function(){return u.MessageWriter},"get")}),Object.defineProperty(e,"AbstractMessageWriter",{enumerable:!0,get:E(function(){return u.AbstractMessageWriter},"get")}),Object.defineProperty(e,"WriteableStreamMessageWriter",{enumerable:!0,get:E(function(){return u.WriteableStreamMessageWriter},"get")});var h=Mgt();Object.defineProperty(e,"AbstractMessageBuffer",{enumerable:!0,get:E(function(){return h.AbstractMessageBuffer},"get")});var d=Ngt();Object.defineProperty(e,"ConnectionStrategy",{enumerable:!0,get:E(function(){return d.ConnectionStrategy},"get")}),Object.defineProperty(e,"ConnectionOptions",{enumerable:!0,get:E(function(){return d.ConnectionOptions},"get")}),Object.defineProperty(e,"NullLogger",{enumerable:!0,get:E(function(){return d.NullLogger},"get")}),Object.defineProperty(e,"createMessageConnection",{enumerable:!0,get:E(function(){return d.createMessageConnection},"get")}),Object.defineProperty(e,"ProgressToken",{enumerable:!0,get:E(function(){return d.ProgressToken},"get")}),Object.defineProperty(e,"ProgressType",{enumerable:!0,get:E(function(){return d.ProgressType},"get")}),Object.defineProperty(e,"Trace",{enumerable:!0,get:E(function(){return d.Trace},"get")}),Object.defineProperty(e,"TraceValues",{enumerable:!0,get:E(function(){return d.TraceValues},"get")}),Object.defineProperty(e,"TraceFormat",{enumerable:!0,get:E(function(){return d.TraceFormat},"get")}),Object.defineProperty(e,"SetTraceNotification",{enumerable:!0,get:E(function(){return d.SetTraceNotification},"get")}),Object.defineProperty(e,"LogTraceNotification",{enumerable:!0,get:E(function(){return d.LogTraceNotification},"get")}),Object.defineProperty(e,"ConnectionErrors",{enumerable:!0,get:E(function(){return d.ConnectionErrors},"get")}),Object.defineProperty(e,"ConnectionError",{enumerable:!0,get:E(function(){return d.ConnectionError},"get")}),Object.defineProperty(e,"CancellationReceiverStrategy",{enumerable:!0,get:E(function(){return d.CancellationReceiverStrategy},"get")}),Object.defineProperty(e,"CancellationSenderStrategy",{enumerable:!0,get:E(function(){return d.CancellationSenderStrategy},"get")}),Object.defineProperty(e,"CancellationStrategy",{enumerable:!0,get:E(function(){return d.CancellationStrategy},"get")}),Object.defineProperty(e,"MessageStrategy",{enumerable:!0,get:E(function(){return d.MessageStrategy},"get")});var f=Qg();e.RAL=f.default}}),Pgt=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/browser/ril.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=Rz(),r=class Rwe extends t.AbstractMessageBuffer{static{s(this,"_MessageBuffer")}static{E(this,"MessageBuffer")}constructor(h="utf-8"){super(h),this.asciiDecoder=new TextDecoder("ascii")}emptyBuffer(){return Rwe.emptyBuffer}fromString(h,d){return new TextEncoder().encode(h)}toString(h,d){return d==="ascii"?this.asciiDecoder.decode(h):new TextDecoder(d).decode(h)}asNative(h,d){return d===void 0?h:h.slice(0,d)}allocNative(h){return new Uint8Array(h)}};r.emptyBuffer=new Uint8Array(0);var n=class{static{s(this,"ReadableStreamWrapper")}static{E(this,"ReadableStreamWrapper")}constructor(u){this.socket=u,this._onData=new t.Emitter,this._messageListener=h=>{h.data.arrayBuffer().then(f=>{this._onData.fire(new Uint8Array(f))},()=>{(0,t.RAL)().console.error("Converting blob to array buffer failed.")})},this.socket.addEventListener("message",this._messageListener)}onClose(u){return this.socket.addEventListener("close",u),t.Disposable.create(()=>this.socket.removeEventListener("close",u))}onError(u){return this.socket.addEventListener("error",u),t.Disposable.create(()=>this.socket.removeEventListener("error",u))}onEnd(u){return this.socket.addEventListener("end",u),t.Disposable.create(()=>this.socket.removeEventListener("end",u))}onData(u){return this._onData.event(u)}},i=class{static{s(this,"WritableStreamWrapper")}static{E(this,"WritableStreamWrapper")}constructor(u){this.socket=u}onClose(u){return this.socket.addEventListener("close",u),t.Disposable.create(()=>this.socket.removeEventListener("close",u))}onError(u){return this.socket.addEventListener("error",u),t.Disposable.create(()=>this.socket.removeEventListener("error",u))}onEnd(u){return this.socket.addEventListener("end",u),t.Disposable.create(()=>this.socket.removeEventListener("end",u))}write(u,h){if(typeof u=="string"){if(h!==void 0&&h!=="utf-8")throw new Error(`In a Browser environments only utf-8 text encoding is supported. But got encoding: ${h}`);this.socket.send(u)}else this.socket.send(u);return Promise.resolve()}end(){this.socket.close()}},a=new TextEncoder,o=Object.freeze({messageBuffer:Object.freeze({create:E(u=>new r(u),"create")}),applicationJson:Object.freeze({encoder:Object.freeze({name:"application/json",encode:E((u,h)=>{if(h.charset!=="utf-8")throw new Error(`In a Browser environments only utf-8 text encoding is supported. But got encoding: ${h.charset}`);return Promise.resolve(a.encode(JSON.stringify(u,void 0,0)))},"encode")}),decoder:Object.freeze({name:"application/json",decode:E((u,h)=>{if(!(u instanceof Uint8Array))throw new Error("In a Browser environments only Uint8Arrays are supported.");return Promise.resolve(JSON.parse(new TextDecoder(h.charset).decode(u)))},"decode")})}),stream:Object.freeze({asReadableStream:E(u=>new n(u),"asReadableStream"),asWritableStream:E(u=>new i(u),"asWritableStream")}),console,timer:Object.freeze({setTimeout(u,h,...d){let f=setTimeout(u,h,...d);return{dispose:E(()=>clearTimeout(f),"dispose")}},setImmediate(u,...h){let d=setTimeout(u,0,...h);return{dispose:E(()=>clearTimeout(d),"dispose")}},setInterval(u,h,...d){let f=setInterval(u,h,...d);return{dispose:E(()=>clearInterval(f),"dispose")}}})});function l(){return o}s(l,"RIL"),E(l,"RIL"),(function(u){function h(){t.RAL.install(o)}s(h,"install"),E(h,"install"),u.install=h})(l||(l={})),e.default=l}}),Lv=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/browser/main.js"(e){"use strict";var t=e&&e.__createBinding||(Object.create?(function(u,h,d,f){f===void 0&&(f=d);var p=Object.getOwnPropertyDescriptor(h,d);(!p||("get"in p?!h.__esModule:p.writable||p.configurable))&&(p={enumerable:!0,get:E(function(){return h[d]},"get")}),Object.defineProperty(u,f,p)}):(function(u,h,d,f){f===void 0&&(f=d),u[f]=h[d]})),r=e&&e.__exportStar||function(u,h){for(var d in u)d!=="default"&&!Object.prototype.hasOwnProperty.call(h,d)&&t(h,u,d)};Object.defineProperty(e,"__esModule",{value:!0}),e.createMessageConnection=e.BrowserMessageWriter=e.BrowserMessageReader=void 0;var n=Pgt();n.default.install();var i=Rz();r(Rz(),e);var a=class extends i.AbstractMessageReader{static{s(this,"BrowserMessageReader")}static{E(this,"BrowserMessageReader")}constructor(u){super(),this._onData=new i.Emitter,this._messageListener=h=>{this._onData.fire(h.data)},u.addEventListener("error",h=>this.fireError(h)),u.onmessage=this._messageListener}listen(u){return this._onData.event(u)}};e.BrowserMessageReader=a;var o=class extends i.AbstractMessageWriter{static{s(this,"BrowserMessageWriter")}static{E(this,"BrowserMessageWriter")}constructor(u){super(),this.port=u,this.errorCount=0,u.addEventListener("error",h=>this.fireError(h))}write(u){try{return this.port.postMessage(u),Promise.resolve()}catch(h){return this.handleError(h,u),Promise.reject(h)}}handleError(u,h){this.errorCount++,this.fireError(u,h,this.errorCount)}end(){}};e.BrowserMessageWriter=o;function l(u,h,d,f){return d===void 0&&(d=i.NullLogger),i.ConnectionStrategy.is(f)&&(f={connectionStrategy:f}),(0,i.createMessageConnection)(u,h,d,f)}s(l,"createMessageConnection"),E(l,"createMessageConnection"),e.createMessageConnection=l}}),LCe=Hr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/browser.js"(e,t){"use strict";t.exports=Lv()}}),_i=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/messages.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ProtocolNotificationType=e.ProtocolNotificationType0=e.ProtocolRequestType=e.ProtocolRequestType0=e.RegistrationType=e.MessageDirection=void 0;var t=Lv(),r;(function(u){u.clientToServer="clientToServer",u.serverToClient="serverToClient",u.both="both"})(r||(e.MessageDirection=r={}));var n=class{static{s(this,"RegistrationType")}static{E(this,"RegistrationType")}constructor(u){this.method=u}};e.RegistrationType=n;var i=class extends t.RequestType0{static{s(this,"ProtocolRequestType0")}static{E(this,"ProtocolRequestType0")}constructor(u){super(u)}};e.ProtocolRequestType0=i;var a=class extends t.RequestType{static{s(this,"ProtocolRequestType")}static{E(this,"ProtocolRequestType")}constructor(u){super(u,t.ParameterStructures.byName)}};e.ProtocolRequestType=a;var o=class extends t.NotificationType0{static{s(this,"ProtocolNotificationType0")}static{E(this,"ProtocolNotificationType0")}constructor(u){super(u)}};e.ProtocolNotificationType0=o;var l=class extends t.NotificationType{static{s(this,"ProtocolNotificationType")}static{E(this,"ProtocolNotificationType")}constructor(u){super(u,t.ParameterStructures.byName)}};e.ProtocolNotificationType=l}}),cW=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/utils/is.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.objectLiteral=e.typedArray=e.stringArray=e.array=e.func=e.error=e.number=e.string=e.boolean=void 0;function t(d){return d===!0||d===!1}s(t,"boolean"),E(t,"boolean"),e.boolean=t;function r(d){return typeof d=="string"||d instanceof String}s(r,"string"),E(r,"string"),e.string=r;function n(d){return typeof d=="number"||d instanceof Number}s(n,"number"),E(n,"number"),e.number=n;function i(d){return d instanceof Error}s(i,"error"),E(i,"error"),e.error=i;function a(d){return typeof d=="function"}s(a,"func"),E(a,"func"),e.func=a;function o(d){return Array.isArray(d)}s(o,"array"),E(o,"array"),e.array=o;function l(d){return o(d)&&d.every(f=>r(f))}s(l,"stringArray"),E(l,"stringArray"),e.stringArray=l;function u(d,f){return Array.isArray(d)&&d.every(f)}s(u,"typedArray"),E(u,"typedArray"),e.typedArray=u;function h(d){return d!==null&&typeof d=="object"}s(h,"objectLiteral"),E(h,"objectLiteral"),e.objectLiteral=h}}),Ogt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.implementation.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ImplementationRequest=void 0;var t=_i(),r;(function(n){n.method="textDocument/implementation",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.ImplementationRequest=r={}))}}),Bgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.typeDefinition.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TypeDefinitionRequest=void 0;var t=_i(),r;(function(n){n.method="textDocument/typeDefinition",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.TypeDefinitionRequest=r={}))}}),$gt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.workspaceFolder.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DidChangeWorkspaceFoldersNotification=e.WorkspaceFoldersRequest=void 0;var t=_i(),r;(function(i){i.method="workspace/workspaceFolders",i.messageDirection=t.MessageDirection.serverToClient,i.type=new t.ProtocolRequestType0(i.method)})(r||(e.WorkspaceFoldersRequest=r={}));var n;(function(i){i.method="workspace/didChangeWorkspaceFolders",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolNotificationType(i.method)})(n||(e.DidChangeWorkspaceFoldersNotification=n={}))}}),Fgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.configuration.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ConfigurationRequest=void 0;var t=_i(),r;(function(n){n.method="workspace/configuration",n.messageDirection=t.MessageDirection.serverToClient,n.type=new t.ProtocolRequestType(n.method)})(r||(e.ConfigurationRequest=r={}))}}),Ggt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.colorProvider.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ColorPresentationRequest=e.DocumentColorRequest=void 0;var t=_i(),r;(function(i){i.method="textDocument/documentColor",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolRequestType(i.method)})(r||(e.DocumentColorRequest=r={}));var n;(function(i){i.method="textDocument/colorPresentation",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolRequestType(i.method)})(n||(e.ColorPresentationRequest=n={}))}}),zgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.foldingRange.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.FoldingRangeRefreshRequest=e.FoldingRangeRequest=void 0;var t=_i(),r;(function(i){i.method="textDocument/foldingRange",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolRequestType(i.method)})(r||(e.FoldingRangeRequest=r={}));var n;(function(i){i.method="workspace/foldingRange/refresh",i.messageDirection=t.MessageDirection.serverToClient,i.type=new t.ProtocolRequestType0(i.method)})(n||(e.FoldingRangeRefreshRequest=n={}))}}),Vgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.declaration.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DeclarationRequest=void 0;var t=_i(),r;(function(n){n.method="textDocument/declaration",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.DeclarationRequest=r={}))}}),Wgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.selectionRange.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SelectionRangeRequest=void 0;var t=_i(),r;(function(n){n.method="textDocument/selectionRange",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.SelectionRangeRequest=r={}))}}),qgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.progress.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WorkDoneProgressCancelNotification=e.WorkDoneProgressCreateRequest=e.WorkDoneProgress=void 0;var t=Lv(),r=_i(),n;(function(o){o.type=new t.ProgressType;function l(u){return u===o.type}s(l,"is"),E(l,"is"),o.is=l})(n||(e.WorkDoneProgress=n={}));var i;(function(o){o.method="window/workDoneProgress/create",o.messageDirection=r.MessageDirection.serverToClient,o.type=new r.ProtocolRequestType(o.method)})(i||(e.WorkDoneProgressCreateRequest=i={}));var a;(function(o){o.method="window/workDoneProgress/cancel",o.messageDirection=r.MessageDirection.clientToServer,o.type=new r.ProtocolNotificationType(o.method)})(a||(e.WorkDoneProgressCancelNotification=a={}))}}),Hgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.callHierarchy.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CallHierarchyOutgoingCallsRequest=e.CallHierarchyIncomingCallsRequest=e.CallHierarchyPrepareRequest=void 0;var t=_i(),r;(function(a){a.method="textDocument/prepareCallHierarchy",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(r||(e.CallHierarchyPrepareRequest=r={}));var n;(function(a){a.method="callHierarchy/incomingCalls",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(n||(e.CallHierarchyIncomingCallsRequest=n={}));var i;(function(a){a.method="callHierarchy/outgoingCalls",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(i||(e.CallHierarchyOutgoingCallsRequest=i={}))}}),Ugt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.semanticTokens.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SemanticTokensRefreshRequest=e.SemanticTokensRangeRequest=e.SemanticTokensDeltaRequest=e.SemanticTokensRequest=e.SemanticTokensRegistrationType=e.TokenFormat=void 0;var t=_i(),r;(function(u){u.Relative="relative"})(r||(e.TokenFormat=r={}));var n;(function(u){u.method="textDocument/semanticTokens",u.type=new t.RegistrationType(u.method)})(n||(e.SemanticTokensRegistrationType=n={}));var i;(function(u){u.method="textDocument/semanticTokens/full",u.messageDirection=t.MessageDirection.clientToServer,u.type=new t.ProtocolRequestType(u.method),u.registrationMethod=n.method})(i||(e.SemanticTokensRequest=i={}));var a;(function(u){u.method="textDocument/semanticTokens/full/delta",u.messageDirection=t.MessageDirection.clientToServer,u.type=new t.ProtocolRequestType(u.method),u.registrationMethod=n.method})(a||(e.SemanticTokensDeltaRequest=a={}));var o;(function(u){u.method="textDocument/semanticTokens/range",u.messageDirection=t.MessageDirection.clientToServer,u.type=new t.ProtocolRequestType(u.method),u.registrationMethod=n.method})(o||(e.SemanticTokensRangeRequest=o={}));var l;(function(u){u.method="workspace/semanticTokens/refresh",u.messageDirection=t.MessageDirection.serverToClient,u.type=new t.ProtocolRequestType0(u.method)})(l||(e.SemanticTokensRefreshRequest=l={}))}}),Ygt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.showDocument.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ShowDocumentRequest=void 0;var t=_i(),r;(function(n){n.method="window/showDocument",n.messageDirection=t.MessageDirection.serverToClient,n.type=new t.ProtocolRequestType(n.method)})(r||(e.ShowDocumentRequest=r={}))}}),jgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.linkedEditingRange.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LinkedEditingRangeRequest=void 0;var t=_i(),r;(function(n){n.method="textDocument/linkedEditingRange",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.LinkedEditingRangeRequest=r={}))}}),Xgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.fileOperations.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WillDeleteFilesRequest=e.DidDeleteFilesNotification=e.DidRenameFilesNotification=e.WillRenameFilesRequest=e.DidCreateFilesNotification=e.WillCreateFilesRequest=e.FileOperationPatternKind=void 0;var t=_i(),r;(function(h){h.file="file",h.folder="folder"})(r||(e.FileOperationPatternKind=r={}));var n;(function(h){h.method="workspace/willCreateFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolRequestType(h.method)})(n||(e.WillCreateFilesRequest=n={}));var i;(function(h){h.method="workspace/didCreateFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolNotificationType(h.method)})(i||(e.DidCreateFilesNotification=i={}));var a;(function(h){h.method="workspace/willRenameFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolRequestType(h.method)})(a||(e.WillRenameFilesRequest=a={}));var o;(function(h){h.method="workspace/didRenameFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolNotificationType(h.method)})(o||(e.DidRenameFilesNotification=o={}));var l;(function(h){h.method="workspace/didDeleteFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolNotificationType(h.method)})(l||(e.DidDeleteFilesNotification=l={}));var u;(function(h){h.method="workspace/willDeleteFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolRequestType(h.method)})(u||(e.WillDeleteFilesRequest=u={}))}}),Kgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.moniker.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MonikerRequest=e.MonikerKind=e.UniquenessLevel=void 0;var t=_i(),r;(function(a){a.document="document",a.project="project",a.group="group",a.scheme="scheme",a.global="global"})(r||(e.UniquenessLevel=r={}));var n;(function(a){a.$import="import",a.$export="export",a.local="local"})(n||(e.MonikerKind=n={}));var i;(function(a){a.method="textDocument/moniker",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(i||(e.MonikerRequest=i={}))}}),Zgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.typeHierarchy.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TypeHierarchySubtypesRequest=e.TypeHierarchySupertypesRequest=e.TypeHierarchyPrepareRequest=void 0;var t=_i(),r;(function(a){a.method="textDocument/prepareTypeHierarchy",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(r||(e.TypeHierarchyPrepareRequest=r={}));var n;(function(a){a.method="typeHierarchy/supertypes",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(n||(e.TypeHierarchySupertypesRequest=n={}));var i;(function(a){a.method="typeHierarchy/subtypes",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(i||(e.TypeHierarchySubtypesRequest=i={}))}}),Qgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.inlineValue.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InlineValueRefreshRequest=e.InlineValueRequest=void 0;var t=_i(),r;(function(i){i.method="textDocument/inlineValue",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolRequestType(i.method)})(r||(e.InlineValueRequest=r={}));var n;(function(i){i.method="workspace/inlineValue/refresh",i.messageDirection=t.MessageDirection.serverToClient,i.type=new t.ProtocolRequestType0(i.method)})(n||(e.InlineValueRefreshRequest=n={}))}}),Jgt=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.inlayHint.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InlayHintRefreshRequest=e.InlayHintResolveRequest=e.InlayHintRequest=void 0;var t=_i(),r;(function(a){a.method="textDocument/inlayHint",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(r||(e.InlayHintRequest=r={}));var n;(function(a){a.method="inlayHint/resolve",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(n||(e.InlayHintResolveRequest=n={}));var i;(function(a){a.method="workspace/inlayHint/refresh",a.messageDirection=t.MessageDirection.serverToClient,a.type=new t.ProtocolRequestType0(a.method)})(i||(e.InlayHintRefreshRequest=i={}))}}),e0t=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.diagnostic.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DiagnosticRefreshRequest=e.WorkspaceDiagnosticRequest=e.DocumentDiagnosticRequest=e.DocumentDiagnosticReportKind=e.DiagnosticServerCancellationData=void 0;var t=Lv(),r=cW(),n=_i(),i;(function(h){function d(f){let p=f;return p&&r.boolean(p.retriggerRequest)}s(d,"is"),E(d,"is"),h.is=d})(i||(e.DiagnosticServerCancellationData=i={}));var a;(function(h){h.Full="full",h.Unchanged="unchanged"})(a||(e.DocumentDiagnosticReportKind=a={}));var o;(function(h){h.method="textDocument/diagnostic",h.messageDirection=n.MessageDirection.clientToServer,h.type=new n.ProtocolRequestType(h.method),h.partialResult=new t.ProgressType})(o||(e.DocumentDiagnosticRequest=o={}));var l;(function(h){h.method="workspace/diagnostic",h.messageDirection=n.MessageDirection.clientToServer,h.type=new n.ProtocolRequestType(h.method),h.partialResult=new t.ProgressType})(l||(e.WorkspaceDiagnosticRequest=l={}));var u;(function(h){h.method="workspace/diagnostic/refresh",h.messageDirection=n.MessageDirection.serverToClient,h.type=new n.ProtocolRequestType0(h.method)})(u||(e.DiagnosticRefreshRequest=u={}))}}),t0t=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.notebook.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DidCloseNotebookDocumentNotification=e.DidSaveNotebookDocumentNotification=e.DidChangeNotebookDocumentNotification=e.NotebookCellArrayChange=e.DidOpenNotebookDocumentNotification=e.NotebookDocumentSyncRegistrationType=e.NotebookDocument=e.NotebookCell=e.ExecutionSummary=e.NotebookCellKind=void 0;var t=(cS(),lW(e6)),r=cW(),n=_i(),i;(function(g){g.Markup=1,g.Code=2;function y(v){return v===1||v===2}s(y,"is"),E(y,"is"),g.is=y})(i||(e.NotebookCellKind=i={}));var a;(function(g){function y(b,T){let w={executionOrder:b};return(T===!0||T===!1)&&(w.success=T),w}s(y,"create"),E(y,"create"),g.create=y;function v(b){let T=b;return r.objectLiteral(T)&&t.uinteger.is(T.executionOrder)&&(T.success===void 0||r.boolean(T.success))}s(v,"is"),E(v,"is"),g.is=v;function x(b,T){return b===T?!0:b==null||T===null||T===void 0?!1:b.executionOrder===T.executionOrder&&b.success===T.success}s(x,"equals"),E(x,"equals"),g.equals=x})(a||(e.ExecutionSummary=a={}));var o;(function(g){function y(T,w){return{kind:T,document:w}}s(y,"create"),E(y,"create"),g.create=y;function v(T){let w=T;return r.objectLiteral(w)&&i.is(w.kind)&&t.DocumentUri.is(w.document)&&(w.metadata===void 0||r.objectLiteral(w.metadata))}s(v,"is"),E(v,"is"),g.is=v;function x(T,w){let C=new Set;return T.document!==w.document&&C.add("document"),T.kind!==w.kind&&C.add("kind"),T.executionSummary!==w.executionSummary&&C.add("executionSummary"),(T.metadata!==void 0||w.metadata!==void 0)&&!b(T.metadata,w.metadata)&&C.add("metadata"),(T.executionSummary!==void 0||w.executionSummary!==void 0)&&!a.equals(T.executionSummary,w.executionSummary)&&C.add("executionSummary"),C}s(x,"diff"),E(x,"diff"),g.diff=x;function b(T,w){if(T===w)return!0;if(T==null||w===null||w===void 0||typeof T!=typeof w||typeof T!="object")return!1;let C=Array.isArray(T),S=Array.isArray(w);if(C!==S)return!1;if(C&&S){if(T.length!==w.length)return!1;for(let k=0;k0}s(ct,"hasId"),E(ct,"hasId"),be.hasId=ct})(O||(e.StaticRegistrationOptions=O={}));var $;(function(be){function ct(xt){let Ke=xt;return Ke&&(Ke.documentSelector===null||_.is(Ke.documentSelector))}s(ct,"is"),E(ct,"is"),be.is=ct})($||(e.TextDocumentRegistrationOptions=$={}));var W;(function(be){function ct(Ke){let De=Ke;return n.objectLiteral(De)&&(De.workDoneProgress===void 0||n.boolean(De.workDoneProgress))}s(ct,"is"),E(ct,"is"),be.is=ct;function xt(Ke){let De=Ke;return De&&n.boolean(De.workDoneProgress)}s(xt,"hasWorkDoneProgress"),E(xt,"hasWorkDoneProgress"),be.hasWorkDoneProgress=xt})(W||(e.WorkDoneProgressOptions=W={}));var V;(function(be){be.method="initialize",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(V||(e.InitializeRequest=V={}));var z;(function(be){be.unknownProtocolVersion=1})(z||(e.InitializeErrorCodes=z={}));var H;(function(be){be.method="initialized",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolNotificationType(be.method)})(H||(e.InitializedNotification=H={}));var K;(function(be){be.method="shutdown",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType0(be.method)})(K||(e.ShutdownRequest=K={}));var J;(function(be){be.method="exit",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolNotificationType0(be.method)})(J||(e.ExitNotification=J={}));var ne;(function(be){be.method="workspace/didChangeConfiguration",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolNotificationType(be.method)})(ne||(e.DidChangeConfigurationNotification=ne={}));var Z;(function(be){be.Error=1,be.Warning=2,be.Info=3,be.Log=4,be.Debug=5})(Z||(e.MessageType=Z={}));var pe;(function(be){be.method="window/showMessage",be.messageDirection=t.MessageDirection.serverToClient,be.type=new t.ProtocolNotificationType(be.method)})(pe||(e.ShowMessageNotification=pe={}));var te;(function(be){be.method="window/showMessageRequest",be.messageDirection=t.MessageDirection.serverToClient,be.type=new t.ProtocolRequestType(be.method)})(te||(e.ShowMessageRequest=te={}));var fe;(function(be){be.method="window/logMessage",be.messageDirection=t.MessageDirection.serverToClient,be.type=new t.ProtocolNotificationType(be.method)})(fe||(e.LogMessageNotification=fe={}));var ce;(function(be){be.method="telemetry/event",be.messageDirection=t.MessageDirection.serverToClient,be.type=new t.ProtocolNotificationType(be.method)})(ce||(e.TelemetryEventNotification=ce={}));var de;(function(be){be.None=0,be.Full=1,be.Incremental=2})(de||(e.TextDocumentSyncKind=de={}));var Ee;(function(be){be.method="textDocument/didOpen",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolNotificationType(be.method)})(Ee||(e.DidOpenTextDocumentNotification=Ee={}));var we;(function(be){function ct(Ke){let De=Ke;return De!=null&&typeof De.text=="string"&&De.range!==void 0&&(De.rangeLength===void 0||typeof De.rangeLength=="number")}s(ct,"isIncremental"),E(ct,"isIncremental"),be.isIncremental=ct;function xt(Ke){let De=Ke;return De!=null&&typeof De.text=="string"&&De.range===void 0&&De.rangeLength===void 0}s(xt,"isFull"),E(xt,"isFull"),be.isFull=xt})(we||(e.TextDocumentContentChangeEvent=we={}));var Fe;(function(be){be.method="textDocument/didChange",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolNotificationType(be.method)})(Fe||(e.DidChangeTextDocumentNotification=Fe={}));var Ye;(function(be){be.method="textDocument/didClose",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolNotificationType(be.method)})(Ye||(e.DidCloseTextDocumentNotification=Ye={}));var He;(function(be){be.method="textDocument/didSave",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolNotificationType(be.method)})(He||(e.DidSaveTextDocumentNotification=He={}));var ye;(function(be){be.Manual=1,be.AfterDelay=2,be.FocusOut=3})(ye||(e.TextDocumentSaveReason=ye={}));var Be;(function(be){be.method="textDocument/willSave",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolNotificationType(be.method)})(Be||(e.WillSaveTextDocumentNotification=Be={}));var Ie;(function(be){be.method="textDocument/willSaveWaitUntil",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Ie||(e.WillSaveTextDocumentWaitUntilRequest=Ie={}));var Ne;(function(be){be.method="workspace/didChangeWatchedFiles",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolNotificationType(be.method)})(Ne||(e.DidChangeWatchedFilesNotification=Ne={}));var re;(function(be){be.Created=1,be.Changed=2,be.Deleted=3})(re||(e.FileChangeType=re={}));var ue;(function(be){function ct(xt){let Ke=xt;return n.objectLiteral(Ke)&&(r.URI.is(Ke.baseUri)||r.WorkspaceFolder.is(Ke.baseUri))&&n.string(Ke.pattern)}s(ct,"is"),E(ct,"is"),be.is=ct})(ue||(e.RelativePattern=ue={}));var le;(function(be){be.Create=1,be.Change=2,be.Delete=4})(le||(e.WatchKind=le={}));var he;(function(be){be.method="textDocument/publishDiagnostics",be.messageDirection=t.MessageDirection.serverToClient,be.type=new t.ProtocolNotificationType(be.method)})(he||(e.PublishDiagnosticsNotification=he={}));var Te;(function(be){be.Invoked=1,be.TriggerCharacter=2,be.TriggerForIncompleteCompletions=3})(Te||(e.CompletionTriggerKind=Te={}));var oe;(function(be){be.method="textDocument/completion",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(oe||(e.CompletionRequest=oe={}));var G;(function(be){be.method="completionItem/resolve",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(G||(e.CompletionResolveRequest=G={}));var X;(function(be){be.method="textDocument/hover",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(X||(e.HoverRequest=X={}));var ee;(function(be){be.Invoked=1,be.TriggerCharacter=2,be.ContentChange=3})(ee||(e.SignatureHelpTriggerKind=ee={}));var q;(function(be){be.method="textDocument/signatureHelp",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(q||(e.SignatureHelpRequest=q={}));var ie;(function(be){be.method="textDocument/definition",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(ie||(e.DefinitionRequest=ie={}));var j;(function(be){be.method="textDocument/references",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(j||(e.ReferencesRequest=j={}));var U;(function(be){be.method="textDocument/documentHighlight",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(U||(e.DocumentHighlightRequest=U={}));var Y;(function(be){be.method="textDocument/documentSymbol",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Y||(e.DocumentSymbolRequest=Y={}));var Ae;(function(be){be.method="textDocument/codeAction",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Ae||(e.CodeActionRequest=Ae={}));var me;(function(be){be.method="codeAction/resolve",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(me||(e.CodeActionResolveRequest=me={}));var Se;(function(be){be.method="workspace/symbol",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Se||(e.WorkspaceSymbolRequest=Se={}));var Ce;(function(be){be.method="workspaceSymbol/resolve",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Ce||(e.WorkspaceSymbolResolveRequest=Ce={}));var Me;(function(be){be.method="textDocument/codeLens",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Me||(e.CodeLensRequest=Me={}));var _e;(function(be){be.method="codeLens/resolve",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(_e||(e.CodeLensResolveRequest=_e={}));var ze;(function(be){be.method="workspace/codeLens/refresh",be.messageDirection=t.MessageDirection.serverToClient,be.type=new t.ProtocolRequestType0(be.method)})(ze||(e.CodeLensRefreshRequest=ze={}));var Le;(function(be){be.method="textDocument/documentLink",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Le||(e.DocumentLinkRequest=Le={}));var qe;(function(be){be.method="documentLink/resolve",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(qe||(e.DocumentLinkResolveRequest=qe={}));var ve;(function(be){be.method="textDocument/formatting",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(ve||(e.DocumentFormattingRequest=ve={}));var lt;(function(be){be.method="textDocument/rangeFormatting",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(lt||(e.DocumentRangeFormattingRequest=lt={}));var Oe;(function(be){be.method="textDocument/rangesFormatting",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Oe||(e.DocumentRangesFormattingRequest=Oe={}));var mt;(function(be){be.method="textDocument/onTypeFormatting",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(mt||(e.DocumentOnTypeFormattingRequest=mt={}));var At;(function(be){be.Identifier=1})(At||(e.PrepareSupportDefaultBehavior=At={}));var Gt;(function(be){be.method="textDocument/rename",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Gt||(e.RenameRequest=Gt={}));var Ct;(function(be){be.method="textDocument/prepareRename",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Ct||(e.PrepareRenameRequest=Ct={}));var Rt;(function(be){be.method="workspace/executeCommand",be.messageDirection=t.MessageDirection.clientToServer,be.type=new t.ProtocolRequestType(be.method)})(Rt||(e.ExecuteCommandRequest=Rt={}));var gt;(function(be){be.method="workspace/applyEdit",be.messageDirection=t.MessageDirection.serverToClient,be.type=new t.ProtocolRequestType("workspace/applyEdit")})(gt||(e.ApplyWorkspaceEditRequest=gt={}))}}),i0t=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/connection.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.createProtocolConnection=void 0;var t=Lv();function r(n,i,a,o){return t.ConnectionStrategy.is(o)&&(o={connectionStrategy:o}),(0,t.createMessageConnection)(n,i,a,o)}s(r,"createProtocolConnection"),E(r,"createProtocolConnection"),e.createProtocolConnection=r}}),a0t=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/api.js"(e){"use strict";var t=e&&e.__createBinding||(Object.create?(function(a,o,l,u){u===void 0&&(u=l);var h=Object.getOwnPropertyDescriptor(o,l);(!h||("get"in h?!o.__esModule:h.writable||h.configurable))&&(h={enumerable:!0,get:E(function(){return o[l]},"get")}),Object.defineProperty(a,u,h)}):(function(a,o,l,u){u===void 0&&(u=l),a[u]=o[l]})),r=e&&e.__exportStar||function(a,o){for(var l in a)l!=="default"&&!Object.prototype.hasOwnProperty.call(o,l)&&t(o,a,l)};Object.defineProperty(e,"__esModule",{value:!0}),e.LSPErrorCodes=e.createProtocolConnection=void 0,r(Lv(),e),r((cS(),lW(e6)),e),r(_i(),e),r(n0t(),e);var n=i0t();Object.defineProperty(e,"createProtocolConnection",{enumerable:!0,get:E(function(){return n.createProtocolConnection},"get")});var i;(function(a){a.lspReservedErrorRangeStart=-32899,a.RequestFailed=-32803,a.ServerCancelled=-32802,a.ContentModified=-32801,a.RequestCancelled=-32800,a.lspReservedErrorRangeEnd=-32800})(i||(e.LSPErrorCodes=i={}))}}),s0t=Hr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/browser/main.js"(e){"use strict";var t=e&&e.__createBinding||(Object.create?(function(a,o,l,u){u===void 0&&(u=l);var h=Object.getOwnPropertyDescriptor(o,l);(!h||("get"in h?!o.__esModule:h.writable||h.configurable))&&(h={enumerable:!0,get:E(function(){return o[l]},"get")}),Object.defineProperty(a,u,h)}):(function(a,o,l,u){u===void 0&&(u=l),a[u]=o[l]})),r=e&&e.__exportStar||function(a,o){for(var l in a)l!=="default"&&!Object.prototype.hasOwnProperty.call(o,l)&&t(o,a,l)};Object.defineProperty(e,"__esModule",{value:!0}),e.createProtocolConnection=void 0;var n=LCe();r(LCe(),e),r(a0t(),e);function i(a,o,l,u){return(0,n.createMessageConnection)(a,o,l,u)}s(i,"createProtocolConnection"),E(i,"createProtocolConnection"),e.createProtocolConnection=i}}),_we={};xp(_we,{AbstractAstReflection:s(()=>dW,"AbstractAstReflection"),AbstractCstNode:s(()=>pH,"AbstractCstNode"),AbstractLangiumParser:s(()=>gH,"AbstractLangiumParser"),AbstractParserErrorMessageProvider:s(()=>yLe,"AbstractParserErrorMessageProvider"),AbstractThreadedAsyncParser:s(()=>Pkt,"AbstractThreadedAsyncParser"),AstUtils:s(()=>fW,"AstUtils"),BiMap:s(()=>jR,"BiMap"),Cancellation:s(()=>ri,"Cancellation"),CompositeCstNodeImpl:s(()=>Z6,"CompositeCstNodeImpl"),ContextCache:s(()=>i_,"ContextCache"),CstNodeBuilder:s(()=>fLe,"CstNodeBuilder"),CstUtils:s(()=>uW,"CstUtils"),DEFAULT_TOKENIZE_OPTIONS:s(()=>NH,"DEFAULT_TOKENIZE_OPTIONS"),DONE_RESULT:s(()=>Us,"DONE_RESULT"),DatatypeSymbol:s(()=>qR,"DatatypeSymbol"),DefaultAstNodeDescriptionProvider:s(()=>YLe,"DefaultAstNodeDescriptionProvider"),DefaultAstNodeLocator:s(()=>XLe,"DefaultAstNodeLocator"),DefaultAsyncParser:s(()=>fDe,"DefaultAsyncParser"),DefaultCommentProvider:s(()=>dDe,"DefaultCommentProvider"),DefaultConfigurationProvider:s(()=>KLe,"DefaultConfigurationProvider"),DefaultDocumentBuilder:s(()=>ZLe,"DefaultDocumentBuilder"),DefaultDocumentValidator:s(()=>ULe,"DefaultDocumentValidator"),DefaultHydrator:s(()=>mDe,"DefaultHydrator"),DefaultIndexManager:s(()=>QLe,"DefaultIndexManager"),DefaultJsonSerializer:s(()=>VLe,"DefaultJsonSerializer"),DefaultLangiumDocumentFactory:s(()=>MLe,"DefaultLangiumDocumentFactory"),DefaultLangiumDocuments:s(()=>NLe,"DefaultLangiumDocuments"),DefaultLangiumProfiler:s(()=>Gkt,"DefaultLangiumProfiler"),DefaultLexer:s(()=>PH,"DefaultLexer"),DefaultLexerErrorMessageProvider:s(()=>eDe,"DefaultLexerErrorMessageProvider"),DefaultLinker:s(()=>PLe,"DefaultLinker"),DefaultNameProvider:s(()=>OLe,"DefaultNameProvider"),DefaultReferenceDescriptionProvider:s(()=>jLe,"DefaultReferenceDescriptionProvider"),DefaultReferences:s(()=>BLe,"DefaultReferences"),DefaultScopeComputation:s(()=>$Le,"DefaultScopeComputation"),DefaultScopeProvider:s(()=>zLe,"DefaultScopeProvider"),DefaultServiceRegistry:s(()=>WLe,"DefaultServiceRegistry"),DefaultTokenBuilder:s(()=>e_,"DefaultTokenBuilder"),DefaultValueConverter:s(()=>SH,"DefaultValueConverter"),DefaultWorkspaceLock:s(()=>pDe,"DefaultWorkspaceLock"),DefaultWorkspaceManager:s(()=>JLe,"DefaultWorkspaceManager"),Deferred:s(()=>td,"Deferred"),Disposable:s(()=>Yg,"Disposable"),DisposableCache:s(()=>n_,"DisposableCache"),DocumentCache:s(()=>GLe,"DocumentCache"),DocumentState:s(()=>nn,"DocumentState"),DocumentValidator:s(()=>Il,"DocumentValidator"),EMPTY_SCOPE:s(()=>Dkt,"EMPTY_SCOPE"),EMPTY_STREAM:s(()=>vv,"EMPTY_STREAM"),EmptyFileSystem:s(()=>Cn,"EmptyFileSystem"),EmptyFileSystemProvider:s(()=>vDe,"EmptyFileSystemProvider"),ErrorWithLocation:s(()=>u6,"ErrorWithLocation"),GrammarAST:s(()=>Iwe,"GrammarAST"),GrammarUtils:s(()=>WW,"GrammarUtils"),IndentationAwareLexer:s(()=>Bkt,"IndentationAwareLexer"),IndentationAwareTokenBuilder:s(()=>yDe,"IndentationAwareTokenBuilder"),JSDocDocumentationProvider:s(()=>hDe,"JSDocDocumentationProvider"),LangiumCompletionParser:s(()=>vLe,"LangiumCompletionParser"),LangiumParser:s(()=>gLe,"LangiumParser"),LangiumParserErrorMessageProvider:s(()=>yH,"LangiumParserErrorMessageProvider"),LeafCstNodeImpl:s(()=>WR,"LeafCstNodeImpl"),LexingMode:s(()=>Hg,"LexingMode"),MapScope:s(()=>Lkt,"MapScope"),Module:s(()=>cV,"Module"),MultiMap:s(()=>rd,"MultiMap"),MultiMapScope:s(()=>FLe,"MultiMapScope"),OperationCancelled:s(()=>Bu,"OperationCancelled"),ParserWorker:s(()=>Okt,"ParserWorker"),ProfilingTask:s(()=>bDe,"ProfilingTask"),Reduction:s(()=>UC,"Reduction"),RefResolving:s(()=>gg,"RefResolving"),RegExpUtils:s(()=>HW,"RegExpUtils"),RootCstNodeImpl:s(()=>mH,"RootCstNodeImpl"),SimpleCache:s(()=>_H,"SimpleCache"),StreamImpl:s(()=>Ou,"StreamImpl"),StreamScope:s(()=>aV,"StreamScope"),TextDocument:s(()=>UR,"TextDocument"),TreeStreamImpl:s(()=>xv,"TreeStreamImpl"),URI:s(()=>Jo,"URI"),UriTrie:s(()=>AH,"UriTrie"),UriUtils:s(()=>js,"UriUtils"),VALIDATE_EACH_NODE:s(()=>HLe,"VALIDATE_EACH_NODE"),ValidationCategory:s(()=>XR,"ValidationCategory"),ValidationRegistry:s(()=>qLe,"ValidationRegistry"),ValueConverter:s(()=>Nu,"ValueConverter"),WorkspaceCache:s(()=>LH,"WorkspaceCache"),assertCondition:s(()=>qW,"assertCondition"),assertUnreachable:s(()=>bp,"assertUnreachable"),createCompletionParser:s(()=>bH,"createCompletionParser"),createDefaultCoreModule:s(()=>yn,"createDefaultCoreModule"),createDefaultSharedCoreModule:s(()=>vn,"createDefaultSharedCoreModule"),createGrammarConfig:s(()=>cq,"createGrammarConfig"),createLangiumParser:s(()=>TH,"createLangiumParser"),createParser:s(()=>Q6,"createParser"),delayNextTick:s(()=>t_,"delayNextTick"),diagnosticData:s(()=>qg,"diagnosticData"),eagerLoad:s(()=>VH,"eagerLoad"),getDiagnosticRange:s(()=>IH,"getDiagnosticRange"),indentationBuilderDefaultOptions:s(()=>hV,"indentationBuilderDefaultOptions"),inject:s(()=>$r,"inject"),interruptAndCheck:s(()=>qa,"interruptAndCheck"),isAstNode:s(()=>sa,"isAstNode"),isAstNodeDescription:s(()=>hW,"isAstNodeDescription"),isAstNodeWithComment:s(()=>DH,"isAstNodeWithComment"),isCompositeCstNode:s(()=>Yh,"isCompositeCstNode"),isIMultiModeLexerDefinition:s(()=>o_,"isIMultiModeLexerDefinition"),isJSDoc:s(()=>BH,"isJSDoc"),isLeafCstNode:s(()=>Jg,"isLeafCstNode"),isLinkingError:s(()=>Tg,"isLinkingError"),isMultiReference:s(()=>$u,"isMultiReference"),isNamed:s(()=>RH,"isNamed"),isOperationCancelled:s(()=>v0,"isOperationCancelled"),isReference:s(()=>Ys,"isReference"),isRootCstNode:s(()=>r6,"isRootCstNode"),isTokenTypeArray:s(()=>s_,"isTokenTypeArray"),isTokenTypeDictionary:s(()=>KR,"isTokenTypeDictionary"),loadGrammarFromJson:s(()=>Ha,"loadGrammarFromJson"),parseJSDoc:s(()=>OH,"parseJSDoc"),prepareLangiumParser:s(()=>CH,"prepareLangiumParser"),setInterruptionPeriod:s(()=>wH,"setInterruptionPeriod"),startCancelableOperation:s(()=>r_,"startCancelableOperation"),stream:s(()=>zn,"stream"),toDiagnosticData:s(()=>MH,"toDiagnosticData"),toDiagnosticSeverity:s(()=>zC,"toDiagnosticSeverity")});uW={};xp(uW,{DefaultNameRegexp:s(()=>$W,"DefaultNameRegexp"),RangeComparison:s(()=>Pu,"RangeComparison"),compareRange:s(()=>OW,"compareRange"),findCommentNode:s(()=>FW,"findCommentNode"),findDeclarationNodeAtOffset:s(()=>Zwe,"findDeclarationNodeAtOffset"),findLeafNodeAtOffset:s(()=>c6,"findLeafNodeAtOffset"),findLeafNodeBeforeOffset:s(()=>GW,"findLeafNodeBeforeOffset"),flattenCst:s(()=>Kwe,"flattenCst"),getDatatypeNode:s(()=>Xwe,"getDatatypeNode"),getInteriorNodes:s(()=>eke,"getInteriorNodes"),getNextNode:s(()=>Qwe,"getNextNode"),getPreviousNode:s(()=>VW,"getPreviousNode"),getStartlineNode:s(()=>Jwe,"getStartlineNode"),inRange:s(()=>BW,"inRange"),isChildNode:s(()=>PW,"isChildNode"),isCommentNode:s(()=>AR,"isCommentNode"),streamCst:s(()=>Sv,"streamCst"),toDocumentSegment:s(()=>wv,"toDocumentSegment"),tokenToRange:s(()=>YC,"tokenToRange")});s(sa,"isAstNode");E(sa,"isAstNode");s(Ys,"isReference");E(Ys,"isReference");s($u,"isMultiReference");E($u,"isMultiReference");s(hW,"isAstNodeDescription");E(hW,"isAstNodeDescription");s(Tg,"isLinkingError");E(Tg,"isLinkingError");dW=class{static{s(this,"AbstractAstReflection")}static{E(this,"AbstractAstReflection")}constructor(){this.subtypes={},this.allSubtypes={}}getAllTypes(){return Object.keys(this.types)}getReferenceType(e){let t=this.types[e.container.$type];if(!t)throw new Error(`Type ${e.container.$type||"undefined"} not found.`);let r=t.properties[e.property]?.referenceType;if(!r)throw new Error(`Property ${e.property||"undefined"} of type ${e.container.$type} is not a reference.`);return r}getTypeMetaData(e){let t=this.types[e];return t||{name:e,properties:{},superTypes:[]}}isInstance(e,t){return sa(e)&&this.isSubtype(e.$type,t)}isSubtype(e,t){if(e===t)return!0;let r=this.subtypes[e];r||(r=this.subtypes[e]={});let n=r[t];if(n!==void 0)return n;{let i=this.types[e],a=i?i.superTypes.some(o=>this.isSubtype(o,t)):!1;return r[t]=a,a}}getAllSubTypes(e){let t=this.allSubtypes[e];if(t)return t;{let r=this.getAllTypes(),n=[];for(let i of r)this.isSubtype(i,e)&&n.push(i);return this.allSubtypes[e]=n,n}}};s(Yh,"isCompositeCstNode");E(Yh,"isCompositeCstNode");s(Jg,"isLeafCstNode");E(Jg,"isLeafCstNode");s(r6,"isRootCstNode");E(r6,"isRootCstNode");Ou=class Vh{static{s(this,"_StreamImpl")}static{E(this,"StreamImpl")}constructor(t,r){this.startFn=t,this.nextFn=r}iterator(){let t={state:this.startFn(),next:E(()=>this.nextFn(t.state),"next"),[Symbol.iterator]:()=>t};return t}[Symbol.iterator](){return this.iterator()}isEmpty(){return!!this.iterator().next().done}count(){let t=this.iterator(),r=0,n=t.next();for(;!n.done;)r++,n=t.next();return r}toArray(){let t=[],r=this.iterator(),n;do n=r.next(),n.value!==void 0&&t.push(n.value);while(!n.done);return t}toSet(){return new Set(this)}toMap(t,r){let n=this.map(i=>[t?t(i):i,r?r(i):i]);return new Map(n)}toString(){return this.join()}concat(t){return new Vh(()=>({first:this.startFn(),firstDone:!1,iterator:t[Symbol.iterator]()}),r=>{let n;if(!r.firstDone){do if(n=this.nextFn(r.first),!n.done)return n;while(!n.done);r.firstDone=!0}do if(n=r.iterator.next(),!n.done)return n;while(!n.done);return Us})}join(t=","){let r=this.iterator(),n="",i,a=!1;do i=r.next(),i.done||(a&&(n+=t),n+=Lwe(i.value)),a=!0;while(!i.done);return n}indexOf(t,r=0){let n=this.iterator(),i=0,a=n.next();for(;!a.done;){if(i>=r&&a.value===t)return i;a=n.next(),i++}return-1}every(t){let r=this.iterator(),n=r.next();for(;!n.done;){if(!t(n.value))return!1;n=r.next()}return!0}some(t){let r=this.iterator(),n=r.next();for(;!n.done;){if(t(n.value))return!0;n=r.next()}return!1}forEach(t){let r=this.iterator(),n=0,i=r.next();for(;!i.done;)t(i.value,n),i=r.next(),n++}map(t){return new Vh(this.startFn,r=>{let{done:n,value:i}=this.nextFn(r);return n?Us:{done:!1,value:t(i)}})}filter(t){return new Vh(this.startFn,r=>{let n;do if(n=this.nextFn(r),!n.done&&t(n.value))return n;while(!n.done);return Us})}nonNullable(){return this.filter(t=>t!=null)}reduce(t,r){let n=this.iterator(),i=r,a=n.next();for(;!a.done;)i===void 0?i=a.value:i=t(i,a.value),a=n.next();return i}reduceRight(t,r){return this.recursiveReduce(this.iterator(),t,r)}recursiveReduce(t,r,n){let i=t.next();if(i.done)return n;let a=this.recursiveReduce(t,r,n);return a===void 0?i.value:r(a,i.value)}find(t){let r=this.iterator(),n=r.next();for(;!n.done;){if(t(n.value))return n.value;n=r.next()}}findIndex(t){let r=this.iterator(),n=0,i=r.next();for(;!i.done;){if(t(i.value))return n;i=r.next(),n++}return-1}includes(t){let r=this.iterator(),n=r.next();for(;!n.done;){if(n.value===t)return!0;n=r.next()}return!1}flatMap(t){return new Vh(()=>({this:this.startFn()}),r=>{do{if(r.iterator){let a=r.iterator.next();if(a.done)r.iterator=void 0;else return a}let{done:n,value:i}=this.nextFn(r.this);if(!n){let a=t(i);if(HC(a))r.iterator=a[Symbol.iterator]();else return{done:!1,value:a}}}while(r.iterator);return Us})}flat(t){if(t===void 0&&(t=1),t<=0)return this;let r=t>1?this.flat(t-1):this;return new Vh(()=>({this:r.startFn()}),n=>{do{if(n.iterator){let o=n.iterator.next();if(o.done)n.iterator=void 0;else return o}let{done:i,value:a}=r.nextFn(n.this);if(!i)if(HC(a))n.iterator=a[Symbol.iterator]();else return{done:!1,value:a}}while(n.iterator);return Us})}head(){let r=this.iterator().next();if(!r.done)return r.value}tail(t=1){return new Vh(()=>{let r=this.startFn();for(let n=0;n({size:0,state:this.startFn()}),r=>(r.size++,r.size>t?Us:this.nextFn(r.state)))}distinct(t){return new Vh(()=>({set:new Set,internalState:this.startFn()}),r=>{let n;do if(n=this.nextFn(r.internalState),!n.done){let i=t?t(n.value):n.value;if(!r.set.has(i))return r.set.add(i),n}while(!n.done);return Us})}exclude(t,r){let n=new Set;for(let i of t){let a=r?r(i):i;n.add(a)}return this.filter(i=>{let a=r?r(i):i;return!n.has(a)})}};s(Lwe,"toString");E(Lwe,"toString");s(HC,"isIterable");E(HC,"isIterable");vv=new Ou(()=>{},()=>Us),Us=Object.freeze({done:!0,value:void 0});s(zn,"stream");E(zn,"stream");xv=class extends Ou{static{s(this,"TreeStreamImpl")}static{E(this,"TreeStreamImpl")}constructor(e,t,r){super(()=>({iterators:r?.includeRoot?[[e][Symbol.iterator]()]:[t(e)[Symbol.iterator]()],pruned:!1}),n=>{for(n.pruned&&(n.iterators.pop(),n.pruned=!1);n.iterators.length>0;){let a=n.iterators[n.iterators.length-1].next();if(a.done)n.iterators.pop();else return n.iterators.push(t(a.value)[Symbol.iterator]()),a}return Us})}iterator(){let e={state:this.startFn(),next:E(()=>this.nextFn(e.state),"next"),prune:E(()=>{e.state.pruned=!0},"prune"),[Symbol.iterator]:()=>e};return e}};(function(e){function t(a){return a.reduce((o,l)=>o+l,0)}s(t,"sum"),E(t,"sum"),e.sum=t;function r(a){return a.reduce((o,l)=>o*l,0)}s(r,"product"),E(r,"product"),e.product=r;function n(a){return a.reduce((o,l)=>Math.min(o,l))}s(n,"min2"),E(n,"min"),e.min=n;function i(a){return a.reduce((o,l)=>Math.max(o,l))}s(i,"max"),E(i,"max"),e.max=i})(UC||(UC={}));fW={};xp(fW,{assignMandatoryProperties:s(()=>pW,"assignMandatoryProperties"),copyAstNode:s(()=>fR,"copyAstNode"),findRootNode:s(()=>pv,"findRootNode"),getContainerOfType:s(()=>e0,"getContainerOfType"),getDocument:s(()=>Cc,"getDocument"),getReferenceNodes:s(()=>hR,"getReferenceNodes"),hasContainerOfType:s(()=>Dwe,"hasContainerOfType"),linkContentToContainer:s(()=>bv,"linkContentToContainer"),streamAllContents:s(()=>nd,"streamAllContents"),streamAst:s(()=>Sc,"streamAst"),streamContents:s(()=>hS,"streamContents"),streamReferences:s(()=>Tv,"streamReferences")});s(bv,"linkContentToContainer");E(bv,"linkContentToContainer");s(e0,"getContainerOfType");E(e0,"getContainerOfType");s(Dwe,"hasContainerOfType");E(Dwe,"hasContainerOfType");s(Cc,"getDocument");E(Cc,"getDocument");s(pv,"findRootNode");E(pv,"findRootNode");s(hR,"getReferenceNodes");E(hR,"getReferenceNodes");s(hS,"streamContents");E(hS,"streamContents");s(nd,"streamAllContents");E(nd,"streamAllContents");s(Sc,"streamAst");E(Sc,"streamAst");s(dR,"isAstNodeInRange");E(dR,"isAstNodeInRange");s(Tv,"streamReferences");E(Tv,"streamReferences");s(pW,"assignMandatoryProperties");E(pW,"assignMandatoryProperties");s(mW,"copyDefaultValue");E(mW,"copyDefaultValue");s(fR,"copyAstNode");E(fR,"copyAstNode");Iwe={};xp(Iwe,{AbstractElement:s(()=>So,"AbstractElement"),AbstractParserRule:s(()=>AC,"AbstractParserRule"),AbstractRule:s(()=>iv,"AbstractRule"),AbstractType:s(()=>Qo,"AbstractType"),Action:s(()=>sp,"Action"),Alternatives:s(()=>RC,"Alternatives"),ArrayLiteral:s(()=>pR,"ArrayLiteral"),ArrayType:s(()=>mR,"ArrayType"),Assignment:s(()=>op,"Assignment"),BooleanLiteral:s(()=>gR,"BooleanLiteral"),CharacterRange:s(()=>lp,"CharacterRange"),Condition:s(()=>cp,"Condition"),Conjunction:s(()=>_C,"Conjunction"),CrossReference:s(()=>up,"CrossReference"),Disjunction:s(()=>LC,"Disjunction"),EndOfFile:s(()=>yR,"EndOfFile"),Grammar:s(()=>qh,"Grammar"),GrammarImport:s(()=>vR,"GrammarImport"),Group:s(()=>Cg,"Group"),InferredType:s(()=>xR,"InferredType"),InfixRule:s(()=>Mu,"InfixRule"),InfixRuleOperatorList:s(()=>DC,"InfixRuleOperatorList"),InfixRuleOperators:s(()=>bR,"InfixRuleOperators"),Interface:s(()=>av,"Interface"),Keyword:s(()=>sv,"Keyword"),LangiumGrammarAstReflection:s(()=>NW,"LangiumGrammarAstReflection"),LangiumGrammarTerminals:s(()=>o0t,"LangiumGrammarTerminals"),NamedArgument:s(()=>ov,"NamedArgument"),NegatedToken:s(()=>Sg,"NegatedToken"),Negation:s(()=>TR,"Negation"),NumberLiteral:s(()=>CR,"NumberLiteral"),Parameter:s(()=>lv,"Parameter"),ParameterReference:s(()=>SR,"ParameterReference"),ParserRule:s(()=>vc,"ParserRule"),ReferenceType:s(()=>IC,"ReferenceType"),RegexToken:s(()=>wg,"RegexToken"),ReturnType:s(()=>wR,"ReturnType"),RuleCall:s(()=>kg,"RuleCall"),SimpleType:s(()=>cv,"SimpleType"),StringLiteral:s(()=>kR,"StringLiteral"),TerminalAlternatives:s(()=>Eg,"TerminalAlternatives"),TerminalElement:s(()=>wo,"TerminalElement"),TerminalGroup:s(()=>Ag,"TerminalGroup"),TerminalRule:s(()=>Hh,"TerminalRule"),TerminalRuleCall:s(()=>Rg,"TerminalRuleCall"),Type:s(()=>MC,"Type"),TypeAttribute:s(()=>_g,"TypeAttribute"),TypeDefinition:s(()=>Lg,"TypeDefinition"),UnionType:s(()=>ER,"UnionType"),UnorderedGroup:s(()=>NC,"UnorderedGroup"),UntilToken:s(()=>Dg,"UntilToken"),ValueLiteral:s(()=>Ig,"ValueLiteral"),Wildcard:s(()=>uv,"Wildcard"),isAbstractElement:s(()=>n6,"isAbstractElement"),isAbstractParserRule:s(()=>t0,"isAbstractParserRule"),isAbstractRule:s(()=>Mwe,"isAbstractRule"),isAbstractType:s(()=>Nwe,"isAbstractType"),isAction:s(()=>pp,"isAction"),isAlternatives:s(()=>i6,"isAlternatives"),isArrayLiteral:s(()=>Pwe,"isArrayLiteral"),isArrayType:s(()=>gW,"isArrayType"),isAssignment:s(()=>jh,"isAssignment"),isBooleanLiteral:s(()=>yW,"isBooleanLiteral"),isCharacterRange:s(()=>vW,"isCharacterRange"),isCondition:s(()=>Owe,"isCondition"),isConjunction:s(()=>xW,"isConjunction"),isCrossReference:s(()=>r0,"isCrossReference"),isDisjunction:s(()=>bW,"isDisjunction"),isEndOfFile:s(()=>TW,"isEndOfFile"),isGrammar:s(()=>Bwe,"isGrammar"),isGrammarImport:s(()=>$we,"isGrammarImport"),isGroup:s(()=>n0,"isGroup"),isInferredType:s(()=>dS,"isInferredType"),isInfixRule:s(()=>Cv,"isInfixRule"),isInfixRuleOperatorList:s(()=>Fwe,"isInfixRuleOperatorList"),isInfixRuleOperators:s(()=>Gwe,"isInfixRuleOperators"),isInterface:s(()=>CW,"isInterface"),isKeyword:s(()=>Xh,"isKeyword"),isNamedArgument:s(()=>zwe,"isNamedArgument"),isNegatedToken:s(()=>SW,"isNegatedToken"),isNegation:s(()=>wW,"isNegation"),isNumberLiteral:s(()=>Vwe,"isNumberLiteral"),isParameter:s(()=>Wwe,"isParameter"),isParameterReference:s(()=>kW,"isParameterReference"),isParserRule:s(()=>Ks,"isParserRule"),isReferenceType:s(()=>EW,"isReferenceType"),isRegexToken:s(()=>AW,"isRegexToken"),isReturnType:s(()=>RW,"isReturnType"),isRuleCall:s(()=>Kh,"isRuleCall"),isSimpleType:s(()=>a6,"isSimpleType"),isStringLiteral:s(()=>qwe,"isStringLiteral"),isTerminalAlternatives:s(()=>_W,"isTerminalAlternatives"),isTerminalElement:s(()=>Hwe,"isTerminalElement"),isTerminalGroup:s(()=>LW,"isTerminalGroup"),isTerminalRule:s(()=>Ol,"isTerminalRule"),isTerminalRuleCall:s(()=>s6,"isTerminalRuleCall"),isType:s(()=>o6,"isType"),isTypeAttribute:s(()=>Uwe,"isTypeAttribute"),isTypeDefinition:s(()=>Ywe,"isTypeDefinition"),isUnionType:s(()=>DW,"isUnionType"),isUnorderedGroup:s(()=>l6,"isUnorderedGroup"),isUntilToken:s(()=>IW,"isUntilToken"),isValueLiteral:s(()=>jwe,"isValueLiteral"),isWildcard:s(()=>MW,"isWildcard"),reflection:s(()=>Er,"reflection")});o0t={ID:/\^?[_a-zA-Z][\w_]*/,STRING:/"(\\.|[^"\\])*"|'(\\.|[^'\\])*'/,NUMBER:/NaN|-?((\d*\.\d+|\d+)([Ee][+-]?\d+)?|Infinity)/,RegexLiteral:/\/(?![*+?])(?:[^\r\n\[/\\]|\\.|\[(?:[^\r\n\]\\]|\\.)*\])+\/[a-z]*/,WS:/\s+/,ML_COMMENT:/\/\*[\s\S]*?\*\//,SL_COMMENT:/\/\/[^\n\r]*/},So={$type:"AbstractElement",cardinality:"cardinality"};s(n6,"isAbstractElement");E(n6,"isAbstractElement");AC={$type:"AbstractParserRule"};s(t0,"isAbstractParserRule");E(t0,"isAbstractParserRule");iv={$type:"AbstractRule"};s(Mwe,"isAbstractRule");E(Mwe,"isAbstractRule");Qo={$type:"AbstractType"};s(Nwe,"isAbstractType");E(Nwe,"isAbstractType");sp={$type:"Action",cardinality:"cardinality",feature:"feature",inferredType:"inferredType",operator:"operator",type:"type"};s(pp,"isAction");E(pp,"isAction");RC={$type:"Alternatives",cardinality:"cardinality",elements:"elements"};s(i6,"isAlternatives");E(i6,"isAlternatives");pR={$type:"ArrayLiteral",elements:"elements"};s(Pwe,"isArrayLiteral");E(Pwe,"isArrayLiteral");mR={$type:"ArrayType",elementType:"elementType"};s(gW,"isArrayType");E(gW,"isArrayType");op={$type:"Assignment",cardinality:"cardinality",feature:"feature",operator:"operator",predicate:"predicate",terminal:"terminal"};s(jh,"isAssignment");E(jh,"isAssignment");gR={$type:"BooleanLiteral",true:"true"};s(yW,"isBooleanLiteral");E(yW,"isBooleanLiteral");lp={$type:"CharacterRange",cardinality:"cardinality",left:"left",lookahead:"lookahead",parenthesized:"parenthesized",right:"right"};s(vW,"isCharacterRange");E(vW,"isCharacterRange");cp={$type:"Condition"};s(Owe,"isCondition");E(Owe,"isCondition");_C={$type:"Conjunction",left:"left",right:"right"};s(xW,"isConjunction");E(xW,"isConjunction");up={$type:"CrossReference",cardinality:"cardinality",deprecatedSyntax:"deprecatedSyntax",isMulti:"isMulti",terminal:"terminal",type:"type"};s(r0,"isCrossReference");E(r0,"isCrossReference");LC={$type:"Disjunction",left:"left",right:"right"};s(bW,"isDisjunction");E(bW,"isDisjunction");yR={$type:"EndOfFile",cardinality:"cardinality"};s(TW,"isEndOfFile");E(TW,"isEndOfFile");qh={$type:"Grammar",imports:"imports",interfaces:"interfaces",isDeclared:"isDeclared",name:"name",rules:"rules",types:"types"};s(Bwe,"isGrammar");E(Bwe,"isGrammar");vR={$type:"GrammarImport",path:"path"};s($we,"isGrammarImport");E($we,"isGrammarImport");Cg={$type:"Group",cardinality:"cardinality",elements:"elements",guardCondition:"guardCondition",predicate:"predicate"};s(n0,"isGroup");E(n0,"isGroup");xR={$type:"InferredType",name:"name"};s(dS,"isInferredType");E(dS,"isInferredType");Mu={$type:"InfixRule",call:"call",dataType:"dataType",inferredType:"inferredType",name:"name",operators:"operators",parameters:"parameters",returnType:"returnType"};s(Cv,"isInfixRule");E(Cv,"isInfixRule");DC={$type:"InfixRuleOperatorList",associativity:"associativity",operators:"operators"};s(Fwe,"isInfixRuleOperatorList");E(Fwe,"isInfixRuleOperatorList");bR={$type:"InfixRuleOperators",precedences:"precedences"};s(Gwe,"isInfixRuleOperators");E(Gwe,"isInfixRuleOperators");av={$type:"Interface",attributes:"attributes",name:"name",superTypes:"superTypes"};s(CW,"isInterface");E(CW,"isInterface");sv={$type:"Keyword",cardinality:"cardinality",predicate:"predicate",value:"value"};s(Xh,"isKeyword");E(Xh,"isKeyword");ov={$type:"NamedArgument",calledByName:"calledByName",parameter:"parameter",value:"value"};s(zwe,"isNamedArgument");E(zwe,"isNamedArgument");Sg={$type:"NegatedToken",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized",terminal:"terminal"};s(SW,"isNegatedToken");E(SW,"isNegatedToken");TR={$type:"Negation",value:"value"};s(wW,"isNegation");E(wW,"isNegation");CR={$type:"NumberLiteral",value:"value"};s(Vwe,"isNumberLiteral");E(Vwe,"isNumberLiteral");lv={$type:"Parameter",name:"name"};s(Wwe,"isParameter");E(Wwe,"isParameter");SR={$type:"ParameterReference",parameter:"parameter"};s(kW,"isParameterReference");E(kW,"isParameterReference");vc={$type:"ParserRule",dataType:"dataType",definition:"definition",entry:"entry",fragment:"fragment",inferredType:"inferredType",name:"name",parameters:"parameters",returnType:"returnType"};s(Ks,"isParserRule");E(Ks,"isParserRule");IC={$type:"ReferenceType",isMulti:"isMulti",referenceType:"referenceType"};s(EW,"isReferenceType");E(EW,"isReferenceType");wg={$type:"RegexToken",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized",regex:"regex"};s(AW,"isRegexToken");E(AW,"isRegexToken");wR={$type:"ReturnType",name:"name"};s(RW,"isReturnType");E(RW,"isReturnType");kg={$type:"RuleCall",arguments:"arguments",cardinality:"cardinality",predicate:"predicate",rule:"rule"};s(Kh,"isRuleCall");E(Kh,"isRuleCall");cv={$type:"SimpleType",primitiveType:"primitiveType",stringType:"stringType",typeRef:"typeRef"};s(a6,"isSimpleType");E(a6,"isSimpleType");kR={$type:"StringLiteral",value:"value"};s(qwe,"isStringLiteral");E(qwe,"isStringLiteral");Eg={$type:"TerminalAlternatives",cardinality:"cardinality",elements:"elements",lookahead:"lookahead",parenthesized:"parenthesized"};s(_W,"isTerminalAlternatives");E(_W,"isTerminalAlternatives");wo={$type:"TerminalElement",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized"};s(Hwe,"isTerminalElement");E(Hwe,"isTerminalElement");Ag={$type:"TerminalGroup",cardinality:"cardinality",elements:"elements",lookahead:"lookahead",parenthesized:"parenthesized"};s(LW,"isTerminalGroup");E(LW,"isTerminalGroup");Hh={$type:"TerminalRule",definition:"definition",fragment:"fragment",hidden:"hidden",name:"name",type:"type"};s(Ol,"isTerminalRule");E(Ol,"isTerminalRule");Rg={$type:"TerminalRuleCall",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized",rule:"rule"};s(s6,"isTerminalRuleCall");E(s6,"isTerminalRuleCall");MC={$type:"Type",name:"name",type:"type"};s(o6,"isType");E(o6,"isType");_g={$type:"TypeAttribute",defaultValue:"defaultValue",isOptional:"isOptional",name:"name",type:"type"};s(Uwe,"isTypeAttribute");E(Uwe,"isTypeAttribute");Lg={$type:"TypeDefinition"};s(Ywe,"isTypeDefinition");E(Ywe,"isTypeDefinition");ER={$type:"UnionType",types:"types"};s(DW,"isUnionType");E(DW,"isUnionType");NC={$type:"UnorderedGroup",cardinality:"cardinality",elements:"elements"};s(l6,"isUnorderedGroup");E(l6,"isUnorderedGroup");Dg={$type:"UntilToken",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized",terminal:"terminal"};s(IW,"isUntilToken");E(IW,"isUntilToken");Ig={$type:"ValueLiteral"};s(jwe,"isValueLiteral");E(jwe,"isValueLiteral");uv={$type:"Wildcard",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized"};s(MW,"isWildcard");E(MW,"isWildcard");NW=class extends dW{static{s(this,"LangiumGrammarAstReflection")}static{E(this,"LangiumGrammarAstReflection")}constructor(){super(...arguments),this.types={AbstractElement:{name:So.$type,properties:{cardinality:{name:So.cardinality}},superTypes:[]},AbstractParserRule:{name:AC.$type,properties:{},superTypes:[iv.$type,Qo.$type]},AbstractRule:{name:iv.$type,properties:{},superTypes:[]},AbstractType:{name:Qo.$type,properties:{},superTypes:[]},Action:{name:sp.$type,properties:{cardinality:{name:sp.cardinality},feature:{name:sp.feature},inferredType:{name:sp.inferredType},operator:{name:sp.operator},type:{name:sp.type,referenceType:Qo.$type}},superTypes:[So.$type]},Alternatives:{name:RC.$type,properties:{cardinality:{name:RC.cardinality},elements:{name:RC.elements,defaultValue:[]}},superTypes:[So.$type]},ArrayLiteral:{name:pR.$type,properties:{elements:{name:pR.elements,defaultValue:[]}},superTypes:[Ig.$type]},ArrayType:{name:mR.$type,properties:{elementType:{name:mR.elementType}},superTypes:[Lg.$type]},Assignment:{name:op.$type,properties:{cardinality:{name:op.cardinality},feature:{name:op.feature},operator:{name:op.operator},predicate:{name:op.predicate},terminal:{name:op.terminal}},superTypes:[So.$type]},BooleanLiteral:{name:gR.$type,properties:{true:{name:gR.true,defaultValue:!1}},superTypes:[cp.$type,Ig.$type]},CharacterRange:{name:lp.$type,properties:{cardinality:{name:lp.cardinality},left:{name:lp.left},lookahead:{name:lp.lookahead},parenthesized:{name:lp.parenthesized,defaultValue:!1},right:{name:lp.right}},superTypes:[wo.$type]},Condition:{name:cp.$type,properties:{},superTypes:[]},Conjunction:{name:_C.$type,properties:{left:{name:_C.left},right:{name:_C.right}},superTypes:[cp.$type]},CrossReference:{name:up.$type,properties:{cardinality:{name:up.cardinality},deprecatedSyntax:{name:up.deprecatedSyntax,defaultValue:!1},isMulti:{name:up.isMulti,defaultValue:!1},terminal:{name:up.terminal},type:{name:up.type,referenceType:Qo.$type}},superTypes:[So.$type]},Disjunction:{name:LC.$type,properties:{left:{name:LC.left},right:{name:LC.right}},superTypes:[cp.$type]},EndOfFile:{name:yR.$type,properties:{cardinality:{name:yR.cardinality}},superTypes:[So.$type]},Grammar:{name:qh.$type,properties:{imports:{name:qh.imports,defaultValue:[]},interfaces:{name:qh.interfaces,defaultValue:[]},isDeclared:{name:qh.isDeclared,defaultValue:!1},name:{name:qh.name},rules:{name:qh.rules,defaultValue:[]},types:{name:qh.types,defaultValue:[]}},superTypes:[]},GrammarImport:{name:vR.$type,properties:{path:{name:vR.path}},superTypes:[]},Group:{name:Cg.$type,properties:{cardinality:{name:Cg.cardinality},elements:{name:Cg.elements,defaultValue:[]},guardCondition:{name:Cg.guardCondition},predicate:{name:Cg.predicate}},superTypes:[So.$type]},InferredType:{name:xR.$type,properties:{name:{name:xR.name}},superTypes:[Qo.$type]},InfixRule:{name:Mu.$type,properties:{call:{name:Mu.call},dataType:{name:Mu.dataType},inferredType:{name:Mu.inferredType},name:{name:Mu.name},operators:{name:Mu.operators},parameters:{name:Mu.parameters,defaultValue:[]},returnType:{name:Mu.returnType,referenceType:Qo.$type}},superTypes:[AC.$type]},InfixRuleOperatorList:{name:DC.$type,properties:{associativity:{name:DC.associativity},operators:{name:DC.operators,defaultValue:[]}},superTypes:[]},InfixRuleOperators:{name:bR.$type,properties:{precedences:{name:bR.precedences,defaultValue:[]}},superTypes:[]},Interface:{name:av.$type,properties:{attributes:{name:av.attributes,defaultValue:[]},name:{name:av.name},superTypes:{name:av.superTypes,defaultValue:[],referenceType:Qo.$type}},superTypes:[Qo.$type]},Keyword:{name:sv.$type,properties:{cardinality:{name:sv.cardinality},predicate:{name:sv.predicate},value:{name:sv.value}},superTypes:[So.$type]},NamedArgument:{name:ov.$type,properties:{calledByName:{name:ov.calledByName,defaultValue:!1},parameter:{name:ov.parameter,referenceType:lv.$type},value:{name:ov.value}},superTypes:[]},NegatedToken:{name:Sg.$type,properties:{cardinality:{name:Sg.cardinality},lookahead:{name:Sg.lookahead},parenthesized:{name:Sg.parenthesized,defaultValue:!1},terminal:{name:Sg.terminal}},superTypes:[wo.$type]},Negation:{name:TR.$type,properties:{value:{name:TR.value}},superTypes:[cp.$type]},NumberLiteral:{name:CR.$type,properties:{value:{name:CR.value}},superTypes:[Ig.$type]},Parameter:{name:lv.$type,properties:{name:{name:lv.name}},superTypes:[]},ParameterReference:{name:SR.$type,properties:{parameter:{name:SR.parameter,referenceType:lv.$type}},superTypes:[cp.$type]},ParserRule:{name:vc.$type,properties:{dataType:{name:vc.dataType},definition:{name:vc.definition},entry:{name:vc.entry,defaultValue:!1},fragment:{name:vc.fragment,defaultValue:!1},inferredType:{name:vc.inferredType},name:{name:vc.name},parameters:{name:vc.parameters,defaultValue:[]},returnType:{name:vc.returnType,referenceType:Qo.$type}},superTypes:[AC.$type]},ReferenceType:{name:IC.$type,properties:{isMulti:{name:IC.isMulti,defaultValue:!1},referenceType:{name:IC.referenceType}},superTypes:[Lg.$type]},RegexToken:{name:wg.$type,properties:{cardinality:{name:wg.cardinality},lookahead:{name:wg.lookahead},parenthesized:{name:wg.parenthesized,defaultValue:!1},regex:{name:wg.regex}},superTypes:[wo.$type]},ReturnType:{name:wR.$type,properties:{name:{name:wR.name}},superTypes:[]},RuleCall:{name:kg.$type,properties:{arguments:{name:kg.arguments,defaultValue:[]},cardinality:{name:kg.cardinality},predicate:{name:kg.predicate},rule:{name:kg.rule,referenceType:iv.$type}},superTypes:[So.$type]},SimpleType:{name:cv.$type,properties:{primitiveType:{name:cv.primitiveType},stringType:{name:cv.stringType},typeRef:{name:cv.typeRef,referenceType:Qo.$type}},superTypes:[Lg.$type]},StringLiteral:{name:kR.$type,properties:{value:{name:kR.value}},superTypes:[Ig.$type]},TerminalAlternatives:{name:Eg.$type,properties:{cardinality:{name:Eg.cardinality},elements:{name:Eg.elements,defaultValue:[]},lookahead:{name:Eg.lookahead},parenthesized:{name:Eg.parenthesized,defaultValue:!1}},superTypes:[wo.$type]},TerminalElement:{name:wo.$type,properties:{cardinality:{name:wo.cardinality},lookahead:{name:wo.lookahead},parenthesized:{name:wo.parenthesized,defaultValue:!1}},superTypes:[So.$type]},TerminalGroup:{name:Ag.$type,properties:{cardinality:{name:Ag.cardinality},elements:{name:Ag.elements,defaultValue:[]},lookahead:{name:Ag.lookahead},parenthesized:{name:Ag.parenthesized,defaultValue:!1}},superTypes:[wo.$type]},TerminalRule:{name:Hh.$type,properties:{definition:{name:Hh.definition},fragment:{name:Hh.fragment,defaultValue:!1},hidden:{name:Hh.hidden,defaultValue:!1},name:{name:Hh.name},type:{name:Hh.type}},superTypes:[iv.$type]},TerminalRuleCall:{name:Rg.$type,properties:{cardinality:{name:Rg.cardinality},lookahead:{name:Rg.lookahead},parenthesized:{name:Rg.parenthesized,defaultValue:!1},rule:{name:Rg.rule,referenceType:Hh.$type}},superTypes:[wo.$type]},Type:{name:MC.$type,properties:{name:{name:MC.name},type:{name:MC.type}},superTypes:[Qo.$type]},TypeAttribute:{name:_g.$type,properties:{defaultValue:{name:_g.defaultValue},isOptional:{name:_g.isOptional,defaultValue:!1},name:{name:_g.name},type:{name:_g.type}},superTypes:[]},TypeDefinition:{name:Lg.$type,properties:{},superTypes:[]},UnionType:{name:ER.$type,properties:{types:{name:ER.types,defaultValue:[]}},superTypes:[Lg.$type]},UnorderedGroup:{name:NC.$type,properties:{cardinality:{name:NC.cardinality},elements:{name:NC.elements,defaultValue:[]}},superTypes:[So.$type]},UntilToken:{name:Dg.$type,properties:{cardinality:{name:Dg.cardinality},lookahead:{name:Dg.lookahead},parenthesized:{name:Dg.parenthesized,defaultValue:!1},terminal:{name:Dg.terminal}},superTypes:[wo.$type]},ValueLiteral:{name:Ig.$type,properties:{},superTypes:[]},Wildcard:{name:uv.$type,properties:{cardinality:{name:uv.cardinality},lookahead:{name:uv.lookahead},parenthesized:{name:uv.parenthesized,defaultValue:!1}},superTypes:[wo.$type]}}}},Er=new NW;s(Xwe,"getDatatypeNode");E(Xwe,"getDatatypeNode");s(Sv,"streamCst");E(Sv,"streamCst");s(Kwe,"flattenCst");E(Kwe,"flattenCst");s(PW,"isChildNode");E(PW,"isChildNode");s(YC,"tokenToRange");E(YC,"tokenToRange");s(wv,"toDocumentSegment");E(wv,"toDocumentSegment");(function(e){e[e.Before=0]="Before",e[e.After=1]="After",e[e.OverlapFront=2]="OverlapFront",e[e.OverlapBack=3]="OverlapBack",e[e.Inside=4]="Inside",e[e.Outside=5]="Outside"})(Pu||(Pu={}));s(OW,"compareRange");E(OW,"compareRange");s(BW,"inRange");E(BW,"inRange");$W=/^[\w\p{L}]$/u;s(Zwe,"findDeclarationNodeAtOffset");E(Zwe,"findDeclarationNodeAtOffset");s(FW,"findCommentNode");E(FW,"findCommentNode");s(AR,"isCommentNode");E(AR,"isCommentNode");s(c6,"findLeafNodeAtOffset");E(c6,"findLeafNodeAtOffset");s(GW,"findLeafNodeBeforeOffset");E(GW,"findLeafNodeBeforeOffset");s(zW,"binarySearch");E(zW,"binarySearch");s(VW,"getPreviousNode");E(VW,"getPreviousNode");s(Qwe,"getNextNode");E(Qwe,"getNextNode");s(Jwe,"getStartlineNode");E(Jwe,"getStartlineNode");s(eke,"getInteriorNodes");E(eke,"getInteriorNodes");s(tke,"getCommonParent");E(tke,"getCommonParent");s(_z,"getParentChain");E(_z,"getParentChain");WW={};xp(WW,{findAssignment:s(()=>rq,"findAssignment"),findNameAssignment:s(()=>y6,"findNameAssignment"),findNodeForKeyword:s(()=>tq,"findNodeForKeyword"),findNodeForProperty:s(()=>p6,"findNodeForProperty"),findNodesForKeyword:s(()=>lke,"findNodesForKeyword"),findNodesForKeywordInternal:s(()=>g6,"findNodesForKeywordInternal"),findNodesForProperty:s(()=>eq,"findNodesForProperty"),getActionAtElement:s(()=>iq,"getActionAtElement"),getActionType:s(()=>sq,"getActionType"),getAllReachableRules:s(()=>f6,"getAllReachableRules"),getAllRulesUsedForCrossReferences:s(()=>oke,"getAllRulesUsedForCrossReferences"),getCrossReferenceTerminal:s(()=>QW,"getCrossReferenceTerminal"),getEntryRule:s(()=>XW,"getEntryRule"),getExplicitRuleType:s(()=>pS,"getExplicitRuleType"),getHiddenRules:s(()=>KW,"getHiddenRules"),getRuleType:s(()=>oq,"getRuleType"),getRuleTypeName:s(()=>fke,"getRuleTypeName"),getTypeName:s(()=>jg,"getTypeName"),isArrayCardinality:s(()=>uke,"isArrayCardinality"),isArrayOperator:s(()=>hke,"isArrayOperator"),isCommentTerminal:s(()=>JW,"isCommentTerminal"),isDataType:s(()=>dke,"isDataType"),isDataTypeRule:s(()=>fS,"isDataTypeRule"),isOptionalCardinality:s(()=>cke,"isOptionalCardinality"),terminalRegex:s(()=>mS,"terminalRegex")});u6=class extends Error{static{s(this,"ErrorWithLocation")}static{E(this,"ErrorWithLocation")}constructor(e,t){super(e?`${t} at ${e.range.start.line}:${e.range.start.character}`:t)}};s(bp,"assertUnreachable");E(bp,"assertUnreachable");s(qW,"assertCondition");E(qW,"assertCondition");HW={};xp(HW,{NEWLINE_REGEXP:s(()=>nke,"NEWLINE_REGEXP"),escapeRegExp:s(()=>Dv,"escapeRegExp"),getTerminalParts:s(()=>ake,"getTerminalParts"),isMultilineComment:s(()=>UW,"isMultilineComment"),isWhitespace:s(()=>d6,"isWhitespace"),partialMatches:s(()=>YW,"partialMatches"),partialRegExp:s(()=>jW,"partialRegExp"),whitespaceCharacters:s(()=>ske,"whitespaceCharacters")});s(Or,"cc");E(Or,"cc");s(WA,"insertToSet");E(WA,"insertToSet");s(J1,"addFlag");E(J1,"addFlag");s(fg,"ASSERT_EXISTS");E(fg,"ASSERT_EXISTS");s(CC,"ASSERT_NEVER_REACH_HERE");E(CC,"ASSERT_NEVER_REACH_HERE");s(Lz,"isCharacter");E(Lz,"isCharacter");RR=[];for(let e=Or("0");e<=Or("9");e++)RR.push(e);_R=[Or("_")].concat(RR);for(let e=Or("a");e<=Or("z");e++)_R.push(e);for(let e=Or("A");e<=Or("Z");e++)_R.push(e);DCe=[Or(" "),Or("\f"),Or(` +`),Or("\r"),Or(" "),Or("\v"),Or(" "),Or("\xA0"),Or("\u1680"),Or("\u2000"),Or("\u2001"),Or("\u2002"),Or("\u2003"),Or("\u2004"),Or("\u2005"),Or("\u2006"),Or("\u2007"),Or("\u2008"),Or("\u2009"),Or("\u200A"),Or("\u2028"),Or("\u2029"),Or("\u202F"),Or("\u205F"),Or("\u3000"),Or("\uFEFF")],l0t=/[0-9a-fA-F]/,yA=/[0-9]/,c0t=/[1-9]/,rke=class{static{s(this,"RegExpParser")}static{E(this,"RegExpParser")}constructor(){this.idx=0,this.input="",this.groupIdx=0}saveState(){return{idx:this.idx,input:this.input,groupIdx:this.groupIdx}}restoreState(e){this.idx=e.idx,this.input=e.input,this.groupIdx=e.groupIdx}pattern(e){this.idx=0,this.input=e,this.groupIdx=0,this.consumeChar("/");let t=this.disjunction();this.consumeChar("/");let r={type:"Flags",loc:{begin:this.idx,end:e.length},global:!1,ignoreCase:!1,multiLine:!1,unicode:!1,sticky:!1};for(;this.isRegExpFlag();)switch(this.popChar()){case"g":J1(r,"global");break;case"i":J1(r,"ignoreCase");break;case"m":J1(r,"multiLine");break;case"u":J1(r,"unicode");break;case"y":J1(r,"sticky");break}if(this.idx!==this.input.length)throw Error("Redundant input: "+this.input.substring(this.idx));return{type:"Pattern",flags:r,value:t,loc:this.loc(0)}}disjunction(){let e=[],t=this.idx;for(e.push(this.alternative());this.peekChar()==="|";)this.consumeChar("|"),e.push(this.alternative());return{type:"Disjunction",value:e,loc:this.loc(t)}}alternative(){let e=[],t=this.idx;for(;this.isTerm();)e.push(this.term());return{type:"Alternative",value:e,loc:this.loc(t)}}term(){return this.isAssertion()?this.assertion():this.atom()}assertion(){let e=this.idx;switch(this.popChar()){case"^":return{type:"StartAnchor",loc:this.loc(e)};case"$":return{type:"EndAnchor",loc:this.loc(e)};case"\\":switch(this.popChar()){case"b":return{type:"WordBoundary",loc:this.loc(e)};case"B":return{type:"NonWordBoundary",loc:this.loc(e)}}throw Error("Invalid Assertion Escape");case"(":this.consumeChar("?");let t;switch(this.popChar()){case"=":t="Lookahead";break;case"!":t="NegativeLookahead";break;case"<":{switch(this.popChar()){case"=":t="Lookbehind";break;case"!":t="NegativeLookbehind"}break}}fg(t);let r=this.disjunction();return this.consumeChar(")"),{type:t,value:r,loc:this.loc(e)}}return CC()}quantifier(e=!1){let t,r=this.idx;switch(this.popChar()){case"*":t={atLeast:0,atMost:1/0};break;case"+":t={atLeast:1,atMost:1/0};break;case"?":t={atLeast:0,atMost:1};break;case"{":let n=this.integerIncludingZero();switch(this.popChar()){case"}":t={atLeast:n,atMost:n};break;case",":let i;this.isDigit()?(i=this.integerIncludingZero(),t={atLeast:n,atMost:i}):t={atLeast:n,atMost:1/0},this.consumeChar("}");break}if(e===!0&&t===void 0)return;fg(t);break}if(!(e===!0&&t===void 0)&&fg(t))return this.peekChar(0)==="?"?(this.consumeChar("?"),t.greedy=!1):t.greedy=!0,t.type="Quantifier",t.loc=this.loc(r),t}atom(){let e,t=this.idx;switch(this.peekChar()){case".":e=this.dotAll();break;case"\\":e=this.atomEscape();break;case"[":e=this.characterClass();break;case"(":e=this.group();break}return e===void 0&&this.isPatternCharacter()&&(e=this.patternCharacter()),fg(e)?(e.loc=this.loc(t),this.isQuantifier()&&(e.quantifier=this.quantifier()),e):CC()}dotAll(){return this.consumeChar("."),{type:"Set",complement:!0,value:[Or(` +`),Or("\r"),Or("\u2028"),Or("\u2029")]}}atomEscape(){switch(this.consumeChar("\\"),this.peekChar()){case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":return this.decimalEscapeAtom();case"d":case"D":case"s":case"S":case"w":case"W":return this.characterClassEscape();case"f":case"n":case"r":case"t":case"v":return this.controlEscapeAtom();case"c":return this.controlLetterEscapeAtom();case"0":return this.nulCharacterAtom();case"x":return this.hexEscapeSequenceAtom();case"u":return this.regExpUnicodeEscapeSequenceAtom();default:return this.identityEscapeAtom()}}decimalEscapeAtom(){return{type:"GroupBackReference",value:this.positiveInteger()}}characterClassEscape(){let e,t=!1;switch(this.popChar()){case"d":e=RR;break;case"D":e=RR,t=!0;break;case"s":e=DCe;break;case"S":e=DCe,t=!0;break;case"w":e=_R;break;case"W":e=_R,t=!0;break}return fg(e)?{type:"Set",value:e,complement:t}:CC()}controlEscapeAtom(){let e;switch(this.popChar()){case"f":e=Or("\f");break;case"n":e=Or(` +`);break;case"r":e=Or("\r");break;case"t":e=Or(" ");break;case"v":e=Or("\v");break}return fg(e)?{type:"Character",value:e}:CC()}controlLetterEscapeAtom(){this.consumeChar("c");let e=this.popChar();if(/[a-zA-Z]/.test(e)===!1)throw Error("Invalid ");return{type:"Character",value:e.toUpperCase().charCodeAt(0)-64}}nulCharacterAtom(){return this.consumeChar("0"),{type:"Character",value:Or("\0")}}hexEscapeSequenceAtom(){return this.consumeChar("x"),this.parseHexDigits(2)}regExpUnicodeEscapeSequenceAtom(){return this.consumeChar("u"),this.parseHexDigits(4)}identityEscapeAtom(){let e=this.popChar();return{type:"Character",value:Or(e)}}classPatternCharacterAtom(){switch(this.peekChar()){case` +`:case"\r":case"\u2028":case"\u2029":case"\\":case"]":throw Error("TBD");default:let e=this.popChar();return{type:"Character",value:Or(e)}}}characterClass(){let e=[],t=!1;for(this.consumeChar("["),this.peekChar(0)==="^"&&(this.consumeChar("^"),t=!0);this.isClassAtom();){let r=this.classAtom(),n=r.type==="Character";if(Lz(r)&&this.isRangeDash()){this.consumeChar("-");let i=this.classAtom(),a=i.type==="Character";if(Lz(i)){if(i.value=this.input.length)throw Error("Unexpected end of input");this.idx++}loc(e){return{begin:e,end:this.idx}}},h6=class{static{s(this,"BaseRegExpVisitor")}static{E(this,"BaseRegExpVisitor")}visitChildren(e){for(let t in e){let r=e[t];e.hasOwnProperty(t)&&(r.type!==void 0?this.visit(r):Array.isArray(r)&&r.forEach(n=>{this.visit(n)},this))}}visit(e){switch(e.type){case"Pattern":this.visitPattern(e);break;case"Flags":this.visitFlags(e);break;case"Disjunction":this.visitDisjunction(e);break;case"Alternative":this.visitAlternative(e);break;case"StartAnchor":this.visitStartAnchor(e);break;case"EndAnchor":this.visitEndAnchor(e);break;case"WordBoundary":this.visitWordBoundary(e);break;case"NonWordBoundary":this.visitNonWordBoundary(e);break;case"Lookahead":this.visitLookahead(e);break;case"NegativeLookahead":this.visitNegativeLookahead(e);break;case"Lookbehind":this.visitLookbehind(e);break;case"NegativeLookbehind":this.visitNegativeLookbehind(e);break;case"Character":this.visitCharacter(e);break;case"Set":this.visitSet(e);break;case"Group":this.visitGroup(e);break;case"GroupBackReference":this.visitGroupBackReference(e);break;case"Quantifier":this.visitQuantifier(e);break}this.visitChildren(e)}visitPattern(e){}visitFlags(e){}visitDisjunction(e){}visitAlternative(e){}visitStartAnchor(e){}visitEndAnchor(e){}visitWordBoundary(e){}visitNonWordBoundary(e){}visitLookahead(e){}visitNegativeLookahead(e){}visitLookbehind(e){}visitNegativeLookbehind(e){}visitCharacter(e){}visitSet(e){}visitGroup(e){}visitGroupBackReference(e){}visitQuantifier(e){}},nke=/\r?\n/gm,ike=new rke,u0t=class extends h6{static{s(this,"TerminalRegExpVisitor")}static{E(this,"TerminalRegExpVisitor")}constructor(){super(...arguments),this.isStarting=!0,this.endRegexpStack=[],this.multiline=!1}get endRegex(){return this.endRegexpStack.join("")}reset(e){this.multiline=!1,this.regex=e,this.startRegexp="",this.isStarting=!0,this.endRegexpStack=[]}visitGroup(e){e.quantifier&&(this.isStarting=!1,this.endRegexpStack=[])}visitCharacter(e){let t=String.fromCharCode(e.value);if(!this.multiline&&t===` +`&&(this.multiline=!0),e.quantifier)this.isStarting=!1,this.endRegexpStack=[];else{let r=Dv(t);this.endRegexpStack.push(r),this.isStarting&&(this.startRegexp+=r)}}visitSet(e){if(!this.multiline){let t=this.regex.substring(e.loc.begin,e.loc.end),r=new RegExp(t);this.multiline=!!` +`.match(r)}if(e.quantifier)this.isStarting=!1,this.endRegexpStack=[];else{let t=this.regex.substring(e.loc.begin,e.loc.end);this.endRegexpStack.push(t),this.isStarting&&(this.startRegexp+=t)}}visitChildren(e){e.type==="Group"&&e.quantifier||super.visitChildren(e)}},Vg=new u0t;s(ake,"getTerminalParts");E(ake,"getTerminalParts");s(UW,"isMultilineComment");E(UW,"isMultilineComment");ske=`\f +\r \v \xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF`.split("");s(d6,"isWhitespace");E(d6,"isWhitespace");s(Dv,"escapeRegExp");E(Dv,"escapeRegExp");s(YW,"partialMatches");E(YW,"partialMatches");s(jW,"partialRegExp");E(jW,"partialRegExp");s(XW,"getEntryRule");E(XW,"getEntryRule");s(KW,"getHiddenRules");E(KW,"getHiddenRules");s(f6,"getAllReachableRules");E(f6,"getAllReachableRules");s(ZW,"ruleDfs");E(ZW,"ruleDfs");s(oke,"getAllRulesUsedForCrossReferences");E(oke,"getAllRulesUsedForCrossReferences");s(QW,"getCrossReferenceTerminal");E(QW,"getCrossReferenceTerminal");s(JW,"isCommentTerminal");E(JW,"isCommentTerminal");s(eq,"findNodesForProperty");E(eq,"findNodesForProperty");s(p6,"findNodeForProperty");E(p6,"findNodeForProperty");s(m6,"findNodesForPropertyInternal");E(m6,"findNodesForPropertyInternal");s(lke,"findNodesForKeyword");E(lke,"findNodesForKeyword");s(tq,"findNodeForKeyword");E(tq,"findNodeForKeyword");s(g6,"findNodesForKeywordInternal");E(g6,"findNodesForKeywordInternal");s(rq,"findAssignment");E(rq,"findAssignment");s(y6,"findNameAssignment");E(y6,"findNameAssignment");s(nq,"findNameAssignmentInternal");E(nq,"findNameAssignmentInternal");s(iq,"getActionAtElement");E(iq,"getActionAtElement");s(cke,"isOptionalCardinality");E(cke,"isOptionalCardinality");s(uke,"isArrayCardinality");E(uke,"isArrayCardinality");s(hke,"isArrayOperator");E(hke,"isArrayOperator");s(fS,"isDataTypeRule");E(fS,"isDataTypeRule");s(aq,"isDataTypeRuleInternal");E(aq,"isDataTypeRuleInternal");s(dke,"isDataType");E(dke,"isDataType");s(LR,"isDataTypeInternal");E(LR,"isDataTypeInternal");s(pS,"getExplicitRuleType");E(pS,"getExplicitRuleType");s(jg,"getTypeName");E(jg,"getTypeName");s(sq,"getActionType");E(sq,"getActionType");s(fke,"getRuleTypeName");E(fke,"getRuleTypeName");s(oq,"getRuleType");E(oq,"getRuleType");s(mS,"terminalRegex");E(mS,"terminalRegex");lq=/[\s\S]/.source;s(i0,"abstractElementToRegex");E(i0,"abstractElementToRegex");s(pke,"terminalAlternativesToRegex");E(pke,"terminalAlternativesToRegex");s(mke,"terminalGroupToRegex");E(mke,"terminalGroupToRegex");s(gke,"untilTokenToRegex");E(gke,"untilTokenToRegex");s(yke,"negateTokenToRegex");E(yke,"negateTokenToRegex");s(vke,"characterRangeToRegex");E(vke,"characterRangeToRegex");s(qA,"keywordToRegex");E(qA,"keywordToRegex");s(Fu,"withCardinality");E(Fu,"withCardinality");s(cq,"createGrammarConfig");E(cq,"createGrammarConfig");h0t=typeof global=="object"&&global&&global.Object===Object&&global,xke=h0t,d0t=typeof self=="object"&&self&&self.Object===Object&&self,f0t=xke||d0t||Function("return this")(),zu=f0t,p0t=zu.Symbol,Nl=p0t,bke=Object.prototype,m0t=bke.hasOwnProperty,g0t=bke.toString,oC=Nl?Nl.toStringTag:void 0;s(Tke,"getRawTag");E(Tke,"getRawTag");y0t=Tke,v0t=Object.prototype,x0t=v0t.toString;s(Cke,"objectToString");E(Cke,"objectToString");b0t=Cke,T0t="[object Null]",C0t="[object Undefined]",ICe=Nl?Nl.toStringTag:void 0;s(Ske,"baseGetTag");E(Ske,"baseGetTag");Tp=Ske;s(wke,"isObjectLike");E(wke,"isObjectLike");Ec=wke,S0t="[object Symbol]";s(kke,"isSymbol");E(kke,"isSymbol");v6=kke;s(Eke,"arrayMap");E(Eke,"arrayMap");gS=Eke,w0t=Array.isArray,gn=w0t,k0t=1/0,MCe=Nl?Nl.prototype:void 0,NCe=MCe?MCe.toString:void 0;s(uq,"baseToString");E(uq,"baseToString");E0t=uq,A0t=/\s/;s(Ake,"trimmedEndIndex");E(Ake,"trimmedEndIndex");R0t=Ake,_0t=/^\s+/;s(Rke,"baseTrim");E(Rke,"baseTrim");L0t=Rke;s(_ke,"isObject");E(_ke,"isObject");Pl=_ke,PCe=NaN,D0t=/^[-+]0x[0-9a-f]+$/i,I0t=/^0b[01]+$/i,M0t=/^0o[0-7]+$/i,N0t=parseInt;s(Lke,"toNumber");E(Lke,"toNumber");P0t=Lke,OCe=1/0,O0t=17976931348623157e292;s(Dke,"toFinite");E(Dke,"toFinite");B0t=Dke;s(Ike,"toInteger");E(Ike,"toInteger");yS=Ike;s(Mke,"identity");E(Mke,"identity");vS=Mke,$0t="[object AsyncFunction]",F0t="[object Function]",G0t="[object GeneratorFunction]",z0t="[object Proxy]";s(Nke,"isFunction");E(Nke,"isFunction");id=Nke,V0t=zu["__core-js_shared__"],oG=V0t,BCe=(function(){var e=/[^.]+$/.exec(oG&&oG.keys&&oG.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""})();s(Pke,"isMasked");E(Pke,"isMasked");W0t=Pke,q0t=Function.prototype,H0t=q0t.toString;s(Oke,"toSource");E(Oke,"toSource");a0=Oke,U0t=/[\\^$.*+?()[\]{}|]/g,Y0t=/^\[object .+?Constructor\]$/,j0t=Function.prototype,X0t=Object.prototype,K0t=j0t.toString,Z0t=X0t.hasOwnProperty,Q0t=RegExp("^"+K0t.call(Z0t).replace(U0t,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");s(Bke,"baseIsNative");E(Bke,"baseIsNative");J0t=Bke;s($ke,"getValue");E($ke,"getValue");eyt=$ke;s(Fke,"getNative");E(Fke,"getNative");s0=Fke,tyt=s0(zu,"WeakMap"),Dz=tyt,$Ce=Object.create,ryt=(function(){function e(){}return s(e,"object"),E(e,"object"),function(t){if(!Pl(t))return{};if($Ce)return $Ce(t);e.prototype=t;var r=new e;return e.prototype=void 0,r}})(),nyt=ryt;s(Gke,"apply");E(Gke,"apply");iyt=Gke;s(zke,"noop");E(zke,"noop");Wa=zke;s(Vke,"copyArray");E(Vke,"copyArray");ayt=Vke,syt=800,oyt=16,lyt=Date.now;s(Wke,"shortOut");E(Wke,"shortOut");cyt=Wke;s(qke,"constant");E(qke,"constant");uyt=qke,hyt=(function(){try{var e=s0(Object,"defineProperty");return e({},"",{}),e}catch{}})(),DR=hyt,dyt=DR?function(e,t){return DR(e,"toString",{configurable:!0,enumerable:!1,value:uyt(t),writable:!0})}:vS,fyt=dyt,pyt=cyt(fyt),myt=pyt;s(Hke,"arrayEach");E(Hke,"arrayEach");Uke=Hke;s(Yke,"baseFindIndex");E(Yke,"baseFindIndex");jke=Yke;s(Xke,"baseIsNaN");E(Xke,"baseIsNaN");gyt=Xke;s(Kke,"strictIndexOf");E(Kke,"strictIndexOf");yyt=Kke;s(Zke,"baseIndexOf");E(Zke,"baseIndexOf");hq=Zke;s(Qke,"arrayIncludes");E(Qke,"arrayIncludes");Jke=Qke,vyt=9007199254740991,xyt=/^(?:0|[1-9]\d*)$/;s(eEe,"isIndex");E(eEe,"isIndex");x6=eEe;s(tEe,"baseAssignValue");E(tEe,"baseAssignValue");dq=tEe;s(rEe,"eq");E(rEe,"eq");xS=rEe,byt=Object.prototype,Tyt=byt.hasOwnProperty;s(nEe,"assignValue");E(nEe,"assignValue");b6=nEe;s(iEe,"copyObject");E(iEe,"copyObject");bS=iEe,FCe=Math.max;s(aEe,"overRest");E(aEe,"overRest");Cyt=aEe;s(sEe,"baseRest");E(sEe,"baseRest");fq=sEe,Syt=9007199254740991;s(oEe,"isLength");E(oEe,"isLength");pq=oEe;s(lEe,"isArrayLike");E(lEe,"isArrayLike");Vu=lEe;s(cEe,"isIterateeCall");E(cEe,"isIterateeCall");T6=cEe;s(uEe,"createAssigner");E(uEe,"createAssigner");wyt=uEe,kyt=Object.prototype;s(hEe,"isPrototype");E(hEe,"isPrototype");TS=hEe;s(dEe,"baseTimes");E(dEe,"baseTimes");Eyt=dEe,Ayt="[object Arguments]";s(fEe,"baseIsArguments");E(fEe,"baseIsArguments");GCe=fEe,pEe=Object.prototype,Ryt=pEe.hasOwnProperty,_yt=pEe.propertyIsEnumerable,Lyt=GCe((function(){return arguments})())?GCe:function(e){return Ec(e)&&Ryt.call(e,"callee")&&!_yt.call(e,"callee")},C6=Lyt;s(mEe,"stubFalse");E(mEe,"stubFalse");Dyt=mEe,gEe=typeof exports=="object"&&exports&&!exports.nodeType&&exports,zCe=gEe&&typeof module=="object"&&module&&!module.nodeType&&module,Iyt=zCe&&zCe.exports===gEe,VCe=Iyt?zu.Buffer:void 0,Myt=VCe?VCe.isBuffer:void 0,Nyt=Myt||Dyt,jC=Nyt,Pyt="[object Arguments]",Oyt="[object Array]",Byt="[object Boolean]",$yt="[object Date]",Fyt="[object Error]",Gyt="[object Function]",zyt="[object Map]",Vyt="[object Number]",Wyt="[object Object]",qyt="[object RegExp]",Hyt="[object Set]",Uyt="[object String]",Yyt="[object WeakMap]",jyt="[object ArrayBuffer]",Xyt="[object DataView]",Kyt="[object Float32Array]",Zyt="[object Float64Array]",Qyt="[object Int8Array]",Jyt="[object Int16Array]",e1t="[object Int32Array]",t1t="[object Uint8Array]",r1t="[object Uint8ClampedArray]",n1t="[object Uint16Array]",i1t="[object Uint32Array]",hi={};hi[Kyt]=hi[Zyt]=hi[Qyt]=hi[Jyt]=hi[e1t]=hi[t1t]=hi[r1t]=hi[n1t]=hi[i1t]=!0;hi[Pyt]=hi[Oyt]=hi[jyt]=hi[Byt]=hi[Xyt]=hi[$yt]=hi[Fyt]=hi[Gyt]=hi[zyt]=hi[Vyt]=hi[Wyt]=hi[qyt]=hi[Hyt]=hi[Uyt]=hi[Yyt]=!1;s(yEe,"baseIsTypedArray");E(yEe,"baseIsTypedArray");a1t=yEe;s(vEe,"baseUnary");E(vEe,"baseUnary");CS=vEe,xEe=typeof exports=="object"&&exports&&!exports.nodeType&&exports,PC=xEe&&typeof module=="object"&&module&&!module.nodeType&&module,s1t=PC&&PC.exports===xEe,lG=s1t&&xke.process,o1t=(function(){try{var e=PC&&PC.require&&PC.require("util").types;return e||lG&&lG.binding&&lG.binding("util")}catch{}})(),mp=o1t,WCe=mp&&mp.isTypedArray,l1t=WCe?CS(WCe):a1t,mq=l1t,c1t=Object.prototype,u1t=c1t.hasOwnProperty;s(bEe,"arrayLikeKeys");E(bEe,"arrayLikeKeys");TEe=bEe;s(CEe,"overArg");E(CEe,"overArg");SEe=CEe,h1t=SEe(Object.keys,Object),d1t=h1t,f1t=Object.prototype,p1t=f1t.hasOwnProperty;s(wEe,"baseKeys");E(wEe,"baseKeys");kEe=wEe;s(EEe,"keys");E(EEe,"keys");el=EEe,m1t=Object.prototype,g1t=m1t.hasOwnProperty,y1t=wyt(function(e,t){if(TS(t)||Vu(t)){bS(t,el(t),e);return}for(var r in t)g1t.call(t,r)&&b6(e,r,t[r])}),tl=y1t;s(AEe,"nativeKeysIn");E(AEe,"nativeKeysIn");v1t=AEe,x1t=Object.prototype,b1t=x1t.hasOwnProperty;s(REe,"baseKeysIn");E(REe,"baseKeysIn");T1t=REe;s(_Ee,"keysIn");E(_Ee,"keysIn");S6=_Ee,C1t=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,S1t=/^\w*$/;s(LEe,"isKey");E(LEe,"isKey");gq=LEe,w1t=s0(Object,"create"),XC=w1t;s(DEe,"hashClear");E(DEe,"hashClear");k1t=DEe;s(IEe,"hashDelete");E(IEe,"hashDelete");E1t=IEe,A1t="__lodash_hash_undefined__",R1t=Object.prototype,_1t=R1t.hasOwnProperty;s(MEe,"hashGet");E(MEe,"hashGet");L1t=MEe,D1t=Object.prototype,I1t=D1t.hasOwnProperty;s(NEe,"hashHas");E(NEe,"hashHas");M1t=NEe,N1t="__lodash_hash_undefined__";s(PEe,"hashSet");E(PEe,"hashSet");P1t=PEe;s(o0,"Hash");E(o0,"Hash");o0.prototype.clear=k1t;o0.prototype.delete=E1t;o0.prototype.get=L1t;o0.prototype.has=M1t;o0.prototype.set=P1t;qCe=o0;s(OEe,"listCacheClear");E(OEe,"listCacheClear");O1t=OEe;s(BEe,"assocIndexOf");E(BEe,"assocIndexOf");w6=BEe,B1t=Array.prototype,$1t=B1t.splice;s($Ee,"listCacheDelete");E($Ee,"listCacheDelete");F1t=$Ee;s(FEe,"listCacheGet");E(FEe,"listCacheGet");G1t=FEe;s(GEe,"listCacheHas");E(GEe,"listCacheHas");z1t=GEe;s(zEe,"listCacheSet");E(zEe,"listCacheSet");V1t=zEe;s(l0,"ListCache");E(l0,"ListCache");l0.prototype.clear=O1t;l0.prototype.delete=F1t;l0.prototype.get=G1t;l0.prototype.has=z1t;l0.prototype.set=V1t;k6=l0,W1t=s0(zu,"Map"),KC=W1t;s(VEe,"mapCacheClear");E(VEe,"mapCacheClear");q1t=VEe;s(WEe,"isKeyable");E(WEe,"isKeyable");H1t=WEe;s(qEe,"getMapData");E(qEe,"getMapData");E6=qEe;s(HEe,"mapCacheDelete");E(HEe,"mapCacheDelete");U1t=HEe;s(UEe,"mapCacheGet");E(UEe,"mapCacheGet");Y1t=UEe;s(YEe,"mapCacheHas");E(YEe,"mapCacheHas");j1t=YEe;s(jEe,"mapCacheSet");E(jEe,"mapCacheSet");X1t=jEe;s(c0,"MapCache");E(c0,"MapCache");c0.prototype.clear=q1t;c0.prototype.delete=U1t;c0.prototype.get=Y1t;c0.prototype.has=j1t;c0.prototype.set=X1t;A6=c0,K1t="Expected a function";s(R6,"memoize");E(R6,"memoize");R6.Cache=A6;Z1t=R6,Q1t=500;s(XEe,"memoizeCapped");E(XEe,"memoizeCapped");J1t=XEe,evt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,tvt=/\\(\\)?/g,rvt=J1t(function(e){var t=[];return e.charCodeAt(0)===46&&t.push(""),e.replace(evt,function(r,n,i,a){t.push(i?a.replace(tvt,"$1"):n||r)}),t}),nvt=rvt;s(KEe,"toString2");E(KEe,"toString");ivt=KEe;s(ZEe,"castPath");E(ZEe,"castPath");_6=ZEe,avt=1/0;s(QEe,"toKey");E(QEe,"toKey");SS=QEe;s(JEe,"baseGet");E(JEe,"baseGet");yq=JEe;s(e4e,"get");E(e4e,"get");svt=e4e;s(t4e,"arrayPush");E(t4e,"arrayPush");vq=t4e,HCe=Nl?Nl.isConcatSpreadable:void 0;s(r4e,"isFlattenable");E(r4e,"isFlattenable");ovt=r4e;s(xq,"baseFlatten");E(xq,"baseFlatten");bq=xq;s(n4e,"flatten");E(n4e,"flatten");wc=n4e,lvt=SEe(Object.getPrototypeOf,Object),i4e=lvt;s(a4e,"baseSlice");E(a4e,"baseSlice");s4e=a4e;s(o4e,"arrayReduce");E(o4e,"arrayReduce");cvt=o4e;s(l4e,"stackClear");E(l4e,"stackClear");uvt=l4e;s(c4e,"stackDelete");E(c4e,"stackDelete");hvt=c4e;s(u4e,"stackGet");E(u4e,"stackGet");dvt=u4e;s(h4e,"stackHas");E(h4e,"stackHas");fvt=h4e,pvt=200;s(d4e,"stackSet");E(d4e,"stackSet");mvt=d4e;s(u0,"Stack");E(u0,"Stack");u0.prototype.clear=uvt;u0.prototype.delete=hvt;u0.prototype.get=dvt;u0.prototype.has=fvt;u0.prototype.set=mvt;OC=u0;s(f4e,"baseAssign");E(f4e,"baseAssign");gvt=f4e;s(p4e,"baseAssignIn");E(p4e,"baseAssignIn");yvt=p4e,m4e=typeof exports=="object"&&exports&&!exports.nodeType&&exports,UCe=m4e&&typeof module=="object"&&module&&!module.nodeType&&module,vvt=UCe&&UCe.exports===m4e,YCe=vvt?zu.Buffer:void 0,jCe=YCe?YCe.allocUnsafe:void 0;s(g4e,"cloneBuffer");E(g4e,"cloneBuffer");xvt=g4e;s(y4e,"arrayFilter");E(y4e,"arrayFilter");Tq=y4e;s(v4e,"stubArray");E(v4e,"stubArray");x4e=v4e,bvt=Object.prototype,Tvt=bvt.propertyIsEnumerable,XCe=Object.getOwnPropertySymbols,Cvt=XCe?function(e){return e==null?[]:(e=Object(e),Tq(XCe(e),function(t){return Tvt.call(e,t)}))}:x4e,Cq=Cvt;s(b4e,"copySymbols");E(b4e,"copySymbols");Svt=b4e,wvt=Object.getOwnPropertySymbols,kvt=wvt?function(e){for(var t=[];e;)vq(t,Cq(e)),e=i4e(e);return t}:x4e,T4e=kvt;s(C4e,"copySymbolsIn");E(C4e,"copySymbolsIn");Evt=C4e;s(S4e,"baseGetAllKeys");E(S4e,"baseGetAllKeys");w4e=S4e;s(k4e,"getAllKeys");E(k4e,"getAllKeys");Iz=k4e;s(E4e,"getAllKeysIn");E(E4e,"getAllKeysIn");A4e=E4e,Avt=s0(zu,"DataView"),Mz=Avt,Rvt=s0(zu,"Promise"),Nz=Rvt,_vt=s0(zu,"Set"),mv=_vt,KCe="[object Map]",Lvt="[object Object]",ZCe="[object Promise]",QCe="[object Set]",JCe="[object WeakMap]",eSe="[object DataView]",Dvt=a0(Mz),Ivt=a0(KC),Mvt=a0(Nz),Nvt=a0(mv),Pvt=a0(Dz),pg=Tp;(Mz&&pg(new Mz(new ArrayBuffer(1)))!=eSe||KC&&pg(new KC)!=KCe||Nz&&pg(Nz.resolve())!=ZCe||mv&&pg(new mv)!=QCe||Dz&&pg(new Dz)!=JCe)&&(pg=E(function(e){var t=Tp(e),r=t==Lvt?e.constructor:void 0,n=r?a0(r):"";if(n)switch(n){case Dvt:return eSe;case Ivt:return KCe;case Mvt:return ZCe;case Nvt:return QCe;case Pvt:return JCe}return t},"getTag"));kv=pg,Ovt=Object.prototype,Bvt=Ovt.hasOwnProperty;s(R4e,"initCloneArray");E(R4e,"initCloneArray");$vt=R4e,Fvt=zu.Uint8Array,IR=Fvt;s(_4e,"cloneArrayBuffer");E(_4e,"cloneArrayBuffer");Sq=_4e;s(L4e,"cloneDataView");E(L4e,"cloneDataView");Gvt=L4e,zvt=/\w*$/;s(D4e,"cloneRegExp");E(D4e,"cloneRegExp");Vvt=D4e,tSe=Nl?Nl.prototype:void 0,rSe=tSe?tSe.valueOf:void 0;s(I4e,"cloneSymbol");E(I4e,"cloneSymbol");Wvt=I4e;s(M4e,"cloneTypedArray");E(M4e,"cloneTypedArray");qvt=M4e,Hvt="[object Boolean]",Uvt="[object Date]",Yvt="[object Map]",jvt="[object Number]",Xvt="[object RegExp]",Kvt="[object Set]",Zvt="[object String]",Qvt="[object Symbol]",Jvt="[object ArrayBuffer]",ext="[object DataView]",txt="[object Float32Array]",rxt="[object Float64Array]",nxt="[object Int8Array]",ixt="[object Int16Array]",axt="[object Int32Array]",sxt="[object Uint8Array]",oxt="[object Uint8ClampedArray]",lxt="[object Uint16Array]",cxt="[object Uint32Array]";s(N4e,"initCloneByTag");E(N4e,"initCloneByTag");uxt=N4e;s(P4e,"initCloneObject");E(P4e,"initCloneObject");hxt=P4e,dxt="[object Map]";s(O4e,"baseIsMap");E(O4e,"baseIsMap");fxt=O4e,nSe=mp&&mp.isMap,pxt=nSe?CS(nSe):fxt,mxt=pxt,gxt="[object Set]";s(B4e,"baseIsSet");E(B4e,"baseIsSet");yxt=B4e,iSe=mp&&mp.isSet,vxt=iSe?CS(iSe):yxt,xxt=vxt,bxt=1,Txt=2,Cxt=4,$4e="[object Arguments]",Sxt="[object Array]",wxt="[object Boolean]",kxt="[object Date]",Ext="[object Error]",F4e="[object Function]",Axt="[object GeneratorFunction]",Rxt="[object Map]",_xt="[object Number]",G4e="[object Object]",Lxt="[object RegExp]",Dxt="[object Set]",Ixt="[object String]",Mxt="[object Symbol]",Nxt="[object WeakMap]",Pxt="[object ArrayBuffer]",Oxt="[object DataView]",Bxt="[object Float32Array]",$xt="[object Float64Array]",Fxt="[object Int8Array]",Gxt="[object Int16Array]",zxt="[object Int32Array]",Vxt="[object Uint8Array]",Wxt="[object Uint8ClampedArray]",qxt="[object Uint16Array]",Hxt="[object Uint32Array]",ti={};ti[$4e]=ti[Sxt]=ti[Pxt]=ti[Oxt]=ti[wxt]=ti[kxt]=ti[Bxt]=ti[$xt]=ti[Fxt]=ti[Gxt]=ti[zxt]=ti[Rxt]=ti[_xt]=ti[G4e]=ti[Lxt]=ti[Dxt]=ti[Ixt]=ti[Mxt]=ti[Vxt]=ti[Wxt]=ti[qxt]=ti[Hxt]=!0;ti[Ext]=ti[F4e]=ti[Nxt]=!1;s(BC,"baseClone");E(BC,"baseClone");Uxt=BC,Yxt=4;s(z4e,"clone");E(z4e,"clone");ss=z4e;s(V4e,"compact");E(V4e,"compact");wS=V4e,jxt="__lodash_hash_undefined__";s(W4e,"setCacheAdd");E(W4e,"setCacheAdd");Xxt=W4e;s(q4e,"setCacheHas");E(q4e,"setCacheHas");Kxt=q4e;s(ZC,"SetCache");E(ZC,"SetCache");ZC.prototype.add=ZC.prototype.push=Xxt;ZC.prototype.has=Kxt;wq=ZC;s(H4e,"arraySome");E(H4e,"arraySome");U4e=H4e;s(Y4e,"cacheHas");E(Y4e,"cacheHas");kq=Y4e,Zxt=1,Qxt=2;s(j4e,"equalArrays");E(j4e,"equalArrays");X4e=j4e;s(K4e,"mapToArray");E(K4e,"mapToArray");Jxt=K4e;s(Z4e,"setToArray");E(Z4e,"setToArray");Eq=Z4e,ebt=1,tbt=2,rbt="[object Boolean]",nbt="[object Date]",ibt="[object Error]",abt="[object Map]",sbt="[object Number]",obt="[object RegExp]",lbt="[object Set]",cbt="[object String]",ubt="[object Symbol]",hbt="[object ArrayBuffer]",dbt="[object DataView]",aSe=Nl?Nl.prototype:void 0,cG=aSe?aSe.valueOf:void 0;s(Q4e,"equalByTag");E(Q4e,"equalByTag");fbt=Q4e,pbt=1,mbt=Object.prototype,gbt=mbt.hasOwnProperty;s(J4e,"equalObjects");E(J4e,"equalObjects");ybt=J4e,vbt=1,sSe="[object Arguments]",oSe="[object Array]",vA="[object Object]",xbt=Object.prototype,lSe=xbt.hasOwnProperty;s(e3e,"baseIsEqualDeep");E(e3e,"baseIsEqualDeep");bbt=e3e;s(Aq,"baseIsEqual");E(Aq,"baseIsEqual");t3e=Aq,Tbt=1,Cbt=2;s(r3e,"baseIsMatch");E(r3e,"baseIsMatch");Sbt=r3e;s(n3e,"isStrictComparable");E(n3e,"isStrictComparable");i3e=n3e;s(a3e,"getMatchData");E(a3e,"getMatchData");wbt=a3e;s(s3e,"matchesStrictComparable");E(s3e,"matchesStrictComparable");o3e=s3e;s(l3e,"baseMatches");E(l3e,"baseMatches");kbt=l3e;s(c3e,"baseHasIn");E(c3e,"baseHasIn");Ebt=c3e;s(u3e,"hasPath");E(u3e,"hasPath");h3e=u3e;s(d3e,"hasIn");E(d3e,"hasIn");Abt=d3e,Rbt=1,_bt=2;s(f3e,"baseMatchesProperty");E(f3e,"baseMatchesProperty");Lbt=f3e;s(p3e,"baseProperty");E(p3e,"baseProperty");Dbt=p3e;s(m3e,"basePropertyDeep");E(m3e,"basePropertyDeep");Ibt=m3e;s(g3e,"property");E(g3e,"property");Mbt=g3e;s(y3e,"baseIteratee");E(y3e,"baseIteratee");Wu=y3e;s(v3e,"arrayAggregator");E(v3e,"arrayAggregator");Nbt=v3e;s(x3e,"createBaseFor");E(x3e,"createBaseFor");Pbt=x3e,Obt=Pbt(),Bbt=Obt;s(b3e,"baseForOwn");E(b3e,"baseForOwn");$bt=b3e;s(T3e,"createBaseEach");E(T3e,"createBaseEach");Fbt=T3e,Gbt=Fbt($bt),h0=Gbt;s(C3e,"baseAggregator");E(C3e,"baseAggregator");zbt=C3e;s(S3e,"createAggregator");E(S3e,"createAggregator");Vbt=S3e,w3e=Object.prototype,Wbt=w3e.hasOwnProperty,qbt=fq(function(e,t){e=Object(e);var r=-1,n=t.length,i=n>2?t[2]:void 0;for(i&&T6(t[0],t[1],i)&&(n=1);++r{t.accept(e)})}},Zs=class extends qu{static{s(this,"NonTerminal")}static{E(this,"NonTerminal")}constructor(e){super([]),this.idx=1,tl(this,Rc(e,t=>t!==void 0))}set definition(e){}get definition(){return this.referencedRule!==void 0?this.referencedRule.definition:[]}accept(e){e.visit(this)}},Iv=class extends qu{static{s(this,"Rule")}static{E(this,"Rule")}constructor(e){super(e.definition),this.orgText="",tl(this,Rc(e,t=>t!==void 0))}},Eo=class extends qu{static{s(this,"Alternative")}static{E(this,"Alternative")}constructor(e){super(e.definition),this.ignoreAmbiguities=!1,tl(this,Rc(e,t=>t!==void 0))}},as=class extends qu{static{s(this,"Option")}static{E(this,"Option")}constructor(e){super(e.definition),this.idx=1,tl(this,Rc(e,t=>t!==void 0))}},nl=class extends qu{static{s(this,"RepetitionMandatory")}static{E(this,"RepetitionMandatory")}constructor(e){super(e.definition),this.idx=1,tl(this,Rc(e,t=>t!==void 0))}},il=class extends qu{static{s(this,"RepetitionMandatoryWithSeparator")}static{E(this,"RepetitionMandatoryWithSeparator")}constructor(e){super(e.definition),this.idx=1,tl(this,Rc(e,t=>t!==void 0))}},Gi=class extends qu{static{s(this,"Repetition")}static{E(this,"Repetition")}constructor(e){super(e.definition),this.idx=1,tl(this,Rc(e,t=>t!==void 0))}},Ao=class extends qu{static{s(this,"RepetitionWithSeparator")}static{E(this,"RepetitionWithSeparator")}constructor(e){super(e.definition),this.idx=1,tl(this,Rc(e,t=>t!==void 0))}},Ro=class extends qu{static{s(this,"Alternation")}static{E(this,"Alternation")}get definition(){return this._definition}set definition(e){this._definition=e}constructor(e){super(e.definition),this.idx=1,this.ignoreAmbiguities=!1,this.hasPredicates=!1,tl(this,Rc(e,t=>t!==void 0))}},fi=class{static{s(this,"Terminal")}static{E(this,"Terminal")}constructor(e){this.idx=1,tl(this,Rc(e,t=>t!==void 0))}accept(e){e.visit(this)}};s(g5e,"serializeGrammar");E(g5e,"serializeGrammar");s($C,"serializeProduction");E($C,"serializeProduction");Mv=class{static{s(this,"GAstVisitor")}static{E(this,"GAstVisitor")}visit(e){let t=e;switch(t.constructor){case Zs:return this.visitNonTerminal(t);case Eo:return this.visitAlternative(t);case as:return this.visitOption(t);case nl:return this.visitRepetitionMandatory(t);case il:return this.visitRepetitionMandatoryWithSeparator(t);case Ao:return this.visitRepetitionWithSeparator(t);case Gi:return this.visitRepetition(t);case Ro:return this.visitAlternation(t);case fi:return this.visitTerminal(t);case Iv:return this.visitRule(t);default:throw Error("non exhaustive match")}}visitNonTerminal(e){}visitAlternative(e){}visitOption(e){}visitRepetition(e){}visitRepetitionMandatory(e){}visitRepetitionMandatoryWithSeparator(e){}visitRepetitionWithSeparator(e){}visitAlternation(e){}visitTerminal(e){}visitRule(e){}};s(y5e,"isSequenceProd");E(y5e,"isSequenceProd");s(JC,"isOptionalProd");E(JC,"isOptionalProd");s(v5e,"isBranchingProd");E(v5e,"isBranchingProd");s(bc,"getProductionDslName");E(bc,"getProductionDslName");I6=class{static{s(this,"RestWalker")}static{E(this,"RestWalker")}walk(e,t=[]){Br(e.definition,(r,n)=>{let i=is(e.definition,n+1);if(r instanceof Zs)this.walkProdRef(r,i,t);else if(r instanceof fi)this.walkTerminal(r,i,t);else if(r instanceof Eo)this.walkFlat(r,i,t);else if(r instanceof as)this.walkOption(r,i,t);else if(r instanceof nl)this.walkAtLeastOne(r,i,t);else if(r instanceof il)this.walkAtLeastOneSep(r,i,t);else if(r instanceof Ao)this.walkManySep(r,i,t);else if(r instanceof Gi)this.walkMany(r,i,t);else if(r instanceof Ro)this.walkOr(r,i,t);else throw Error("non exhaustive match")})}walkTerminal(e,t,r){}walkProdRef(e,t,r){}walkFlat(e,t,r){let n=t.concat(r);this.walk(e,n)}walkOption(e,t,r){let n=t.concat(r);this.walk(e,n)}walkAtLeastOne(e,t,r){let n=[new as({definition:e.definition})].concat(t,r);this.walk(e,n)}walkAtLeastOneSep(e,t,r){let n=Pz(e,t,r);this.walk(e,n)}walkMany(e,t,r){let n=[new as({definition:e.definition})].concat(t,r);this.walk(e,n)}walkManySep(e,t,r){let n=Pz(e,t,r);this.walk(e,n)}walkOr(e,t,r){let n=t.concat(r);Br(e.definition,i=>{let a=new Eo({definition:[i]});this.walk(a,n)})}};s(Pz,"restForRepetitionWithSeparator");E(Pz,"restForRepetitionWithSeparator");s(Nv,"first");E(Nv,"first");s(x5e,"firstForSequence");E(x5e,"firstForSequence");s(b5e,"firstForBranching");E(b5e,"firstForBranching");s(T5e,"firstForTerminal");E(T5e,"firstForTerminal");C5e="_~IN~_",D2t=class extends I6{static{s(this,"ResyncFollowsWalker")}static{E(this,"ResyncFollowsWalker")}constructor(e){super(),this.topProd=e,this.follows={}}startWalking(){return this.walk(this.topProd),this.follows}walkTerminal(e,t,r){}walkProdRef(e,t,r){let n=w5e(e.referencedRule,e.idx)+this.topProd.name,i=t.concat(r),a=new Eo({definition:i}),o=Nv(a);this.follows[n]=o}};s(S5e,"computeAllProdsFollows");E(S5e,"computeAllProdsFollows");s(w5e,"buildBetweenProdsFollowPrefix");E(w5e,"buildBetweenProdsFollowPrefix");HA={},I2t=new rke;s(kS,"getRegExpAst");E(kS,"getRegExpAst");s(k5e,"clearRegExpParserCache");E(k5e,"clearRegExpParserCache");E5e="Complement Sets are not supported for first char optimization",NR=`Unable to use "first char" lexer optimizations: +`;s(A5e,"getOptimizedStartCodesIndices");E(A5e,"getOptimizedStartCodesIndices");s(PR,"firstCharOptimizedIndices");E(PR,"firstCharOptimizedIndices");s(SC,"addOptimizedIdxToResult");E(SC,"addOptimizedIdxToResult");s(R5e,"handleIgnoreCase");E(R5e,"handleIgnoreCase");s(Oz,"findCode");E(Oz,"findCode");s(OR,"isWholeOptional");E(OR,"isWholeOptional");M2t=class extends h6{static{s(this,"CharCodeFinder")}static{E(this,"CharCodeFinder")}constructor(e){super(),this.targetCharCodes=e,this.found=!1}visitChildren(e){if(this.found!==!0){switch(e.type){case"Lookahead":this.visitLookahead(e);return;case"NegativeLookahead":this.visitNegativeLookahead(e);return;case"Lookbehind":this.visitLookbehind(e);return;case"NegativeLookbehind":this.visitNegativeLookbehind(e);return}super.visitChildren(e)}}visitCharacter(e){eo(this.targetCharCodes,e.value)&&(this.found=!0)}visitSet(e){e.complement?Oz(e,this.targetCharCodes)===void 0&&(this.found=!0):Oz(e,this.targetCharCodes)!==void 0&&(this.found=!0)}};s(M6,"canMatchCharCode");E(M6,"canMatchCharCode");Kg="PATTERN",wC="defaultMode",xA="modes";s(_5e,"analyzeTokenTypes");E(_5e,"analyzeTokenTypes");s(L5e,"validatePatterns");E(L5e,"validatePatterns");s(D5e,"validateRegExpPattern");E(D5e,"validateRegExpPattern");s(I5e,"findMissingPatterns");E(I5e,"findMissingPatterns");s(M5e,"findInvalidPatterns");E(M5e,"findInvalidPatterns");N2t=/[^\\][$]/;s(N5e,"findEndOfInputAnchor");E(N5e,"findEndOfInputAnchor");s(P5e,"findEmptyMatchRegExps");E(P5e,"findEmptyMatchRegExps");P2t=/[^\\[][\^]|^\^/;s(O5e,"findStartOfInputAnchor");E(O5e,"findStartOfInputAnchor");s(B5e,"findUnsupportedFlags");E(B5e,"findUnsupportedFlags");s($5e,"findDuplicatePatterns");E($5e,"findDuplicatePatterns");s(F5e,"findInvalidGroupType");E(F5e,"findInvalidGroupType");s(G5e,"findModesThatDoNotExist");E(G5e,"findModesThatDoNotExist");s(z5e,"findUnreachablePatterns");E(z5e,"findUnreachablePatterns");s(V5e,"tryToMatchStrToPattern");E(V5e,"tryToMatchStrToPattern");s(W5e,"noMetaChar");E(W5e,"noMetaChar");s(q5e,"usesLookAheadOrBehind");E(q5e,"usesLookAheadOrBehind");s(Bz,"addStickyFlag");E(Bz,"addStickyFlag");s(H5e,"performRuntimeChecks");E(H5e,"performRuntimeChecks");s(U5e,"performWarningRuntimeChecks");E(U5e,"performWarningRuntimeChecks");s(Y5e,"cloneEmptyGroups");E(Y5e,"cloneEmptyGroups");s(Mq,"isCustomPattern");E(Mq,"isCustomPattern");s(j5e,"isShortPattern");E(j5e,"isShortPattern");O2t={test:E(function(e){let t=e.length;for(let r=this.lastIndex;r${e.image}<- The Mode Stack is empty`},buildUnexpectedCharactersMessage(e,t,r,n,i,a){return`unexpected character: ->${e.charAt(t)}<- at offset: ${t}, skipped ${r} characters.`}};(function(e){e[e.MISSING_PATTERN=0]="MISSING_PATTERN",e[e.INVALID_PATTERN=1]="INVALID_PATTERN",e[e.EOI_ANCHOR_FOUND=2]="EOI_ANCHOR_FOUND",e[e.UNSUPPORTED_FLAGS_FOUND=3]="UNSUPPORTED_FLAGS_FOUND",e[e.DUPLICATE_PATTERNS_FOUND=4]="DUPLICATE_PATTERNS_FOUND",e[e.INVALID_GROUP_TYPE_FOUND=5]="INVALID_GROUP_TYPE_FOUND",e[e.PUSH_MODE_DOES_NOT_EXIST=6]="PUSH_MODE_DOES_NOT_EXIST",e[e.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE=7]="MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE",e[e.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY=8]="MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY",e[e.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST=9]="MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST",e[e.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED=10]="LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED",e[e.SOI_ANCHOR_FOUND=11]="SOI_ANCHOR_FOUND",e[e.EMPTY_MATCH_PATTERN=12]="EMPTY_MATCH_PATTERN",e[e.NO_LINE_BREAKS_FLAGS=13]="NO_LINE_BREAKS_FLAGS",e[e.UNREACHABLE_PATTERN=14]="UNREACHABLE_PATTERN",e[e.IDENTIFY_TERMINATOR=15]="IDENTIFY_TERMINATOR",e[e.CUSTOM_LINE_BREAK=16]="CUSTOM_LINE_BREAK",e[e.MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE=17]="MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE"})(zi||(zi={}));EC={deferDefinitionErrorsHandling:!1,positionTracking:"full",lineTerminatorsPattern:/\n|\r\n?/g,lineTerminatorCharacters:[` +`,"\r"],ensureOptimizations:!1,safeMode:!1,errorMessageProvider:Fz,traceInitPerf:!1,skipValidations:!1,recoveryEnabled:!0};Object.freeze(EC);Xs=class{static{s(this,"Lexer")}static{E(this,"Lexer")}constructor(e,t=EC){if(this.lexerDefinition=e,this.lexerDefinitionErrors=[],this.lexerDefinitionWarning=[],this.patternIdxToConfig={},this.charCodeToPatternIdxToConfig={},this.modes=[],this.emptyGroups={},this.trackStartLines=!0,this.trackEndLines=!0,this.hasCustom=!1,this.canModeBeOptimized={},this.TRACE_INIT=(n,i)=>{if(this.traceInitPerf===!0){this.traceInitIndent++;let a=new Array(this.traceInitIndent+1).join(" ");this.traceInitIndent <${n}>`);let{time:o,value:l}=Dq(i),u=o>10?console.warn:console.log;return this.traceInitIndent time: ${o}ms`),this.traceInitIndent--,l}else return i()},typeof t=="boolean")throw Error(`The second argument to the Lexer constructor is now an ILexerConfig Object. +a boolean 2nd argument is no longer supported`);this.config=tl({},EC,t);let r=this.config.traceInitPerf;r===!0?(this.traceInitMaxIdent=1/0,this.traceInitPerf=!0):typeof r=="number"&&(this.traceInitMaxIdent=r,this.traceInitPerf=!0),this.traceInitIndent=-1,this.TRACE_INIT("Lexer Constructor",()=>{let n,i=!0;this.TRACE_INIT("Lexer Config handling",()=>{if(this.config.lineTerminatorsPattern===EC.lineTerminatorsPattern)this.config.lineTerminatorsPattern=O2t;else if(this.config.lineTerminatorCharacters===EC.lineTerminatorCharacters)throw Error(`Error: Missing property on the Lexer config. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#MISSING_LINE_TERM_CHARS`);if(t.safeMode&&t.ensureOptimizations)throw Error('"safeMode" and "ensureOptimizations" flags are mutually exclusive.');this.trackStartLines=/full|onlyStart/i.test(this.config.positionTracking),this.trackEndLines=/full/i.test(this.config.positionTracking),gn(e)?n={modes:{defaultMode:ss(e)},defaultMode:wC}:(i=!1,n=ss(e))}),this.config.skipValidations===!1&&(this.TRACE_INIT("performRuntimeChecks",()=>{this.lexerDefinitionErrors=this.lexerDefinitionErrors.concat(H5e(n,this.trackStartLines,this.config.lineTerminatorCharacters))}),this.TRACE_INIT("performWarningRuntimeChecks",()=>{this.lexerDefinitionWarning=this.lexerDefinitionWarning.concat(U5e(n,this.trackStartLines,this.config.lineTerminatorCharacters))})),n.modes=n.modes?n.modes:{},Br(n.modes,(o,l)=>{n.modes[l]=D6(o,u=>Qh(u))});let a=el(n.modes);if(Br(n.modes,(o,l)=>{this.TRACE_INIT(`Mode: <${l}> processing`,()=>{if(this.modes.push(l),this.config.skipValidations===!1&&this.TRACE_INIT("validatePatterns",()=>{this.lexerDefinitionErrors=this.lexerDefinitionErrors.concat(L5e(o,a))}),ni(this.lexerDefinitionErrors)){Ov(o);let u;this.TRACE_INIT("analyzeTokenTypes",()=>{u=_5e(o,{lineTerminatorCharacters:this.config.lineTerminatorCharacters,positionTracking:t.positionTracking,ensureOptimizations:t.ensureOptimizations,safeMode:t.safeMode,tracer:this.TRACE_INIT})}),this.patternIdxToConfig[l]=u.patternIdxToConfig,this.charCodeToPatternIdxToConfig[l]=u.charCodeToPatternIdxToConfig,this.emptyGroups=tl({},this.emptyGroups,u.emptyGroups),this.hasCustom=u.hasCustom||this.hasCustom,this.canModeBeOptimized[l]=u.canBeOptimized}})}),this.defaultMode=n.defaultMode,!ni(this.lexerDefinitionErrors)&&!this.config.deferDefinitionErrorsHandling){let l=gr(this.lexerDefinitionErrors,u=>u.message).join(`----------------------- +`);throw new Error(`Errors detected in definition of Lexer: +`+l)}Br(this.lexerDefinitionWarning,o=>{Lq(o.message)}),this.TRACE_INIT("Choosing sub-methods implementations",()=>{if(i&&(this.handleModes=Wa),this.trackStartLines===!1&&(this.computeNewColumn=vS),this.trackEndLines===!1&&(this.updateTokenEndLineColumnLocation=Wa),/full/i.test(this.config.positionTracking))this.createTokenInstance=this.createFullToken;else if(/onlyStart/i.test(this.config.positionTracking))this.createTokenInstance=this.createStartOnlyToken;else if(/onlyOffset/i.test(this.config.positionTracking))this.createTokenInstance=this.createOffsetOnlyToken;else throw Error(`Invalid config option: "${this.config.positionTracking}"`);this.hasCustom?(this.addToken=this.addTokenUsingPush,this.handlePayload=this.handlePayloadWithCustom):(this.addToken=this.addTokenUsingMemberAccess,this.handlePayload=this.handlePayloadNoCustom)}),this.TRACE_INIT("Failed Optimization Warnings",()=>{let o=rl(this.canModeBeOptimized,(l,u,h)=>(u===!1&&l.push(h),l),[]);if(t.ensureOptimizations&&!ni(o))throw Error(`Lexer Modes: < ${o.join(", ")} > cannot be optimized. + Disable the "ensureOptimizations" lexer config flag to silently ignore this and run the lexer in an un-optimized mode. + Or inspect the console log for details on how to resolve these issues.`)}),this.TRACE_INIT("clearRegExpParserCache",()=>{k5e()}),this.TRACE_INIT("toFastProperties",()=>{Iq(this)})})}tokenize(e,t=this.defaultMode){if(!ni(this.lexerDefinitionErrors)){let n=gr(this.lexerDefinitionErrors,i=>i.message).join(`----------------------- +`);throw new Error(`Unable to Tokenize because Errors detected in definition of Lexer: +`+n)}return this.tokenizeInternal(e,t)}tokenizeInternal(e,t){let r,n,i,a,o,l,u,h,d,f,p,m,g,y,v,x=e,b=x.length,T=0,w=0,C=this.hasCustom?0:Math.floor(e.length/10),S=new Array(C),k=[],R=this.trackStartLines?1:void 0,M=this.trackStartLines?1:void 0,P=Y5e(this.emptyGroups),N=this.trackStartLines,_=this.config.lineTerminatorsPattern,A=0,I=[],D=[],L=[],B=[];Object.freeze(B);let O=!1,$=E(H=>{if(L.length===1&&H.tokenType.PUSH_MODE===void 0){let K=this.config.errorMessageProvider.buildUnableToPopLexerModeMessage(H);k.push({offset:H.startOffset,line:H.startLine,column:H.startColumn,length:H.image.length,message:K})}else{L.pop();let K=Xg(L);I=this.patternIdxToConfig[K],D=this.charCodeToPatternIdxToConfig[K],A=I.length;let J=this.canModeBeOptimized[K]&&this.config.safeMode===!1;D&&J?O=!0:O=!1}},"pop_mode");function W(H){L.push(H),D=this.charCodeToPatternIdxToConfig[H],I=this.patternIdxToConfig[H],A=I.length,A=I.length;let K=this.canModeBeOptimized[H]&&this.config.safeMode===!1;D&&K?O=!0:O=!1}s(W,"push_mode"),E(W,"push_mode"),W.call(this,t);let V,z=this.config.recoveryEnabled;for(;Tl.length){l=a,d=a.length,u=h,V=te;break}}}break}}if(d!==-1){if(f=V.group,f!==void 0&&(l=l!==null?l:e.substring(T,T+d),p=V.tokenTypeIdx,m=this.createTokenInstance(l,T,p,V.tokenType,R,M,d),this.handlePayload(m,u),f===!1?w=this.addToken(S,w,m):P[f].push(m)),N===!0&&V.canLineTerminator===!0){let ne=0,Z,pe;_.lastIndex=0;do l=l!==null?l:e.substring(T,T+d),Z=_.test(l),Z===!0&&(pe=_.lastIndex-1,ne++);while(Z===!0);ne!==0?(R=R+ne,M=d-pe,this.updateTokenEndLineColumnLocation(m,f,pe,ne,R,M,d)):M=this.computeNewColumn(M,d)}else M=this.computeNewColumn(M,d);T=T+d,this.handleModes(V,$,W,m)}else{let ne=T,Z=R,pe=M,te=z===!1;for(;te===!1&&T ${Ug(e)} <--`:`token of type --> ${e.name} <--`} but found --> '${t.image}' <--`},buildNotAllInputParsedMessage({firstRedundant:e,ruleName:t}){return"Redundant input, expecting EOF but found: "+e.image},buildNoViableAltMessage({expectedPathsPerAlt:e,actual:t,previous:r,customUserDescription:n,ruleName:i}){let a="Expecting: ",l=` +but found: '`+Ac(t).image+"'";if(n)return a+n+l;{let u=rl(e,(p,m)=>p.concat(m),[]),h=gr(u,p=>`[${gr(p,m=>Ug(m)).join(", ")}]`),f=`one of these possible Token sequences: +${gr(h,(p,m)=>` ${m+1}. ${p}`).join(` +`)}`;return a+f+l}},buildEarlyExitMessage({expectedIterationPaths:e,actual:t,customUserDescription:r,ruleName:n}){let i="Expecting: ",o=` +but found: '`+Ac(t).image+"'";if(r)return i+r+o;{let u=`expecting at least one iteration which starts with one of these possible Token sequences:: + <${gr(e,h=>`[${gr(h,d=>Ug(d)).join(",")}]`).join(" ,")}>`;return i+u+o}}};Object.freeze(hv);$2t={buildRuleNotFoundError(e,t){return"Invalid grammar, reference to a rule which is not defined: ->"+t.nonTerminalName+`<- +inside top level rule: ->`+e.name+"<-"}},Wg={buildDuplicateFoundError(e,t){function r(d){return d instanceof fi?d.terminalType.name:d instanceof Zs?d.nonTerminalName:""}s(r,"getExtraProductionArgument2"),E(r,"getExtraProductionArgument");let n=e.name,i=Ac(t),a=i.idx,o=bc(i),l=r(i),u=a>0,h=`->${o}${u?a:""}<- ${l?`with argument: ->${l}<-`:""} + appears more than once (${t.length} times) in the top level rule: ->${n}<-. + For further details see: https://chevrotain.io/docs/FAQ.html#NUMERICAL_SUFFIXES + `;return h=h.replace(/[ \t]+/g," "),h=h.replace(/\s\s+/g,` +`),h},buildNamespaceConflictError(e){return`Namespace conflict found in grammar. +The grammar has both a Terminal(Token) and a Non-Terminal(Rule) named: <${e.name}>. +To resolve this make sure each Terminal and Non-Terminal names are unique +This is easy to accomplish by using the convention that Terminal names start with an uppercase letter +and Non-Terminal names start with a lower case letter.`},buildAlternationPrefixAmbiguityError(e){let t=gr(e.prefixPath,i=>Ug(i)).join(", "),r=e.alternation.idx===0?"":e.alternation.idx;return`Ambiguous alternatives: <${e.ambiguityIndices.join(" ,")}> due to common lookahead prefix +in inside <${e.topLevelRule.name}> Rule, +<${t}> may appears as a prefix path in all these alternatives. +See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#COMMON_PREFIX +For Further details.`},buildAlternationAmbiguityError(e){let t=e.alternation.idx===0?"":e.alternation.idx,r=e.prefixPath.length===0,n=`Ambiguous Alternatives Detected: <${e.ambiguityIndices.join(" ,")}> in inside <${e.topLevelRule.name}> Rule, +`;if(r)n+=`These alternatives are all empty (match no tokens), making them indistinguishable. +Only the last alternative may be empty. +`;else{let i=gr(e.prefixPath,a=>Ug(a)).join(", ");n+=`<${i}> may appears as a prefix path in all these alternatives. +`}return n+=`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#AMBIGUOUS_ALTERNATIVES +For Further details.`,n},buildEmptyRepetitionError(e){let t=bc(e.repetition);return e.repetition.idx!==0&&(t+=e.repetition.idx),`The repetition <${t}> within Rule <${e.topLevelRule.name}> can never consume any tokens. +This could lead to an infinite loop.`},buildTokenNameError(e){return"deprecated"},buildEmptyAlternationError(e){return`Ambiguous empty alternative: <${e.emptyChoiceIdx+1}> in inside <${e.topLevelRule.name}> Rule. +Only the last alternative may be an empty alternative.`},buildTooManyAlternativesError(e){return`An Alternation cannot have more than 256 alternatives: + inside <${e.topLevelRule.name}> Rule. + has ${e.alternation.definition.length+1} alternatives.`},buildLeftRecursionError(e){let t=e.topLevelRule.name,r=gr(e.leftRecursionPath,a=>a.name),n=`${t} --> ${r.concat([t]).join(" --> ")}`;return`Left Recursion found in grammar. +rule: <${t}> can be invoked from itself (directly or indirectly) +without consuming any Tokens. The grammar path that causes this is: + ${n} + To fix this refactor your grammar to remove the left recursion. +see: https://en.wikipedia.org/wiki/LL_parser#Left_factoring.`},buildInvalidRuleNameError(e){return"deprecated"},buildDuplicateRuleNameError(e){let t;return e.topLevelRule instanceof Iv?t=e.topLevelRule.name:t=e.topLevelRule,`Duplicate definition, rule: ->${t}<- is already defined in the grammar: ->${e.grammarName}<-`}};s(sAe,"resolveGrammar");E(sAe,"resolveGrammar");F2t=class extends Mv{static{s(this,"GastRefResolverVisitor")}static{E(this,"GastRefResolverVisitor")}constructor(e,t){super(),this.nameToTopRule=e,this.errMsgProvider=t,this.errors=[]}resolveRefs(){Br(xa(this.nameToTopRule),e=>{this.currTopLevel=e,e.accept(this)})}visitNonTerminal(e){let t=this.nameToTopRule[e.nonTerminalName];if(t)e.referencedRule=t;else{let r=this.errMsgProvider.buildRuleNotFoundError(this.currTopLevel,e);this.errors.push({message:r,type:Qs.UNRESOLVED_SUBRULE_REF,ruleName:this.currTopLevel.name,unresolvedRefName:e.nonTerminalName})}}},G2t=class extends I6{static{s(this,"AbstractNextPossibleTokensWalker")}static{E(this,"AbstractNextPossibleTokensWalker")}constructor(e,t){super(),this.topProd=e,this.path=t,this.possibleTokTypes=[],this.nextProductionName="",this.nextProductionOccurrence=0,this.found=!1,this.isAtEndOfPath=!1}startWalking(){if(this.found=!1,this.path.ruleStack[0]!==this.topProd.name)throw Error("The path does not start with the walker's top Rule!");return this.ruleStack=ss(this.path.ruleStack).reverse(),this.occurrenceStack=ss(this.path.occurrenceStack).reverse(),this.ruleStack.pop(),this.occurrenceStack.pop(),this.updateExpectedNext(),this.walk(this.topProd),this.possibleTokTypes}walk(e,t=[]){this.found||super.walk(e,t)}walkProdRef(e,t,r){if(e.referencedRule.name===this.nextProductionName&&e.idx===this.nextProductionOccurrence){let n=t.concat(r);this.updateExpectedNext(),this.walk(e.referencedRule,n)}}updateExpectedNext(){ni(this.ruleStack)?(this.nextProductionName="",this.nextProductionOccurrence=0,this.isAtEndOfPath=!0):(this.nextProductionName=this.ruleStack.pop(),this.nextProductionOccurrence=this.occurrenceStack.pop())}},z2t=class extends G2t{static{s(this,"NextAfterTokenWalker")}static{E(this,"NextAfterTokenWalker")}constructor(e,t){super(e,t),this.path=t,this.nextTerminalName="",this.nextTerminalOccurrence=0,this.nextTerminalName=this.path.lastTok.name,this.nextTerminalOccurrence=this.path.lastTokOccurrence}walkTerminal(e,t,r){if(this.isAtEndOfPath&&e.terminalType.name===this.nextTerminalName&&e.idx===this.nextTerminalOccurrence&&!this.found){let n=t.concat(r),i=new Eo({definition:n});this.possibleTokTypes=Nv(i),this.found=!0}}},N6=class extends I6{static{s(this,"AbstractNextTerminalAfterProductionWalker")}static{E(this,"AbstractNextTerminalAfterProductionWalker")}constructor(e,t){super(),this.topRule=e,this.occurrence=t,this.result={token:void 0,occurrence:void 0,isEndOfRule:void 0}}startWalking(){return this.walk(this.topRule),this.result}},V2t=class extends N6{static{s(this,"NextTerminalAfterManyWalker")}static{E(this,"NextTerminalAfterManyWalker")}walkMany(e,t,r){if(e.idx===this.occurrence){let n=Ac(t.concat(r));this.result.isEndOfRule=n===void 0,n instanceof fi&&(this.result.token=n.terminalType,this.result.occurrence=n.idx)}else super.walkMany(e,t,r)}},TSe=class extends N6{static{s(this,"NextTerminalAfterManySepWalker")}static{E(this,"NextTerminalAfterManySepWalker")}walkManySep(e,t,r){if(e.idx===this.occurrence){let n=Ac(t.concat(r));this.result.isEndOfRule=n===void 0,n instanceof fi&&(this.result.token=n.terminalType,this.result.occurrence=n.idx)}else super.walkManySep(e,t,r)}},W2t=class extends N6{static{s(this,"NextTerminalAfterAtLeastOneWalker")}static{E(this,"NextTerminalAfterAtLeastOneWalker")}walkAtLeastOne(e,t,r){if(e.idx===this.occurrence){let n=Ac(t.concat(r));this.result.isEndOfRule=n===void 0,n instanceof fi&&(this.result.token=n.terminalType,this.result.occurrence=n.idx)}else super.walkAtLeastOne(e,t,r)}},CSe=class extends N6{static{s(this,"NextTerminalAfterAtLeastOneSepWalker")}static{E(this,"NextTerminalAfterAtLeastOneSepWalker")}walkAtLeastOneSep(e,t,r){if(e.idx===this.occurrence){let n=Ac(t.concat(r));this.result.isEndOfRule=n===void 0,n instanceof fi&&(this.result.token=n.terminalType,this.result.occurrence=n.idx)}else super.walkAtLeastOneSep(e,t,r)}};s(BR,"possiblePathsFrom");E(BR,"possiblePathsFrom");s(Gq,"nextPossibleTokensAfter");E(Gq,"nextPossibleTokensAfter");s(oAe,"expandTopLevelRule");E(oAe,"expandTopLevelRule");(function(e){e[e.OPTION=0]="OPTION",e[e.REPETITION=1]="REPETITION",e[e.REPETITION_MANDATORY=2]="REPETITION_MANDATORY",e[e.REPETITION_MANDATORY_WITH_SEPARATOR=3]="REPETITION_MANDATORY_WITH_SEPARATOR",e[e.REPETITION_WITH_SEPARATOR=4]="REPETITION_WITH_SEPARATOR",e[e.ALTERNATION=5]="ALTERNATION"})(Ri||(Ri={}));s(P6,"getProdType");E(P6,"getProdType");s(Gz,"getLookaheadPaths");E(Gz,"getLookaheadPaths");s(lAe,"buildLookaheadFuncForOr");E(lAe,"buildLookaheadFuncForOr");s(cAe,"buildLookaheadFuncForOptionalProd");E(cAe,"buildLookaheadFuncForOptionalProd");s(uAe,"buildAlternativesLookAheadFunc");E(uAe,"buildAlternativesLookAheadFunc");s(hAe,"buildSingleAlternativeLookaheadFunction");E(hAe,"buildSingleAlternativeLookaheadFunction");q2t=class extends I6{static{s(this,"RestDefinitionFinderWalker")}static{E(this,"RestDefinitionFinderWalker")}constructor(e,t,r){super(),this.topProd=e,this.targetOccurrence=t,this.targetProdType=r}startWalking(){return this.walk(this.topProd),this.restDef}checkIsTarget(e,t,r,n){return e.idx===this.targetOccurrence&&this.targetProdType===t?(this.restDef=r.concat(n),!0):!1}walkOption(e,t,r){this.checkIsTarget(e,Ri.OPTION,t,r)||super.walkOption(e,t,r)}walkAtLeastOne(e,t,r){this.checkIsTarget(e,Ri.REPETITION_MANDATORY,t,r)||super.walkOption(e,t,r)}walkAtLeastOneSep(e,t,r){this.checkIsTarget(e,Ri.REPETITION_MANDATORY_WITH_SEPARATOR,t,r)||super.walkOption(e,t,r)}walkMany(e,t,r){this.checkIsTarget(e,Ri.REPETITION,t,r)||super.walkOption(e,t,r)}walkManySep(e,t,r){this.checkIsTarget(e,Ri.REPETITION_WITH_SEPARATOR,t,r)||super.walkOption(e,t,r)}},dAe=class extends Mv{static{s(this,"InsideDefinitionFinderVisitor")}static{E(this,"InsideDefinitionFinderVisitor")}constructor(e,t,r){super(),this.targetOccurrence=e,this.targetProdType=t,this.targetRef=r,this.result=[]}checkIsTarget(e,t){e.idx===this.targetOccurrence&&this.targetProdType===t&&(this.targetRef===void 0||e===this.targetRef)&&(this.result=e.definition)}visitOption(e){this.checkIsTarget(e,Ri.OPTION)}visitRepetition(e){this.checkIsTarget(e,Ri.REPETITION)}visitRepetitionMandatory(e){this.checkIsTarget(e,Ri.REPETITION_MANDATORY)}visitRepetitionMandatoryWithSeparator(e){this.checkIsTarget(e,Ri.REPETITION_MANDATORY_WITH_SEPARATOR)}visitRepetitionWithSeparator(e){this.checkIsTarget(e,Ri.REPETITION_WITH_SEPARATOR)}visitAlternation(e){this.checkIsTarget(e,Ri.ALTERNATION)}};s(zz,"initializeArrayOfArrays");E(zz,"initializeArrayOfArrays");s(jA,"pathToHashKeys");E(jA,"pathToHashKeys");s(fAe,"isUniquePrefixHash");E(fAe,"isUniquePrefixHash");s(zq,"lookAheadSequenceFromAlternatives");E(zq,"lookAheadSequenceFromAlternatives");s(AS,"getLookaheadPathsForOr");E(AS,"getLookaheadPathsForOr");s(RS,"getLookaheadPathsForOptionalProd");E(RS,"getLookaheadPathsForOptionalProd");s($R,"containsPath");E($R,"containsPath");s(pAe,"isStrictPrefixOfPath");E(pAe,"isStrictPrefixOfPath");s(Vq,"areTokenCategoriesNotUsed");E(Vq,"areTokenCategoriesNotUsed");s(mAe,"validateLookahead");E(mAe,"validateLookahead");s(gAe,"validateGrammar");E(gAe,"validateGrammar");s(yAe,"validateDuplicateProductions");E(yAe,"validateDuplicateProductions");s(vAe,"identifyProductionForDuplicates");E(vAe,"identifyProductionForDuplicates");s(Wq,"getExtraProductionArgument");E(Wq,"getExtraProductionArgument");H2t=class extends Mv{static{s(this,"OccurrenceValidationCollector")}static{E(this,"OccurrenceValidationCollector")}constructor(){super(...arguments),this.allProductions=[]}visitNonTerminal(e){this.allProductions.push(e)}visitOption(e){this.allProductions.push(e)}visitRepetitionWithSeparator(e){this.allProductions.push(e)}visitRepetitionMandatory(e){this.allProductions.push(e)}visitRepetitionMandatoryWithSeparator(e){this.allProductions.push(e)}visitRepetition(e){this.allProductions.push(e)}visitAlternation(e){this.allProductions.push(e)}visitTerminal(e){this.allProductions.push(e)}};s(xAe,"validateRuleDoesNotAlreadyExist");E(xAe,"validateRuleDoesNotAlreadyExist");s(bAe,"validateRuleIsOverridden");E(bAe,"validateRuleIsOverridden");s(qq,"validateNoLeftRecursion");E(qq,"validateNoLeftRecursion");s(FC,"getFirstNoneTerminal");E(FC,"getFirstNoneTerminal");Hq=class extends Mv{static{s(this,"OrCollector")}static{E(this,"OrCollector")}constructor(){super(...arguments),this.alternations=[]}visitAlternation(e){this.alternations.push(e)}};s(TAe,"validateEmptyOrAlternative");E(TAe,"validateEmptyOrAlternative");s(CAe,"validateAmbiguousAlternationAlternatives");E(CAe,"validateAmbiguousAlternationAlternatives");U2t=class extends Mv{static{s(this,"RepetitionCollector")}static{E(this,"RepetitionCollector")}constructor(){super(...arguments),this.allProductions=[]}visitRepetitionWithSeparator(e){this.allProductions.push(e)}visitRepetitionMandatory(e){this.allProductions.push(e)}visitRepetitionMandatoryWithSeparator(e){this.allProductions.push(e)}visitRepetition(e){this.allProductions.push(e)}};s(SAe,"validateTooManyAlts");E(SAe,"validateTooManyAlts");s(wAe,"validateSomeNonEmptyLookaheadPath");E(wAe,"validateSomeNonEmptyLookaheadPath");s(kAe,"checkAlternativesAmbiguities");E(kAe,"checkAlternativesAmbiguities");s(EAe,"checkPrefixAlternativesAmbiguities");E(EAe,"checkPrefixAlternativesAmbiguities");s(AAe,"checkTerminalAndNoneTerminalsNameSpace");E(AAe,"checkTerminalAndNoneTerminalsNameSpace");s(RAe,"resolveGrammar2");E(RAe,"resolveGrammar");s(_Ae,"validateGrammar2");E(_Ae,"validateGrammar");LAe="MismatchedTokenException",DAe="NoViableAltException",IAe="EarlyExitException",MAe="NotAllInputParsedException",NAe=[LAe,DAe,IAe,MAe];Object.freeze(NAe);s(tS,"isRecognitionException");E(tS,"isRecognitionException");O6=class extends Error{static{s(this,"RecognitionException")}static{E(this,"RecognitionException")}constructor(e,t){super(e),this.token=t,this.resyncedTokens=[],Object.setPrototypeOf(this,new.target.prototype),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},PAe=class extends O6{static{s(this,"MismatchedTokenException")}static{E(this,"MismatchedTokenException")}constructor(e,t,r){super(e,t),this.previousToken=r,this.name=LAe}},Y2t=class extends O6{static{s(this,"NoViableAltException")}static{E(this,"NoViableAltException")}constructor(e,t,r){super(e,t),this.previousToken=r,this.name=DAe}},j2t=class extends O6{static{s(this,"NotAllInputParsedException")}static{E(this,"NotAllInputParsedException")}constructor(e,t){super(e,t),this.name=MAe}},X2t=class extends O6{static{s(this,"EarlyExitException")}static{E(this,"EarlyExitException")}constructor(e,t,r){super(e,t),this.previousToken=r,this.name=IAe}},uG={},OAe="InRuleRecoveryException",K2t=class extends Error{static{s(this,"InRuleRecoveryException")}static{E(this,"InRuleRecoveryException")}constructor(e){super(e),this.name=OAe}},Z2t=class{static{s(this,"Recoverable")}static{E(this,"Recoverable")}initRecoverable(e){this.firstAfterRepMap={},this.resyncFollows={},this.recoveryEnabled=Rr(e,"recoveryEnabled")?e.recoveryEnabled:ed.recoveryEnabled,this.recoveryEnabled&&(this.attemptInRepetitionRecovery=BAe)}getTokenToInsert(e){let t=ES(e,"",NaN,NaN,NaN,NaN,NaN,NaN);return t.isInsertedInRecovery=!0,t}canTokenTypeBeInsertedInRecovery(e){return!0}canTokenTypeBeDeletedInRecovery(e){return!0}tryInRepetitionRecovery(e,t,r,n){let i=this.findReSyncTokenType(),a=this.exportLexerState(),o=[],l=!1,u=this.LA(1),h=this.LA(1),d=E(()=>{let f=this.LA(0),p=this.errorMessageProvider.buildMismatchTokenMessage({expected:n,actual:u,previous:f,ruleName:this.getCurrRuleFullName()}),m=new PAe(p,u,this.LA(0));m.resyncedTokens=QC(o),this.SAVE_ERROR(m)},"generateErrorMessage");for(;!l;)if(this.tokenMatcher(h,n)){d();return}else if(r.call(this)){d(),e.apply(this,t);return}else this.tokenMatcher(h,i)?l=!0:(h=this.SKIP_TOKEN(),this.addToResyncTokens(h,o));this.importLexerState(a)}shouldInRepetitionRecoveryBeTried(e,t,r){return!(r===!1||this.tokenMatcher(this.LA(1),e)||this.isBackTracking()||this.canPerformInRuleRecovery(e,this.getFollowsForInRuleRecovery(e,t)))}getFollowsForInRuleRecovery(e,t){let r=this.getCurrentGrammarPath(e,t);return this.getNextPossibleTokenTypes(r)}tryInRuleRecovery(e,t){if(this.canRecoverWithSingleTokenInsertion(e,t))return this.getTokenToInsert(e);if(this.canRecoverWithSingleTokenDeletion(e)){let r=this.SKIP_TOKEN();return this.consumeToken(),r}throw new K2t("sad sad panda")}canPerformInRuleRecovery(e,t){return this.canRecoverWithSingleTokenInsertion(e,t)||this.canRecoverWithSingleTokenDeletion(e)}canRecoverWithSingleTokenInsertion(e,t){if(!this.canTokenTypeBeInsertedInRecovery(e)||ni(t))return!1;let r=this.LA(1);return Ev(t,i=>this.tokenMatcher(r,i))!==void 0}canRecoverWithSingleTokenDeletion(e){return this.canTokenTypeBeDeletedInRecovery(e)?this.tokenMatcher(this.LA(2),e):!1}isInCurrentRuleReSyncSet(e){let t=this.getCurrFollowKey(),r=this.getFollowSetFromFollowKey(t);return eo(r,e)}findReSyncTokenType(){let e=this.flattenFollowSet(),t=this.LA(1),r=2;for(;;){let n=Ev(e,i=>Fq(t,i));if(n!==void 0)return n;t=this.LA(r),r++}}getCurrFollowKey(){if(this.RULE_STACK.length===1)return uG;let e=this.getLastExplicitRuleShortName(),t=this.getLastExplicitRuleOccurrenceIndex(),r=this.getPreviousExplicitRuleShortName();return{ruleName:this.shortRuleNameToFullName(e),idxInCallingRule:t,inRule:this.shortRuleNameToFullName(r)}}buildFullFollowKeyStack(){let e=this.RULE_STACK,t=this.RULE_OCCURRENCE_STACK;return gr(e,(r,n)=>n===0?uG:{ruleName:this.shortRuleNameToFullName(r),idxInCallingRule:t[n],inRule:this.shortRuleNameToFullName(e[n-1])})}flattenFollowSet(){let e=gr(this.buildFullFollowKeyStack(),t=>this.getFollowSetFromFollowKey(t));return wc(e)}getFollowSetFromFollowKey(e){if(e===uG)return[gp];let t=e.ruleName+e.idxInCallingRule+C5e+e.inRule;return this.resyncFollows[t]}addToResyncTokens(e,t){return this.tokenMatcher(e,gp)||t.push(e),t}reSyncTo(e){let t=[],r=this.LA(1);for(;this.tokenMatcher(r,e)===!1;)r=this.SKIP_TOKEN(),this.addToResyncTokens(r,t);return QC(t)}attemptInRepetitionRecovery(e,t,r,n,i,a,o){}getCurrentGrammarPath(e,t){let r=this.getHumanReadableRuleStack(),n=ss(this.RULE_OCCURRENCE_STACK);return{ruleStack:r,occurrenceStack:n,lastTok:e,lastTokOccurrence:t}}getHumanReadableRuleStack(){return gr(this.RULE_STACK,e=>this.shortRuleNameToFullName(e))}};s(BAe,"attemptInRepetitionRecovery");E(BAe,"attemptInRepetitionRecovery");Q2t=4,Cp=8,J2t=8,$Ae=1<qq(t,t,Wg))}validateEmptyOrAlternatives(e){return Ml(e,t=>TAe(t,Wg))}validateAmbiguousAlternationAlternatives(e,t){return Ml(e,r=>CAe(r,t,Wg))}validateSomeNonEmptyLookaheadPath(e,t){return wAe(e,t,Wg)}buildLookaheadForAlternation(e){return lAe(e.prodOccurrence,e.rule,e.maxLookahead,e.hasPredicates,e.dynamicTokensEnabled,uAe)}buildLookaheadForOptional(e){return cAe(e.prodOccurrence,e.rule,e.maxLookahead,e.dynamicTokensEnabled,P6(e.prodType),hAe)}},eTt=class{static{s(this,"LooksAhead")}static{E(this,"LooksAhead")}initLooksAhead(e){this.dynamicTokensEnabled=Rr(e,"dynamicTokensEnabled")?e.dynamicTokensEnabled:ed.dynamicTokensEnabled,this.maxLookahead=Rr(e,"maxLookahead")?e.maxLookahead:ed.maxLookahead,this.lookaheadStrategy=Rr(e,"lookaheadStrategy")?e.lookaheadStrategy:new Uq({maxLookahead:this.maxLookahead}),this.lookAheadFuncsCache=new Map}preComputeLookaheadFunctions(e){Br(e,t=>{this.TRACE_INIT(`${t.name} Rule Lookahead`,()=>{let{alternation:r,repetition:n,option:i,repetitionMandatory:a,repetitionMandatoryWithSeparator:o,repetitionWithSeparator:l}=GAe(t);Br(r,u=>{let h=u.idx===0?"":u.idx;this.TRACE_INIT(`${bc(u)}${h}`,()=>{let d=this.lookaheadStrategy.buildLookaheadForAlternation({prodOccurrence:u.idx,rule:t,maxLookahead:u.maxLookahead||this.maxLookahead,hasPredicates:u.hasPredicates,dynamicTokensEnabled:this.dynamicTokensEnabled}),f=KA(this.fullRuleNameToShort[t.name],$Ae,u.idx);this.setLaFuncCache(f,d)})}),Br(n,u=>{this.computeLookaheadFunc(t,u.idx,Vz,"Repetition",u.maxLookahead,bc(u))}),Br(i,u=>{this.computeLookaheadFunc(t,u.idx,FAe,"Option",u.maxLookahead,bc(u))}),Br(a,u=>{this.computeLookaheadFunc(t,u.idx,Wz,"RepetitionMandatory",u.maxLookahead,bc(u))}),Br(o,u=>{this.computeLookaheadFunc(t,u.idx,XA,"RepetitionMandatoryWithSeparator",u.maxLookahead,bc(u))}),Br(l,u=>{this.computeLookaheadFunc(t,u.idx,qz,"RepetitionWithSeparator",u.maxLookahead,bc(u))})})})}computeLookaheadFunc(e,t,r,n,i,a){this.TRACE_INIT(`${a}${t===0?"":t}`,()=>{let o=this.lookaheadStrategy.buildLookaheadForOptional({prodOccurrence:t,rule:e,maxLookahead:i||this.maxLookahead,dynamicTokensEnabled:this.dynamicTokensEnabled,prodType:n}),l=KA(this.fullRuleNameToShort[e.name],r,t);this.setLaFuncCache(l,o)})}getKeyForAutomaticLookahead(e,t){let r=this.getLastExplicitRuleShortName();return KA(r,e,t)}getLaFuncFromCache(e){return this.lookAheadFuncsCache.get(e)}setLaFuncCache(e,t){this.lookAheadFuncsCache.set(e,t)}},tTt=class extends Mv{static{s(this,"DslMethodsCollectorVisitor")}static{E(this,"DslMethodsCollectorVisitor")}constructor(){super(...arguments),this.dslMethods={option:[],alternation:[],repetition:[],repetitionWithSeparator:[],repetitionMandatory:[],repetitionMandatoryWithSeparator:[]}}reset(){this.dslMethods={option:[],alternation:[],repetition:[],repetitionWithSeparator:[],repetitionMandatory:[],repetitionMandatoryWithSeparator:[]}}visitOption(e){this.dslMethods.option.push(e)}visitRepetitionWithSeparator(e){this.dslMethods.repetitionWithSeparator.push(e)}visitRepetitionMandatory(e){this.dslMethods.repetitionMandatory.push(e)}visitRepetitionMandatoryWithSeparator(e){this.dslMethods.repetitionMandatoryWithSeparator.push(e)}visitRepetition(e){this.dslMethods.repetition.push(e)}visitAlternation(e){this.dslMethods.alternation.push(e)}},bA=new tTt;s(GAe,"collectMethods");E(GAe,"collectMethods");s(Hz,"setNodeLocationOnlyOffset");E(Hz,"setNodeLocationOnlyOffset");s(Uz,"setNodeLocationFull");E(Uz,"setNodeLocationFull");s(zAe,"addTerminalToCst");E(zAe,"addTerminalToCst");s(VAe,"addNoneTerminalToCst");E(VAe,"addNoneTerminalToCst");rTt="name";s(Yq,"defineNameProp");E(Yq,"defineNameProp");s(WAe,"defaultVisit");E(WAe,"defaultVisit");s(qAe,"createBaseSemanticVisitorConstructor");E(qAe,"createBaseSemanticVisitorConstructor");s(HAe,"createBaseVisitorConstructorWithDefaults");E(HAe,"createBaseVisitorConstructorWithDefaults");(function(e){e[e.REDUNDANT_METHOD=0]="REDUNDANT_METHOD",e[e.MISSING_METHOD=1]="MISSING_METHOD"})(Yz||(Yz={}));s(UAe,"validateVisitor");E(UAe,"validateVisitor");s(YAe,"validateMissingCstMethods");E(YAe,"validateMissingCstMethods");nTt=class{static{s(this,"TreeBuilder")}static{E(this,"TreeBuilder")}initTreeBuilder(e){if(this.CST_STACK=[],this.outputCst=e.outputCst,this.nodeLocationTracking=Rr(e,"nodeLocationTracking")?e.nodeLocationTracking:ed.nodeLocationTracking,!this.outputCst)this.cstInvocationStateUpdate=Wa,this.cstFinallyStateUpdate=Wa,this.cstPostTerminal=Wa,this.cstPostNonTerminal=Wa,this.cstPostRule=Wa;else if(/full/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=Uz,this.setNodeLocationFromNode=Uz,this.cstPostRule=Wa,this.setInitialNodeLocation=this.setInitialNodeLocationFullRecovery):(this.setNodeLocationFromToken=Wa,this.setNodeLocationFromNode=Wa,this.cstPostRule=this.cstPostRuleFull,this.setInitialNodeLocation=this.setInitialNodeLocationFullRegular);else if(/onlyOffset/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=Hz,this.setNodeLocationFromNode=Hz,this.cstPostRule=Wa,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRecovery):(this.setNodeLocationFromToken=Wa,this.setNodeLocationFromNode=Wa,this.cstPostRule=this.cstPostRuleOnlyOffset,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRegular);else if(/none/i.test(this.nodeLocationTracking))this.setNodeLocationFromToken=Wa,this.setNodeLocationFromNode=Wa,this.cstPostRule=Wa,this.setInitialNodeLocation=Wa;else throw Error(`Invalid config option: "${e.nodeLocationTracking}"`)}setInitialNodeLocationOnlyOffsetRecovery(e){e.location={startOffset:NaN,endOffset:NaN}}setInitialNodeLocationOnlyOffsetRegular(e){e.location={startOffset:this.LA(1).startOffset,endOffset:NaN}}setInitialNodeLocationFullRecovery(e){e.location={startOffset:NaN,startLine:NaN,startColumn:NaN,endOffset:NaN,endLine:NaN,endColumn:NaN}}setInitialNodeLocationFullRegular(e){let t=this.LA(1);e.location={startOffset:t.startOffset,startLine:t.startLine,startColumn:t.startColumn,endOffset:NaN,endLine:NaN,endColumn:NaN}}cstInvocationStateUpdate(e){let t={name:e,children:Object.create(null)};this.setInitialNodeLocation(t),this.CST_STACK.push(t)}cstFinallyStateUpdate(){this.CST_STACK.pop()}cstPostRuleFull(e){let t=this.LA(0),r=e.location;r.startOffset<=t.startOffset?(r.endOffset=t.endOffset,r.endLine=t.endLine,r.endColumn=t.endColumn):(r.startOffset=NaN,r.startLine=NaN,r.startColumn=NaN)}cstPostRuleOnlyOffset(e){let t=this.LA(0),r=e.location;r.startOffset<=t.startOffset?r.endOffset=t.endOffset:r.startOffset=NaN}cstPostTerminal(e,t){let r=this.CST_STACK[this.CST_STACK.length-1];zAe(r,t,e),this.setNodeLocationFromToken(r.location,t)}cstPostNonTerminal(e,t){let r=this.CST_STACK[this.CST_STACK.length-1];VAe(r,t,e),this.setNodeLocationFromNode(r.location,e.location)}getBaseCstVisitorConstructor(){if(Qh(this.baseCstVisitorConstructor)){let e=qAe(this.className,el(this.gastProductionsCache));return this.baseCstVisitorConstructor=e,e}return this.baseCstVisitorConstructor}getBaseCstVisitorConstructorWithDefaults(){if(Qh(this.baseCstVisitorWithDefaultsConstructor)){let e=HAe(this.className,el(this.gastProductionsCache),this.getBaseCstVisitorConstructor());return this.baseCstVisitorWithDefaultsConstructor=e,e}return this.baseCstVisitorWithDefaultsConstructor}getLastExplicitRuleShortName(){let e=this.RULE_STACK;return e[e.length-1]}getPreviousExplicitRuleShortName(){let e=this.RULE_STACK;return e[e.length-2]}getLastExplicitRuleOccurrenceIndex(){let e=this.RULE_OCCURRENCE_STACK;return e[e.length-1]}},iTt=class{static{s(this,"LexerAdapter")}static{E(this,"LexerAdapter")}initLexerAdapter(){this.tokVector=[],this.tokVectorLength=0,this.currIdx=-1}set input(e){if(this.selfAnalysisDone!==!0)throw Error("Missing invocation at the end of the Parser's constructor.");this.reset(),this.tokVector=e,this.tokVectorLength=e.length}get input(){return this.tokVector}SKIP_TOKEN(){return this.currIdx<=this.tokVector.length-2?(this.consumeToken(),this.LA(1)):FR}LA(e){let t=this.currIdx+e;return t<0||this.tokVectorLength<=t?FR:this.tokVector[t]}consumeToken(){this.currIdx++}exportLexerState(){return this.currIdx}importLexerState(e){this.currIdx=e}resetLexerState(){this.currIdx=-1}moveToTerminatedState(){this.currIdx=this.tokVector.length-1}getLexerPosition(){return this.exportLexerState()}},aTt=class{static{s(this,"RecognizerApi")}static{E(this,"RecognizerApi")}ACTION(e){return e.call(this)}consume(e,t,r){return this.consumeInternal(t,e,r)}subrule(e,t,r){return this.subruleInternal(t,e,r)}option(e,t){return this.optionInternal(t,e)}or(e,t){return this.orInternal(t,e)}many(e,t){return this.manyInternal(e,t)}atLeastOne(e,t){return this.atLeastOneInternal(e,t)}CONSUME(e,t){return this.consumeInternal(e,0,t)}CONSUME1(e,t){return this.consumeInternal(e,1,t)}CONSUME2(e,t){return this.consumeInternal(e,2,t)}CONSUME3(e,t){return this.consumeInternal(e,3,t)}CONSUME4(e,t){return this.consumeInternal(e,4,t)}CONSUME5(e,t){return this.consumeInternal(e,5,t)}CONSUME6(e,t){return this.consumeInternal(e,6,t)}CONSUME7(e,t){return this.consumeInternal(e,7,t)}CONSUME8(e,t){return this.consumeInternal(e,8,t)}CONSUME9(e,t){return this.consumeInternal(e,9,t)}SUBRULE(e,t){return this.subruleInternal(e,0,t)}SUBRULE1(e,t){return this.subruleInternal(e,1,t)}SUBRULE2(e,t){return this.subruleInternal(e,2,t)}SUBRULE3(e,t){return this.subruleInternal(e,3,t)}SUBRULE4(e,t){return this.subruleInternal(e,4,t)}SUBRULE5(e,t){return this.subruleInternal(e,5,t)}SUBRULE6(e,t){return this.subruleInternal(e,6,t)}SUBRULE7(e,t){return this.subruleInternal(e,7,t)}SUBRULE8(e,t){return this.subruleInternal(e,8,t)}SUBRULE9(e,t){return this.subruleInternal(e,9,t)}OPTION(e){return this.optionInternal(e,0)}OPTION1(e){return this.optionInternal(e,1)}OPTION2(e){return this.optionInternal(e,2)}OPTION3(e){return this.optionInternal(e,3)}OPTION4(e){return this.optionInternal(e,4)}OPTION5(e){return this.optionInternal(e,5)}OPTION6(e){return this.optionInternal(e,6)}OPTION7(e){return this.optionInternal(e,7)}OPTION8(e){return this.optionInternal(e,8)}OPTION9(e){return this.optionInternal(e,9)}OR(e){return this.orInternal(e,0)}OR1(e){return this.orInternal(e,1)}OR2(e){return this.orInternal(e,2)}OR3(e){return this.orInternal(e,3)}OR4(e){return this.orInternal(e,4)}OR5(e){return this.orInternal(e,5)}OR6(e){return this.orInternal(e,6)}OR7(e){return this.orInternal(e,7)}OR8(e){return this.orInternal(e,8)}OR9(e){return this.orInternal(e,9)}MANY(e){this.manyInternal(0,e)}MANY1(e){this.manyInternal(1,e)}MANY2(e){this.manyInternal(2,e)}MANY3(e){this.manyInternal(3,e)}MANY4(e){this.manyInternal(4,e)}MANY5(e){this.manyInternal(5,e)}MANY6(e){this.manyInternal(6,e)}MANY7(e){this.manyInternal(7,e)}MANY8(e){this.manyInternal(8,e)}MANY9(e){this.manyInternal(9,e)}MANY_SEP(e){this.manySepFirstInternal(0,e)}MANY_SEP1(e){this.manySepFirstInternal(1,e)}MANY_SEP2(e){this.manySepFirstInternal(2,e)}MANY_SEP3(e){this.manySepFirstInternal(3,e)}MANY_SEP4(e){this.manySepFirstInternal(4,e)}MANY_SEP5(e){this.manySepFirstInternal(5,e)}MANY_SEP6(e){this.manySepFirstInternal(6,e)}MANY_SEP7(e){this.manySepFirstInternal(7,e)}MANY_SEP8(e){this.manySepFirstInternal(8,e)}MANY_SEP9(e){this.manySepFirstInternal(9,e)}AT_LEAST_ONE(e){this.atLeastOneInternal(0,e)}AT_LEAST_ONE1(e){return this.atLeastOneInternal(1,e)}AT_LEAST_ONE2(e){this.atLeastOneInternal(2,e)}AT_LEAST_ONE3(e){this.atLeastOneInternal(3,e)}AT_LEAST_ONE4(e){this.atLeastOneInternal(4,e)}AT_LEAST_ONE5(e){this.atLeastOneInternal(5,e)}AT_LEAST_ONE6(e){this.atLeastOneInternal(6,e)}AT_LEAST_ONE7(e){this.atLeastOneInternal(7,e)}AT_LEAST_ONE8(e){this.atLeastOneInternal(8,e)}AT_LEAST_ONE9(e){this.atLeastOneInternal(9,e)}AT_LEAST_ONE_SEP(e){this.atLeastOneSepFirstInternal(0,e)}AT_LEAST_ONE_SEP1(e){this.atLeastOneSepFirstInternal(1,e)}AT_LEAST_ONE_SEP2(e){this.atLeastOneSepFirstInternal(2,e)}AT_LEAST_ONE_SEP3(e){this.atLeastOneSepFirstInternal(3,e)}AT_LEAST_ONE_SEP4(e){this.atLeastOneSepFirstInternal(4,e)}AT_LEAST_ONE_SEP5(e){this.atLeastOneSepFirstInternal(5,e)}AT_LEAST_ONE_SEP6(e){this.atLeastOneSepFirstInternal(6,e)}AT_LEAST_ONE_SEP7(e){this.atLeastOneSepFirstInternal(7,e)}AT_LEAST_ONE_SEP8(e){this.atLeastOneSepFirstInternal(8,e)}AT_LEAST_ONE_SEP9(e){this.atLeastOneSepFirstInternal(9,e)}RULE(e,t,r=GR){if(eo(this.definedRulesNames,e)){let a={message:Wg.buildDuplicateRuleNameError({topLevelRule:e,grammarName:this.className}),type:Qs.DUPLICATE_RULE_NAME,ruleName:e};this.definitionErrors.push(a)}this.definedRulesNames.push(e);let n=this.defineRule(e,t,r);return this[e]=n,n}OVERRIDE_RULE(e,t,r=GR){let n=bAe(e,this.definedRulesNames,this.className);this.definitionErrors=this.definitionErrors.concat(n);let i=this.defineRule(e,t,r);return this[e]=i,i}BACKTRACK(e,t){return function(){this.isBackTrackingStack.push(1);let r=this.saveRecogState();try{return e.apply(this,t),!0}catch(n){if(tS(n))return!1;throw n}finally{this.reloadRecogState(r),this.isBackTrackingStack.pop()}}}getGAstProductions(){return this.gastProductionsCache}getSerializedGastProductions(){return g5e(xa(this.gastProductionsCache))}},sTt=class{static{s(this,"RecognizerEngine")}static{E(this,"RecognizerEngine")}initRecognizerEngine(e,t){if(this.className=this.constructor.name,this.shortRuleNameToFull={},this.fullRuleNameToShort={},this.ruleShortNameIdx=256,this.tokenMatcher=eS,this.subruleIdx=0,this.definedRulesNames=[],this.tokensMap={},this.isBackTrackingStack=[],this.RULE_STACK=[],this.RULE_OCCURRENCE_STACK=[],this.gastProductionsCache={},Rr(t,"serializedGrammar"))throw Error(`The Parser's configuration can no longer contain a property. + See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_6-0-0 + For Further details.`);if(gn(e)){if(ni(e))throw Error(`A Token Vocabulary cannot be empty. + Note that the first argument for the parser constructor + is no longer a Token vector (since v4.0).`);if(typeof e[0].startOffset=="number")throw Error(`The Parser constructor no longer accepts a token vector as the first argument. + See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_4-0-0 + For Further details.`)}if(gn(e))this.tokensMap=rl(e,(i,a)=>(i[a.name]=a,i),{});else if(Rr(e,"modes")&&kc(wc(xa(e.modes)),iAe)){let i=wc(xa(e.modes)),a=_q(i);this.tokensMap=rl(a,(o,l)=>(o[l.name]=l,o),{})}else if(Pl(e))this.tokensMap=ss(e);else throw new Error(" argument must be An Array of Token constructors, A dictionary of Token constructors or an IMultiModeLexerDefinition");this.tokensMap.EOF=gp;let r=Rr(e,"modes")?wc(xa(e.modes)):xa(e),n=kc(r,i=>ni(i.categoryMatches));this.tokenMatcher=n?eS:Pv,Ov(xa(this.tokensMap))}defineRule(e,t,r){if(this.selfAnalysisDone)throw Error(`Grammar rule <${e}> may not be defined after the 'performSelfAnalysis' method has been called' +Make sure that all grammar rule definitions are done before 'performSelfAnalysis' is called.`);let n=Rr(r,"resyncEnabled")?r.resyncEnabled:GR.resyncEnabled,i=Rr(r,"recoveryValueFunc")?r.recoveryValueFunc:GR.recoveryValueFunc,a=this.ruleShortNameIdx<a.call(this)&&o.call(this),"lookAheadFunc")}}else i=e;if(n.call(this)===!0)return i.call(this)}atLeastOneInternal(e,t){let r=this.getKeyForAutomaticLookahead(Wz,e);return this.atLeastOneInternalLogic(e,t,r)}atLeastOneInternalLogic(e,t,r){let n=this.getLaFuncFromCache(r),i;if(typeof t!="function"){i=t.DEF;let a=t.GATE;if(a!==void 0){let o=n;n=E(()=>a.call(this)&&o.call(this),"lookAheadFunc")}}else i=t;if(n.call(this)===!0){let a=this.doSingleRepetition(i);for(;n.call(this)===!0&&a===!0;)a=this.doSingleRepetition(i)}else throw this.raiseEarlyExitException(e,Ri.REPETITION_MANDATORY,t.ERR_MSG);this.attemptInRepetitionRecovery(this.atLeastOneInternal,[e,t],n,Wz,e,W2t)}atLeastOneSepFirstInternal(e,t){let r=this.getKeyForAutomaticLookahead(XA,e);this.atLeastOneSepFirstInternalLogic(e,t,r)}atLeastOneSepFirstInternalLogic(e,t,r){let n=t.DEF,i=t.SEP;if(this.getLaFuncFromCache(r).call(this)===!0){n.call(this);let o=E(()=>this.tokenMatcher(this.LA(1),i),"separatorLookAheadFunc");for(;this.tokenMatcher(this.LA(1),i)===!0;)this.CONSUME(i),n.call(this);this.attemptInRepetitionRecovery(this.repetitionSepSecondInternal,[e,i,o,n,CSe],o,XA,e,CSe)}else throw this.raiseEarlyExitException(e,Ri.REPETITION_MANDATORY_WITH_SEPARATOR,t.ERR_MSG)}manyInternal(e,t){let r=this.getKeyForAutomaticLookahead(Vz,e);return this.manyInternalLogic(e,t,r)}manyInternalLogic(e,t,r){let n=this.getLaFuncFromCache(r),i;if(typeof t!="function"){i=t.DEF;let o=t.GATE;if(o!==void 0){let l=n;n=E(()=>o.call(this)&&l.call(this),"lookaheadFunction")}}else i=t;let a=!0;for(;n.call(this)===!0&&a===!0;)a=this.doSingleRepetition(i);this.attemptInRepetitionRecovery(this.manyInternal,[e,t],n,Vz,e,V2t,a)}manySepFirstInternal(e,t){let r=this.getKeyForAutomaticLookahead(qz,e);this.manySepFirstInternalLogic(e,t,r)}manySepFirstInternalLogic(e,t,r){let n=t.DEF,i=t.SEP;if(this.getLaFuncFromCache(r).call(this)===!0){n.call(this);let o=E(()=>this.tokenMatcher(this.LA(1),i),"separatorLookAheadFunc");for(;this.tokenMatcher(this.LA(1),i)===!0;)this.CONSUME(i),n.call(this);this.attemptInRepetitionRecovery(this.repetitionSepSecondInternal,[e,i,o,n,TSe],o,qz,e,TSe)}}repetitionSepSecondInternal(e,t,r,n,i){for(;r();)this.CONSUME(t),n.call(this);this.attemptInRepetitionRecovery(this.repetitionSepSecondInternal,[e,t,r,n,i],r,XA,e,i)}doSingleRepetition(e){let t=this.getLexerPosition();return e.call(this),this.getLexerPosition()>t}orInternal(e,t){let r=this.getKeyForAutomaticLookahead($Ae,t),n=gn(e)?e:e.DEF,a=this.getLaFuncFromCache(r).call(this,n);if(a!==void 0)return n[a].ALT.call(this);this.raiseNoAltException(t,e.ERR_MSG)}ruleFinallyStateUpdate(){if(this.RULE_STACK.pop(),this.RULE_OCCURRENCE_STACK.pop(),this.cstFinallyStateUpdate(),this.RULE_STACK.length===0&&this.isAtEndOfInput()===!1){let e=this.LA(1),t=this.errorMessageProvider.buildNotAllInputParsedMessage({firstRedundant:e,ruleName:this.getCurrRuleFullName()});this.SAVE_ERROR(new j2t(t,e))}}subruleInternal(e,t,r){let n;try{let i=r!==void 0?r.ARGS:void 0;return this.subruleIdx=t,n=e.apply(this,i),this.cstPostNonTerminal(n,r!==void 0&&r.LABEL!==void 0?r.LABEL:e.ruleName),n}catch(i){throw this.subruleInternalError(i,r,e.ruleName)}}subruleInternalError(e,t,r){throw tS(e)&&e.partialCstResult!==void 0&&(this.cstPostNonTerminal(e.partialCstResult,t!==void 0&&t.LABEL!==void 0?t.LABEL:r),delete e.partialCstResult),e}consumeInternal(e,t,r){let n;try{let i=this.LA(1);this.tokenMatcher(i,e)===!0?(this.consumeToken(),n=i):this.consumeInternalError(e,i,r)}catch(i){n=this.consumeInternalRecovery(e,t,i)}return this.cstPostTerminal(r!==void 0&&r.LABEL!==void 0?r.LABEL:e.name,n),n}consumeInternalError(e,t,r){let n,i=this.LA(0);throw r!==void 0&&r.ERR_MSG?n=r.ERR_MSG:n=this.errorMessageProvider.buildMismatchTokenMessage({expected:e,actual:t,previous:i,ruleName:this.getCurrRuleFullName()}),this.SAVE_ERROR(new PAe(n,t,i))}consumeInternalRecovery(e,t,r){if(this.recoveryEnabled&&r.name==="MismatchedTokenException"&&!this.isBackTracking()){let n=this.getFollowsForInRuleRecovery(e,t);try{return this.tryInRuleRecovery(e,n)}catch(i){throw i.name===OAe?r:i}}else throw r}saveRecogState(){let e=this.errors,t=ss(this.RULE_STACK);return{errors:e,lexerState:this.exportLexerState(),RULE_STACK:t,CST_STACK:this.CST_STACK}}reloadRecogState(e){this.errors=e.errors,this.importLexerState(e.lexerState),this.RULE_STACK=e.RULE_STACK}ruleInvocationStateUpdate(e,t,r){this.RULE_OCCURRENCE_STACK.push(r),this.RULE_STACK.push(e),this.cstInvocationStateUpdate(t)}isBackTracking(){return this.isBackTrackingStack.length!==0}getCurrRuleFullName(){let e=this.getLastExplicitRuleShortName();return this.shortRuleNameToFull[e]}shortRuleNameToFullName(e){return this.shortRuleNameToFull[e]}isAtEndOfInput(){return this.tokenMatcher(this.LA(1),gp)}reset(){this.resetLexerState(),this.subruleIdx=0,this.isBackTrackingStack=[],this.errors=[],this.RULE_STACK=[],this.CST_STACK=[],this.RULE_OCCURRENCE_STACK=[]}},oTt=class{static{s(this,"ErrorHandler")}static{E(this,"ErrorHandler")}initErrorHandler(e){this._errors=[],this.errorMessageProvider=Rr(e,"errorMessageProvider")?e.errorMessageProvider:ed.errorMessageProvider}SAVE_ERROR(e){if(tS(e))return e.context={ruleStack:this.getHumanReadableRuleStack(),ruleOccurrenceStack:ss(this.RULE_OCCURRENCE_STACK)},this._errors.push(e),e;throw Error("Trying to save an Error which is not a RecognitionException")}get errors(){return ss(this._errors)}set errors(e){this._errors=e}raiseEarlyExitException(e,t,r){let n=this.getCurrRuleFullName(),i=this.getGAstProductions()[n],o=RS(e,i,t,this.maxLookahead)[0],l=[];for(let h=1;h<=this.maxLookahead;h++)l.push(this.LA(h));let u=this.errorMessageProvider.buildEarlyExitMessage({expectedIterationPaths:o,actual:l,previous:this.LA(0),customUserDescription:r,ruleName:n});throw this.SAVE_ERROR(new X2t(u,this.LA(1),this.LA(0)))}raiseNoAltException(e,t){let r=this.getCurrRuleFullName(),n=this.getGAstProductions()[r],i=AS(e,n,this.maxLookahead),a=[];for(let u=1;u<=this.maxLookahead;u++)a.push(this.LA(u));let o=this.LA(0),l=this.errorMessageProvider.buildNoViableAltMessage({expectedPathsPerAlt:i,actual:a,previous:o,customUserDescription:t,ruleName:this.getCurrRuleFullName()});throw this.SAVE_ERROR(new Y2t(l,this.LA(1),o))}},lTt=class{static{s(this,"ContentAssist")}static{E(this,"ContentAssist")}initContentAssist(){}computeContentAssist(e,t){let r=this.gastProductionsCache[e];if(Qh(r))throw Error(`Rule ->${e}<- does not exist in this grammar.`);return Gq([r],t,this.tokenMatcher,this.maxLookahead)}getNextPossibleTokenTypes(e){let t=Ac(e.ruleStack),n=this.getGAstProductions()[t];return new z2t(n,e).startWalking()}},B6={description:"This Object indicates the Parser is during Recording Phase"};Object.freeze(B6);SSe=!0,wSe=Math.pow(2,Cp)-1,jAe=gv({name:"RECORDING_PHASE_TOKEN",pattern:Xs.NA});Ov([jAe]);XAe=ES(jAe,`This IToken indicates the Parser is in Recording Phase + See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,-1,-1,-1,-1,-1,-1);Object.freeze(XAe);cTt={name:`This CSTNode indicates the Parser is in Recording Phase + See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,children:{}},uTt=class{static{s(this,"GastRecorder")}static{E(this,"GastRecorder")}initGastRecorder(e){this.recordingProdStack=[],this.RECORDING_PHASE=!1}enableRecording(){this.RECORDING_PHASE=!0,this.TRACE_INIT("Enable Recording",()=>{for(let e=0;e<10;e++){let t=e>0?e:"";this[`CONSUME${t}`]=function(r,n){return this.consumeInternalRecord(r,e,n)},this[`SUBRULE${t}`]=function(r,n){return this.subruleInternalRecord(r,e,n)},this[`OPTION${t}`]=function(r){return this.optionInternalRecord(r,e)},this[`OR${t}`]=function(r){return this.orInternalRecord(r,e)},this[`MANY${t}`]=function(r){this.manyInternalRecord(e,r)},this[`MANY_SEP${t}`]=function(r){this.manySepFirstInternalRecord(e,r)},this[`AT_LEAST_ONE${t}`]=function(r){this.atLeastOneInternalRecord(e,r)},this[`AT_LEAST_ONE_SEP${t}`]=function(r){this.atLeastOneSepFirstInternalRecord(e,r)}}this.consume=function(e,t,r){return this.consumeInternalRecord(t,e,r)},this.subrule=function(e,t,r){return this.subruleInternalRecord(t,e,r)},this.option=function(e,t){return this.optionInternalRecord(t,e)},this.or=function(e,t){return this.orInternalRecord(t,e)},this.many=function(e,t){this.manyInternalRecord(e,t)},this.atLeastOne=function(e,t){this.atLeastOneInternalRecord(e,t)},this.ACTION=this.ACTION_RECORD,this.BACKTRACK=this.BACKTRACK_RECORD,this.LA=this.LA_RECORD})}disableRecording(){this.RECORDING_PHASE=!1,this.TRACE_INIT("Deleting Recording methods",()=>{let e=this;for(let t=0;t<10;t++){let r=t>0?t:"";delete e[`CONSUME${r}`],delete e[`SUBRULE${r}`],delete e[`OPTION${r}`],delete e[`OR${r}`],delete e[`MANY${r}`],delete e[`MANY_SEP${r}`],delete e[`AT_LEAST_ONE${r}`],delete e[`AT_LEAST_ONE_SEP${r}`]}delete e.consume,delete e.subrule,delete e.option,delete e.or,delete e.many,delete e.atLeastOne,delete e.ACTION,delete e.BACKTRACK,delete e.LA})}ACTION_RECORD(e){}BACKTRACK_RECORD(e,t){return()=>!0}LA_RECORD(e){return FR}topLevelRuleRecord(e,t){try{let r=new Iv({definition:[],name:e});return r.name=e,this.recordingProdStack.push(r),t.call(this),this.recordingProdStack.pop(),r}catch(r){if(r.KNOWN_RECORDER_ERROR!==!0)try{r.message=r.message+` + This error was thrown during the "grammar recording phase" For more info see: + https://chevrotain.io/docs/guide/internals.html#grammar-recording`}catch{throw r}throw r}}optionInternalRecord(e,t){return ev.call(this,as,e,t)}atLeastOneInternalRecord(e,t){ev.call(this,nl,t,e)}atLeastOneSepFirstInternalRecord(e,t){ev.call(this,il,t,e,SSe)}manyInternalRecord(e,t){ev.call(this,Gi,t,e)}manySepFirstInternalRecord(e,t){ev.call(this,Ao,t,e,SSe)}orInternalRecord(e,t){return KAe.call(this,e,t)}subruleInternalRecord(e,t,r){if(rS(t),!e||Rr(e,"ruleName")===!1){let o=new Error(` argument is invalid expecting a Parser method reference but got: <${JSON.stringify(e)}> + inside top level rule: <${this.recordingProdStack[0].name}>`);throw o.KNOWN_RECORDER_ERROR=!0,o}let n=Xg(this.recordingProdStack),i=e.ruleName,a=new Zs({idx:t,nonTerminalName:i,label:r?.LABEL,referencedRule:void 0});return n.definition.push(a),this.outputCst?cTt:B6}consumeInternalRecord(e,t,r){if(rS(t),!Bq(e)){let a=new Error(` argument is invalid expecting a TokenType reference but got: <${JSON.stringify(e)}> + inside top level rule: <${this.recordingProdStack[0].name}>`);throw a.KNOWN_RECORDER_ERROR=!0,a}let n=Xg(this.recordingProdStack),i=new fi({idx:t,terminalType:e,label:r?.LABEL});return n.definition.push(i),XAe}};s(ev,"recordProd");E(ev,"recordProd");s(KAe,"recordOrProd");E(KAe,"recordOrProd");s(jz,"getIdxSuffix");E(jz,"getIdxSuffix");s(rS,"assertMethodIdxIsValid");E(rS,"assertMethodIdxIsValid");hTt=class{static{s(this,"PerformanceTracer")}static{E(this,"PerformanceTracer")}initPerformanceTracer(e){if(Rr(e,"traceInitPerf")){let t=e.traceInitPerf,r=typeof t=="number";this.traceInitMaxIdent=r?t:1/0,this.traceInitPerf=r?t>0:t}else this.traceInitMaxIdent=0,this.traceInitPerf=ed.traceInitPerf;this.traceInitIndent=-1}TRACE_INIT(e,t){if(this.traceInitPerf===!0){this.traceInitIndent++;let r=new Array(this.traceInitIndent+1).join(" ");this.traceInitIndent <${e}>`);let{time:n,value:i}=Dq(t),a=n>10?console.warn:console.log;return this.traceInitIndent time: ${n}ms`),this.traceInitIndent--,i}else return t()}};s(ZAe,"applyMixins");E(ZAe,"applyMixins");FR=ES(gp,"",NaN,NaN,NaN,NaN,NaN,NaN);Object.freeze(FR);ed=Object.freeze({recoveryEnabled:!1,maxLookahead:3,dynamicTokensEnabled:!1,outputCst:!0,errorMessageProvider:hv,nodeLocationTracking:"none",traceInitPerf:!1,skipValidations:!1}),GR=Object.freeze({recoveryValueFunc:E(()=>{},"recoveryValueFunc"),resyncEnabled:!0});(function(e){e[e.INVALID_RULE_NAME=0]="INVALID_RULE_NAME",e[e.DUPLICATE_RULE_NAME=1]="DUPLICATE_RULE_NAME",e[e.INVALID_RULE_OVERRIDE=2]="INVALID_RULE_OVERRIDE",e[e.DUPLICATE_PRODUCTIONS=3]="DUPLICATE_PRODUCTIONS",e[e.UNRESOLVED_SUBRULE_REF=4]="UNRESOLVED_SUBRULE_REF",e[e.LEFT_RECURSION=5]="LEFT_RECURSION",e[e.NONE_LAST_EMPTY_ALT=6]="NONE_LAST_EMPTY_ALT",e[e.AMBIGUOUS_ALTS=7]="AMBIGUOUS_ALTS",e[e.CONFLICT_TOKENS_RULES_NAMESPACE=8]="CONFLICT_TOKENS_RULES_NAMESPACE",e[e.INVALID_TOKEN_NAME=9]="INVALID_TOKEN_NAME",e[e.NO_NON_EMPTY_LOOKAHEAD=10]="NO_NON_EMPTY_LOOKAHEAD",e[e.AMBIGUOUS_PREFIX_ALTS=11]="AMBIGUOUS_PREFIX_ALTS",e[e.TOO_MANY_ALTS=12]="TOO_MANY_ALTS",e[e.CUSTOM_LOOKAHEAD_VALIDATION=13]="CUSTOM_LOOKAHEAD_VALIDATION"})(Qs||(Qs={}));s(Xz,"EMPTY_ALT");E(Xz,"EMPTY_ALT");jq=class QAe{static{s(this,"_Parser")}static{E(this,"Parser")}static performSelfAnalysis(t){throw Error("The **static** `performSelfAnalysis` method has been deprecated. \nUse the **instance** method with the same name instead.")}performSelfAnalysis(){this.TRACE_INIT("performSelfAnalysis",()=>{let t;this.selfAnalysisDone=!0;let r=this.className;this.TRACE_INIT("toFastProps",()=>{Iq(this)}),this.TRACE_INIT("Grammar Recording",()=>{try{this.enableRecording(),Br(this.definedRulesNames,i=>{let o=this[i].originalGrammarAction,l;this.TRACE_INIT(`${i} Rule`,()=>{l=this.topLevelRuleRecord(i,o)}),this.gastProductionsCache[i]=l})}finally{this.disableRecording()}});let n=[];if(this.TRACE_INIT("Grammar Resolving",()=>{n=RAe({rules:xa(this.gastProductionsCache)}),this.definitionErrors=this.definitionErrors.concat(n)}),this.TRACE_INIT("Grammar Validations",()=>{if(ni(n)&&this.skipValidations===!1){let i=_Ae({rules:xa(this.gastProductionsCache),tokenTypes:xa(this.tokensMap),errMsgProvider:Wg,grammarName:r}),a=mAe({lookaheadStrategy:this.lookaheadStrategy,rules:xa(this.gastProductionsCache),tokenTypes:xa(this.tokensMap),grammarName:r});this.definitionErrors=this.definitionErrors.concat(i,a)}}),ni(this.definitionErrors)&&(this.recoveryEnabled&&this.TRACE_INIT("computeAllProdsFollows",()=>{let i=S5e(xa(this.gastProductionsCache));this.resyncFollows=i}),this.TRACE_INIT("ComputeLookaheadFunctions",()=>{var i,a;(a=(i=this.lookaheadStrategy).initialize)===null||a===void 0||a.call(i,{rules:xa(this.gastProductionsCache)}),this.preComputeLookaheadFunctions(xa(this.gastProductionsCache))})),!QAe.DEFER_DEFINITION_ERRORS_HANDLING&&!ni(this.definitionErrors))throw t=gr(this.definitionErrors,i=>i.message),new Error(`Parser Definition Errors detected: + ${t.join(` +------------------------------- +`)}`)})}constructor(t,r){this.definitionErrors=[],this.selfAnalysisDone=!1;let n=this;if(n.initErrorHandler(r),n.initLexerAdapter(),n.initLooksAhead(r),n.initRecognizerEngine(t,r),n.initRecoverable(r),n.initTreeBuilder(r),n.initContentAssist(),n.initGastRecorder(r),n.initPerformanceTracer(r),Rr(r,"ignoredIssues"))throw new Error(`The IParserConfig property has been deprecated. + Please use the flag on the relevant DSL method instead. + See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#IGNORING_AMBIGUITIES + For further details.`);this.skipValidations=Rr(r,"skipValidations")?r.skipValidations:ed.skipValidations}};jq.DEFER_DEFINITION_ERRORS_HANDLING=!1;ZAe(jq,[Z2t,eTt,nTt,iTt,sTt,aTt,oTt,lTt,uTt,hTt]);dTt=class extends jq{static{s(this,"EmbeddedActionsParser")}static{E(this,"EmbeddedActionsParser")}constructor(e,t=ed){let r=ss(t);r.outputCst=!1,super(e,r)}};s(JAe,"arrayMap2");E(JAe,"arrayMap");eRe=JAe;s(tRe,"listCacheClear2");E(tRe,"listCacheClear");fTt=tRe;s(rRe,"eq2");E(rRe,"eq");nRe=rRe;s(iRe,"assocIndexOf2");E(iRe,"assocIndexOf");$6=iRe,pTt=Array.prototype,mTt=pTt.splice;s(aRe,"listCacheDelete2");E(aRe,"listCacheDelete");gTt=aRe;s(sRe,"listCacheGet2");E(sRe,"listCacheGet");yTt=sRe;s(oRe,"listCacheHas2");E(oRe,"listCacheHas");vTt=oRe;s(lRe,"listCacheSet2");E(lRe,"listCacheSet");xTt=lRe;s(d0,"ListCache2");E(d0,"ListCache");d0.prototype.clear=fTt;d0.prototype.delete=gTt;d0.prototype.get=yTt;d0.prototype.has=vTt;d0.prototype.set=xTt;F6=d0;s(cRe,"stackClear2");E(cRe,"stackClear");bTt=cRe;s(uRe,"stackDelete2");E(uRe,"stackDelete");TTt=uRe;s(hRe,"stackGet2");E(hRe,"stackGet");CTt=hRe;s(dRe,"stackHas2");E(dRe,"stackHas");STt=dRe,wTt=typeof global=="object"&&global&&global.Object===Object&&global,fRe=wTt,kTt=typeof self=="object"&&self&&self.Object===Object&&self,ETt=fRe||kTt||Function("return this")(),ad=ETt,ATt=ad.Symbol,Gu=ATt,pRe=Object.prototype,RTt=pRe.hasOwnProperty,_Tt=pRe.toString,lC=Gu?Gu.toStringTag:void 0;s(mRe,"getRawTag2");E(mRe,"getRawTag");LTt=mRe,DTt=Object.prototype,ITt=DTt.toString;s(gRe,"objectToString2");E(gRe,"objectToString");MTt=gRe,NTt="[object Null]",PTt="[object Undefined]",kSe=Gu?Gu.toStringTag:void 0;s(yRe,"baseGetTag2");E(yRe,"baseGetTag");Bv=yRe;s(vRe,"isObject2");E(vRe,"isObject");Xq=vRe,OTt="[object AsyncFunction]",BTt="[object Function]",$Tt="[object GeneratorFunction]",FTt="[object Proxy]";s(xRe,"isFunction2");E(xRe,"isFunction");bRe=xRe,GTt=ad["__core-js_shared__"],hG=GTt,ESe=(function(){var e=/[^.]+$/.exec(hG&&hG.keys&&hG.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""})();s(TRe,"isMasked2");E(TRe,"isMasked");zTt=TRe,VTt=Function.prototype,WTt=VTt.toString;s(CRe,"toSource2");E(CRe,"toSource");f0=CRe,qTt=/[\\^$.*+?()[\]{}|]/g,HTt=/^\[object .+?Constructor\]$/,UTt=Function.prototype,YTt=Object.prototype,jTt=UTt.toString,XTt=YTt.hasOwnProperty,KTt=RegExp("^"+jTt.call(XTt).replace(qTt,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");s(SRe,"baseIsNative2");E(SRe,"baseIsNative");ZTt=SRe;s(wRe,"getValue2");E(wRe,"getValue");QTt=wRe;s(kRe,"getNative2");E(kRe,"getNative");$v=kRe,JTt=$v(ad,"Map"),nS=JTt,eCt=$v(Object,"create"),iS=eCt;s(ERe,"hashClear2");E(ERe,"hashClear");tCt=ERe;s(ARe,"hashDelete2");E(ARe,"hashDelete");rCt=ARe,nCt="__lodash_hash_undefined__",iCt=Object.prototype,aCt=iCt.hasOwnProperty;s(RRe,"hashGet2");E(RRe,"hashGet");sCt=RRe,oCt=Object.prototype,lCt=oCt.hasOwnProperty;s(_Re,"hashHas2");E(_Re,"hashHas");cCt=_Re,uCt="__lodash_hash_undefined__";s(LRe,"hashSet2");E(LRe,"hashSet");hCt=LRe;s(p0,"Hash2");E(p0,"Hash");p0.prototype.clear=tCt;p0.prototype.delete=rCt;p0.prototype.get=sCt;p0.prototype.has=cCt;p0.prototype.set=hCt;ASe=p0;s(DRe,"mapCacheClear2");E(DRe,"mapCacheClear");dCt=DRe;s(IRe,"isKeyable2");E(IRe,"isKeyable");fCt=IRe;s(MRe,"getMapData2");E(MRe,"getMapData");G6=MRe;s(NRe,"mapCacheDelete2");E(NRe,"mapCacheDelete");pCt=NRe;s(PRe,"mapCacheGet2");E(PRe,"mapCacheGet");mCt=PRe;s(ORe,"mapCacheHas2");E(ORe,"mapCacheHas");gCt=ORe;s(BRe,"mapCacheSet2");E(BRe,"mapCacheSet");yCt=BRe;s(m0,"MapCache2");E(m0,"MapCache");m0.prototype.clear=dCt;m0.prototype.delete=pCt;m0.prototype.get=mCt;m0.prototype.has=gCt;m0.prototype.set=yCt;z6=m0,vCt=200;s($Re,"stackSet2");E($Re,"stackSet");xCt=$Re;s(g0,"Stack2");E(g0,"Stack");g0.prototype.clear=bTt;g0.prototype.delete=TTt;g0.prototype.get=CTt;g0.prototype.has=STt;g0.prototype.set=xCt;ZA=g0,bCt="__lodash_hash_undefined__";s(FRe,"setCacheAdd2");E(FRe,"setCacheAdd");TCt=FRe;s(GRe,"setCacheHas2");E(GRe,"setCacheHas");CCt=GRe;s(aS,"SetCache2");E(aS,"SetCache");aS.prototype.add=aS.prototype.push=TCt;aS.prototype.has=CCt;zRe=aS;s(VRe,"arraySome2");E(VRe,"arraySome");SCt=VRe;s(WRe,"cacheHas2");E(WRe,"cacheHas");qRe=WRe,wCt=1,kCt=2;s(HRe,"equalArrays2");E(HRe,"equalArrays");URe=HRe,ECt=ad.Uint8Array,RSe=ECt;s(YRe,"mapToArray2");E(YRe,"mapToArray");ACt=YRe;s(jRe,"setToArray2");E(jRe,"setToArray");Kq=jRe,RCt=1,_Ct=2,LCt="[object Boolean]",DCt="[object Date]",ICt="[object Error]",MCt="[object Map]",NCt="[object Number]",PCt="[object RegExp]",OCt="[object Set]",BCt="[object String]",$Ct="[object Symbol]",FCt="[object ArrayBuffer]",GCt="[object DataView]",_Se=Gu?Gu.prototype:void 0,dG=_Se?_Se.valueOf:void 0;s(XRe,"equalByTag2");E(XRe,"equalByTag");zCt=XRe;s(KRe,"arrayPush2");E(KRe,"arrayPush");ZRe=KRe,VCt=Array.isArray,Js=VCt;s(QRe,"baseGetAllKeys2");E(QRe,"baseGetAllKeys");WCt=QRe;s(JRe,"arrayFilter2");E(JRe,"arrayFilter");e6e=JRe;s(t6e,"stubArray2");E(t6e,"stubArray");qCt=t6e,HCt=Object.prototype,UCt=HCt.propertyIsEnumerable,LSe=Object.getOwnPropertySymbols,YCt=LSe?function(e){return e==null?[]:(e=Object(e),e6e(LSe(e),function(t){return UCt.call(e,t)}))}:qCt,jCt=YCt;s(r6e,"baseTimes2");E(r6e,"baseTimes");XCt=r6e;s(n6e,"isObjectLike2");E(n6e,"isObjectLike");Av=n6e,KCt="[object Arguments]";s(i6e,"baseIsArguments2");E(i6e,"baseIsArguments");DSe=i6e,a6e=Object.prototype,ZCt=a6e.hasOwnProperty,QCt=a6e.propertyIsEnumerable,JCt=DSe((function(){return arguments})())?DSe:function(e){return Av(e)&&ZCt.call(e,"callee")&&!QCt.call(e,"callee")},V6=JCt;s(s6e,"stubFalse2");E(s6e,"stubFalse");eSt=s6e,o6e=typeof exports=="object"&&exports&&!exports.nodeType&&exports,ISe=o6e&&typeof module=="object"&&module&&!module.nodeType&&module,tSt=ISe&&ISe.exports===o6e,MSe=tSt?ad.Buffer:void 0,rSt=MSe?MSe.isBuffer:void 0,nSt=rSt||eSt,zR=nSt,iSt=9007199254740991,aSt=/^(?:0|[1-9]\d*)$/;s(l6e,"isIndex2");E(l6e,"isIndex");c6e=l6e,sSt=9007199254740991;s(u6e,"isLength2");E(u6e,"isLength");Zq=u6e,oSt="[object Arguments]",lSt="[object Array]",cSt="[object Boolean]",uSt="[object Date]",hSt="[object Error]",dSt="[object Function]",fSt="[object Map]",pSt="[object Number]",mSt="[object Object]",gSt="[object RegExp]",ySt="[object Set]",vSt="[object String]",xSt="[object WeakMap]",bSt="[object ArrayBuffer]",TSt="[object DataView]",CSt="[object Float32Array]",SSt="[object Float64Array]",wSt="[object Int8Array]",kSt="[object Int16Array]",ESt="[object Int32Array]",ASt="[object Uint8Array]",RSt="[object Uint8ClampedArray]",_St="[object Uint16Array]",LSt="[object Uint32Array]",di={};di[CSt]=di[SSt]=di[wSt]=di[kSt]=di[ESt]=di[ASt]=di[RSt]=di[_St]=di[LSt]=!0;di[oSt]=di[lSt]=di[bSt]=di[cSt]=di[TSt]=di[uSt]=di[hSt]=di[dSt]=di[fSt]=di[pSt]=di[mSt]=di[gSt]=di[ySt]=di[vSt]=di[xSt]=!1;s(h6e,"baseIsTypedArray2");E(h6e,"baseIsTypedArray");DSt=h6e;s(d6e,"baseUnary2");E(d6e,"baseUnary");ISt=d6e,f6e=typeof exports=="object"&&exports&&!exports.nodeType&&exports,GC=f6e&&typeof module=="object"&&module&&!module.nodeType&&module,MSt=GC&&GC.exports===f6e,fG=MSt&&fRe.process,NSt=(function(){try{var e=GC&&GC.require&&GC.require("util").types;return e||fG&&fG.binding&&fG.binding("util")}catch{}})(),NSe=NSt,PSe=NSe&&NSe.isTypedArray,PSt=PSe?ISt(PSe):DSt,Qq=PSt,OSt=Object.prototype,BSt=OSt.hasOwnProperty;s(p6e,"arrayLikeKeys2");E(p6e,"arrayLikeKeys");$St=p6e,FSt=Object.prototype;s(m6e,"isPrototype2");E(m6e,"isPrototype");g6e=m6e;s(y6e,"overArg2");E(y6e,"overArg");GSt=y6e,zSt=GSt(Object.keys,Object),VSt=zSt,WSt=Object.prototype,qSt=WSt.hasOwnProperty;s(v6e,"baseKeys2");E(v6e,"baseKeys");x6e=v6e;s(b6e,"isArrayLike2");E(b6e,"isArrayLike");W6=b6e;s(T6e,"keys2");E(T6e,"keys");Jq=T6e;s(C6e,"getAllKeys2");E(C6e,"getAllKeys");OSe=C6e,HSt=1,USt=Object.prototype,YSt=USt.hasOwnProperty;s(S6e,"equalObjects2");E(S6e,"equalObjects");jSt=S6e,XSt=$v(ad,"DataView"),Kz=XSt,KSt=$v(ad,"Promise"),Zz=KSt,ZSt=$v(ad,"Set"),yv=ZSt,QSt=$v(ad,"WeakMap"),Qz=QSt,BSe="[object Map]",JSt="[object Object]",$Se="[object Promise]",FSe="[object Set]",GSe="[object WeakMap]",zSe="[object DataView]",ewt=f0(Kz),twt=f0(nS),rwt=f0(Zz),nwt=f0(yv),iwt=f0(Qz),mg=Bv;(Kz&&mg(new Kz(new ArrayBuffer(1)))!=zSe||nS&&mg(new nS)!=BSe||Zz&&mg(Zz.resolve())!=$Se||yv&&mg(new yv)!=FSe||Qz&&mg(new Qz)!=GSe)&&(mg=E(function(e){var t=Bv(e),r=t==JSt?e.constructor:void 0,n=r?f0(r):"";if(n)switch(n){case ewt:return zSe;case twt:return BSe;case rwt:return $Se;case nwt:return FSe;case iwt:return GSe}return t},"getTag"));Jz=mg,awt=1,VSe="[object Arguments]",WSe="[object Array]",TA="[object Object]",swt=Object.prototype,qSe=swt.hasOwnProperty;s(w6e,"baseIsEqualDeep2");E(w6e,"baseIsEqualDeep");owt=w6e;s(eH,"baseIsEqual2");E(eH,"baseIsEqual");k6e=eH,lwt=1,cwt=2;s(E6e,"baseIsMatch2");E(E6e,"baseIsMatch");uwt=E6e;s(A6e,"isStrictComparable2");E(A6e,"isStrictComparable");R6e=A6e;s(_6e,"getMatchData2");E(_6e,"getMatchData");hwt=_6e;s(L6e,"matchesStrictComparable2");E(L6e,"matchesStrictComparable");D6e=L6e;s(I6e,"baseMatches2");E(I6e,"baseMatches");dwt=I6e,fwt="[object Symbol]";s(M6e,"isSymbol2");E(M6e,"isSymbol");q6=M6e,pwt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,mwt=/^\w*$/;s(N6e,"isKey2");E(N6e,"isKey");tH=N6e,gwt="Expected a function";s(H6,"memoize2");E(H6,"memoize");H6.Cache=z6;ywt=H6,vwt=500;s(P6e,"memoizeCapped2");E(P6e,"memoizeCapped");xwt=P6e,bwt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Twt=/\\(\\)?/g,Cwt=xwt(function(e){var t=[];return e.charCodeAt(0)===46&&t.push(""),e.replace(bwt,function(r,n,i,a){t.push(i?a.replace(Twt,"$1"):n||r)}),t}),Swt=Cwt,wwt=1/0,HSe=Gu?Gu.prototype:void 0,USe=HSe?HSe.toString:void 0;s(rH,"baseToString2");E(rH,"baseToString");kwt=rH;s(O6e,"toString3");E(O6e,"toString");Ewt=O6e;s(B6e,"castPath2");E(B6e,"castPath");$6e=B6e,Awt=1/0;s(F6e,"toKey2");E(F6e,"toKey");U6=F6e;s(G6e,"baseGet2");E(G6e,"baseGet");z6e=G6e;s(V6e,"get2");E(V6e,"get");Rwt=V6e;s(W6e,"baseHasIn2");E(W6e,"baseHasIn");_wt=W6e;s(q6e,"hasPath2");E(q6e,"hasPath");Lwt=q6e;s(H6e,"hasIn2");E(H6e,"hasIn");Dwt=H6e,Iwt=1,Mwt=2;s(U6e,"baseMatchesProperty2");E(U6e,"baseMatchesProperty");Nwt=U6e;s(Y6e,"identity2");E(Y6e,"identity");nH=Y6e;s(j6e,"baseProperty2");E(j6e,"baseProperty");Pwt=j6e;s(X6e,"basePropertyDeep2");E(X6e,"basePropertyDeep");Owt=X6e;s(K6e,"property2");E(K6e,"property");Bwt=K6e;s(Z6e,"baseIteratee2");E(Z6e,"baseIteratee");Y6=Z6e;s(Q6e,"createBaseFor2");E(Q6e,"createBaseFor");$wt=Q6e,Fwt=$wt(),Gwt=Fwt;s(J6e,"baseForOwn2");E(J6e,"baseForOwn");zwt=J6e;s(e_e,"createBaseEach2");E(e_e,"createBaseEach");Vwt=e_e,Wwt=Vwt(zwt),j6=Wwt;s(t_e,"baseMap2");E(t_e,"baseMap");qwt=t_e;s(r_e,"map2");E(r_e,"map");Uh=r_e;s(n_e,"baseFilter2");E(n_e,"baseFilter");Hwt=n_e;s(i_e,"filter2");E(i_e,"filter");Uwt=i_e;s(Zg,"buildATNKey");E(Zg,"buildATNKey");yp=1,Ywt=2,a_e=4,s_e=5,_S=7,jwt=8,Xwt=9,Kwt=10,Zwt=11,o_e=12,iH=class{static{s(this,"AbstractTransition")}static{E(this,"AbstractTransition")}constructor(e){this.target=e}isEpsilon(){return!1}},aH=class extends iH{static{s(this,"AtomTransition")}static{E(this,"AtomTransition")}constructor(e,t){super(e),this.tokenType=t}},l_e=class extends iH{static{s(this,"EpsilonTransition")}static{E(this,"EpsilonTransition")}constructor(e){super(e)}isEpsilon(){return!0}},sH=class extends iH{static{s(this,"RuleTransition")}static{E(this,"RuleTransition")}constructor(e,t,r){super(e),this.rule=t,this.followState=r}isEpsilon(){return!0}};s(c_e,"createATN");E(c_e,"createATN");s(u_e,"createRuleStartAndStopATNStates");E(u_e,"createRuleStartAndStopATNStates");s(oH,"atom");E(oH,"atom");s(h_e,"repetition");E(h_e,"repetition");s(d_e,"repetitionSep");E(d_e,"repetitionSep");s(f_e,"repetitionMandatory");E(f_e,"repetitionMandatory");s(p_e,"repetitionMandatorySep");E(p_e,"repetitionMandatorySep");s(m_e,"alternation");E(m_e,"alternation");s(g_e,"option");E(g_e,"option");s(Sp,"block");E(Sp,"block");s(lH,"plus");E(lH,"plus");s(cH,"star");E(cH,"star");s(y_e,"optional");E(y_e,"optional");s(sd,"defineDecisionState");E(sd,"defineDecisionState");s(y0,"makeAlts");E(y0,"makeAlts");s(v_e,"getProdType2");E(v_e,"getProdType");s(x_e,"makeBlock");E(x_e,"makeBlock");s(X6,"tokenRef");E(X6,"tokenRef");s(b_e,"ruleRef");E(b_e,"ruleRef");s(T_e,"buildRuleHandle");E(T_e,"buildRuleHandle");s(Hi,"epsilon");E(Hi,"epsilon");s(ba,"newState");E(ba,"newState");s(K6,"addTransition");E(K6,"addTransition");s(C_e,"removeState");E(C_e,"removeState");VR={},eV=class{static{s(this,"ATNConfigSet")}static{E(this,"ATNConfigSet")}constructor(){this.map={},this.configs=[]}get size(){return this.configs.length}finalize(){this.map={}}add(e){let t=uH(e);t in this.map||(this.map[t]=this.configs.length,this.configs.push(e))}get elements(){return this.configs}get alts(){return Uh(this.configs,e=>e.alt)}get key(){let e="";for(let t in this.map)e+=t+":";return e}};s(uH,"getATNConfigKey");E(uH,"getATNConfigKey");s(S_e,"baseExtremum");E(S_e,"baseExtremum");Qwt=S_e;s(w_e,"baseLt");E(w_e,"baseLt");Jwt=w_e;s(k_e,"min");E(k_e,"min");ekt=k_e,YSe=Gu?Gu.isConcatSpreadable:void 0;s(E_e,"isFlattenable2");E(E_e,"isFlattenable");tkt=E_e;s(hH,"baseFlatten2");E(hH,"baseFlatten");A_e=hH;s(R_e,"flatMap2");E(R_e,"flatMap");rkt=R_e;s(__e,"baseFindIndex2");E(__e,"baseFindIndex");nkt=__e;s(L_e,"baseIsNaN2");E(L_e,"baseIsNaN");ikt=L_e;s(D_e,"strictIndexOf2");E(D_e,"strictIndexOf");akt=D_e;s(I_e,"baseIndexOf2");E(I_e,"baseIndexOf");skt=I_e;s(M_e,"arrayIncludes2");E(M_e,"arrayIncludes");okt=M_e;s(N_e,"arrayIncludesWith2");E(N_e,"arrayIncludesWith");lkt=N_e;s(P_e,"noop2");E(P_e,"noop");ckt=P_e,ukt=1/0,hkt=yv&&1/Kq(new yv([,-0]))[1]==ukt?function(e){return new yv(e)}:ckt,dkt=hkt,fkt=200;s(O_e,"baseUniq2");E(O_e,"baseUniq");pkt=O_e;s(B_e,"uniqBy");E(B_e,"uniqBy");mkt=B_e;s($_e,"flatten2");E($_e,"flatten");gkt=$_e;s(F_e,"arrayEach2");E(F_e,"arrayEach");ykt=F_e;s(G_e,"castFunction2");E(G_e,"castFunction");vkt=G_e;s(z_e,"forEach2");E(z_e,"forEach");pG=z_e,xkt="[object Map]",bkt="[object Set]",Tkt=Object.prototype,Ckt=Tkt.hasOwnProperty;s(V_e,"isEmpty2");E(V_e,"isEmpty");Skt=V_e;s(W_e,"arrayReduce2");E(W_e,"arrayReduce");wkt=W_e;s(q_e,"baseReduce2");E(q_e,"baseReduce");kkt=q_e;s(H_e,"reduce2");E(H_e,"reduce");jSe=H_e;s(U_e,"createDFACache");E(U_e,"createDFACache");Y_e=class{static{s(this,"PredicateSet")}static{E(this,"PredicateSet")}constructor(){this.predicates=[]}is(e){return e>=this.predicates.length||this.predicates[e]}set(e,t){this.predicates[e]=t}toString(){let e="",t=this.predicates.length;for(let r=0;rconsole.log(r))}initialize(e){this.atn=c_e(e.rules),this.dfas=j_e(this.atn)}validateAmbiguousAlternationAlternatives(){return[]}validateEmptyOrAlternatives(){return[]}buildLookaheadForAlternation(e){let{prodOccurrence:t,rule:r,hasPredicates:n,dynamicTokensEnabled:i}=e,a=this.dfas,o=this.logging,l=Zg(r,"Alternation",t),h=this.atn.decisionMap[l].decision,d=Uh(Gz({maxLookahead:1,occurrence:t,prodType:"Alternation",rule:r}),f=>Uh(f,p=>p[0]));if(tV(d,!1)&&!i){let f=jSe(d,(p,m,g)=>(pG(m,y=>{y&&(p[y.tokenTypeIdx]=g,pG(y.categoryMatches,v=>{p[v]=g}))}),p),{});return n?function(p){var m;let g=this.LA(1),y=f[g.tokenTypeIdx];if(p!==void 0&&y!==void 0){let v=(m=p[y])===null||m===void 0?void 0:m.GATE;if(v!==void 0&&v.call(this)===!1)return}return y}:function(){let p=this.LA(1);return f[p.tokenTypeIdx]}}else return n?function(f){let p=new Y_e,m=f===void 0?0:f.length;for(let y=0;yUh(f,p=>p[0]));if(tV(d)&&d[0][0]&&!i){let f=d[0],p=gkt(f);if(p.length===1&&Skt(p[0].categoryMatches)){let g=p[0].tokenTypeIdx;return function(){return this.LA(1).tokenTypeIdx===g}}else{let m=jSe(p,(g,y)=>(y!==void 0&&(g[y.tokenTypeIdx]=!0,pG(y.categoryMatches,v=>{g[v]=!0})),g),{});return function(){let g=this.LA(1);return m[g.tokenTypeIdx]===!0}}}return function(){let f=QA.call(this,a,h,XSe,o);return typeof f=="object"?!1:f===0}}};s(tV,"isLL1Sequence");E(tV,"isLL1Sequence");s(j_e,"initATNSimulator");E(j_e,"initATNSimulator");s(QA,"adaptivePredict");E(QA,"adaptivePredict");s(X_e,"performLookahead");E(X_e,"performLookahead");s(K_e,"computeLookaheadTarget");E(K_e,"computeLookaheadTarget");s(Z_e,"reportLookaheadAmbiguity");E(Z_e,"reportLookaheadAmbiguity");s(Q_e,"buildAmbiguityError");E(Q_e,"buildAmbiguityError");s(J_e,"getProductionDslName2");E(J_e,"getProductionDslName");s(eLe,"buildAdaptivePredictError");E(eLe,"buildAdaptivePredictError");s(tLe,"getExistingTargetState");E(tLe,"getExistingTargetState");s(rLe,"computeReachSet");E(rLe,"computeReachSet");s(nLe,"getReachableTarget");E(nLe,"getReachableTarget");s(iLe,"getUniqueAlt");E(iLe,"getUniqueAlt");s(dH,"newDFAState");E(dH,"newDFAState");s(rV,"addDFAEdge");E(rV,"addDFAEdge");s(fH,"addDFAState");E(fH,"addDFAState");s(aLe,"computeStartState");E(aLe,"computeStartState");s(sS,"closure");E(sS,"closure");s(sLe,"getEpsilonTarget");E(sLe,"getEpsilonTarget");s(oLe,"hasConfigInRuleStopState");E(oLe,"hasConfigInRuleStopState");s(lLe,"allConfigsInRuleStopStates");E(lLe,"allConfigsInRuleStopStates");s(cLe,"hasConflictTerminatingPrediction");E(cLe,"hasConflictTerminatingPrediction");s(uLe,"getConflictingAltSets");E(uLe,"getConflictingAltSets");s(hLe,"hasConflictingAltSet");E(hLe,"hasConflictingAltSet");s(dLe,"hasStateAssociatedWithOneAlt");E(dLe,"hasStateAssociatedWithOneAlt");cS();fLe=class{static{s(this,"CstNodeBuilder")}static{E(this,"CstNodeBuilder")}constructor(){this.nodeStack=[]}get current(){return this.nodeStack[this.nodeStack.length-1]??this.rootNode}buildRootNode(e){return this.rootNode=new mH(e),this.rootNode.root=this.rootNode,this.nodeStack=[this.rootNode],this.rootNode}buildCompositeNode(e){let t=new Z6;return t.grammarSource=e,t.root=this.rootNode,this.current.content.push(t),this.nodeStack.push(t),t}buildLeafNode(e,t){let r=new WR(e.startOffset,e.image.length,YC(e),e.tokenType,!t);return r.grammarSource=t,r.root=this.rootNode,this.current.content.push(r),r}removeNode(e){let t=e.container;if(t){let r=t.content.indexOf(e);r>=0&&t.content.splice(r,1)}}addHiddenNodes(e){let t=[];for(let i of e){let a=new WR(i.startOffset,i.image.length,YC(i),i.tokenType,!0);a.root=this.rootNode,t.push(a)}let r=this.current,n=!1;if(r.content.length>0){r.content.push(...t);return}for(;r.container;){let i=r.container.content.indexOf(r);if(i>0){r.container.content.splice(i,0,...t),n=!0;break}r=r.container}n||this.rootNode.content.unshift(...t)}construct(e){let t=this.current;typeof e.$type=="string"&&!e.$infixName&&(this.current.astNode=e),e.$cstNode=t;let r=this.nodeStack.pop();r?.content.length===0&&this.removeNode(r)}},pH=class{static{s(this,"AbstractCstNode")}static{E(this,"AbstractCstNode")}get hidden(){return!1}get astNode(){let e=typeof this._astNode?.$type=="string"?this._astNode:this.container?.astNode;if(!e)throw new Error("This node has no associated AST element");return e}set astNode(e){this._astNode=e}get text(){return this.root.fullText.substring(this.offset,this.end)}},WR=class extends pH{static{s(this,"LeafCstNodeImpl")}static{E(this,"LeafCstNodeImpl")}get offset(){return this._offset}get length(){return this._length}get end(){return this._offset+this._length}get hidden(){return this._hidden}get tokenType(){return this._tokenType}get range(){return this._range}constructor(e,t,r,n,i=!1){super(),this._hidden=i,this._offset=e,this._tokenType=n,this._length=t,this._range=r}},Z6=class extends pH{static{s(this,"CompositeCstNodeImpl")}static{E(this,"CompositeCstNodeImpl")}constructor(){super(...arguments),this.content=new Akt(this)}get offset(){return this.firstNonHiddenNode?.offset??0}get length(){return this.end-this.offset}get end(){return this.lastNonHiddenNode?.end??0}get range(){let e=this.firstNonHiddenNode,t=this.lastNonHiddenNode;if(e&&t){if(this._rangeCache===void 0){let{range:r}=e,{range:n}=t;this._rangeCache={start:r.start,end:n.end.line=0;e--){let t=this.content[e];if(!t.hidden)return t}return this.content[this.content.length-1]}},Akt=class pLe extends Array{static{s(this,"_CstNodeContainer")}static{E(this,"CstNodeContainer")}constructor(t){super(),this.parent=t,Object.setPrototypeOf(this,pLe.prototype)}push(...t){return this.addParents(t),super.push(...t)}unshift(...t){return this.addParents(t),super.unshift(...t)}splice(t,r,...n){return this.addParents(n),super.splice(t,r,...n)}addParents(t){for(let r of t)r.container=this.parent}},mH=class extends Z6{static{s(this,"RootCstNodeImpl")}static{E(this,"RootCstNodeImpl")}get text(){return this._text.substring(this.offset,this.end)}get fullText(){return this._text}constructor(e){super(),this._text="",this._text=e??""}},qR=Symbol("Datatype");s(JA,"isDataTypeNode");E(JA,"isDataTypeNode");KSe="\u200B",mLe=E(e=>e.endsWith(KSe)?e:e+KSe,"withRuleSuffix"),gH=class{static{s(this,"AbstractLangiumParser")}static{E(this,"AbstractLangiumParser")}constructor(e){this._unorderedGroups=new Map,this.allRules=new Map,this.lexer=e.parser.Lexer;let t=this.lexer.definition,r=e.LanguageMetaData.mode==="production";e.shared.profilers.LangiumProfiler?.isActive("parsing")?this.wrapper=new _kt(t,{...e.parser.ParserConfig,skipValidations:r,errorMessageProvider:e.parser.ParserErrorMessageProvider},e.shared.profilers.LangiumProfiler.createTask("parsing",e.LanguageMetaData.languageId)):this.wrapper=new xLe(t,{...e.parser.ParserConfig,skipValidations:r,errorMessageProvider:e.parser.ParserErrorMessageProvider})}alternatives(e,t){this.wrapper.wrapOr(e,t)}optional(e,t){this.wrapper.wrapOption(e,t)}many(e,t){this.wrapper.wrapMany(e,t)}atLeastOne(e,t){this.wrapper.wrapAtLeastOne(e,t)}getRule(e){return this.allRules.get(e)}isRecording(){return this.wrapper.IS_RECORDING}get unorderedGroups(){return this._unorderedGroups}getRuleStack(){return this.wrapper.RULE_STACK}finalize(){this.wrapper.wrapSelfAnalysis()}},gLe=class extends gH{static{s(this,"LangiumParser")}static{E(this,"LangiumParser")}get current(){return this.stack[this.stack.length-1]}constructor(e){super(e),this.nodeBuilder=new fLe,this.stack=[],this.assignmentMap=new Map,this.operatorPrecedence=new Map,this.linker=e.references.Linker,this.converter=e.parser.ValueConverter,this.astReflection=e.shared.AstReflection}rule(e,t){let r=this.computeRuleType(e),n;Cv(e)&&(n=e.name,this.registerPrecedenceMap(e));let i=this.wrapper.DEFINE_RULE(mLe(e.name),this.startImplementation(r,n,t).bind(this));return this.allRules.set(e.name,i),Ks(e)&&e.entry&&(this.mainRule=i),i}registerPrecedenceMap(e){let t=e.name,r=new Map;for(let n=0;n0&&(t=this.construct()),t===void 0)throw new Error("No result from parser");if(this.stack.length>0)throw new Error("Parser stack is not empty after parsing");return t}startImplementation(e,t,r){return n=>{let i=!this.isRecording()&&e!==void 0;if(i){let a={$type:e};this.stack.push(a),e===qR?a.value="":t!==void 0&&(a.$infixName=t)}return r(n),i?this.construct():void 0}}extractHiddenTokens(e){let t=this.lexerResult.hidden;if(!t.length)return[];let r=e.startOffset;for(let n=0;nr)return t.splice(0,n);return t.splice(0,t.length)}consume(e,t,r){let n=this.wrapper.wrapConsume(e,t);if(!this.isRecording()&&this.isValidToken(n)){let i=this.extractHiddenTokens(n);this.nodeBuilder.addHiddenNodes(i);let a=this.nodeBuilder.buildLeafNode(n,r),{assignment:o,crossRef:l}=this.getAssignment(r),u=this.current;if(o){let h=Xh(r)?n.image:this.converter.convert(n.image,a);this.assign(o.operator,o.feature,h,a,l)}else if(JA(u)){let h=n.image;Xh(r)||(h=this.converter.convert(h,a).toString()),u.value+=h}}}isValidToken(e){return!e.isInsertedInRecovery&&!isNaN(e.startOffset)&&typeof e.endOffset=="number"&&!isNaN(e.endOffset)}subrule(e,t,r,n,i){let a;!this.isRecording()&&!r&&(a=this.nodeBuilder.buildCompositeNode(n));let o;try{o=this.wrapper.wrapSubrule(e,t,i)}finally{this.isRecording()||(o===void 0&&!r&&(o=this.construct()),o!==void 0&&a&&a.length>0&&this.performSubruleAssignment(o,n,a))}}performSubruleAssignment(e,t,r){let{assignment:n,crossRef:i}=this.getAssignment(t);if(n)this.assign(n.operator,n.feature,e,r,i);else if(!n){let a=this.current;if(JA(a))a.value+=e.toString();else if(typeof e=="object"&&e){let l=this.assignWithoutOverride(e,a);this.stack.pop(),this.stack.push(l)}}}action(e,t){if(!this.isRecording()){let r=this.current;if(t.feature&&t.operator){r=this.construct(),this.nodeBuilder.removeNode(r.$cstNode),this.nodeBuilder.buildCompositeNode(t).content.push(r.$cstNode);let i={$type:e};this.stack.push(i),this.assign(t.operator,t.feature,r,r.$cstNode)}else r.$type=e}}construct(){if(this.isRecording())return;let e=this.stack.pop();return this.nodeBuilder.construct(e),"$infixName"in e?this.constructInfix(e,this.operatorPrecedence.get(e.$infixName)):JA(e)?this.converter.convert(e.value,e.$cstNode):(pW(this.astReflection,e),e)}constructInfix(e,t){let r=e.parts;if(!Array.isArray(r)||r.length===0)return;let n=e.operators;if(!Array.isArray(n)||r.length<2)return r[0];let i=0,a=-1;for(let g=0;ga?(a=v.precedence,i=g):v.precedence===a&&(v.rightAssoc||(i=g))}let o=n.slice(0,i),l=n.slice(i+1),u=r.slice(0,i+1),h=r.slice(i+1),d={$infixName:e.$infixName,$type:e.$type,$cstNode:e.$cstNode,parts:u,operators:o},f={$infixName:e.$infixName,$type:e.$type,$cstNode:e.$cstNode,parts:h,operators:l},p=this.constructInfix(d,t),m=this.constructInfix(f,t);return{$type:e.$type,$cstNode:e.$cstNode,left:p,operator:n[i],right:m}}getAssignment(e){if(!this.assignmentMap.has(e)){let t=e0(e,jh);this.assignmentMap.set(e,{assignment:t,crossRef:t&&r0(t.terminal)?t.terminal.isMulti?"multi":"single":void 0})}return this.assignmentMap.get(e)}assign(e,t,r,n,i){let a=this.current,o;switch(i==="single"&&typeof r=="string"?o=this.linker.buildReference(a,t,n,r):i==="multi"&&typeof r=="string"?o=this.linker.buildMultiReference(a,t,n,r):o=r,e){case"=":{a[t]=o;break}case"?=":{a[t]=!0;break}case"+=":Array.isArray(a[t])||(a[t]=[]),a[t].push(o)}}assignWithoutOverride(e,t){for(let[n,i]of Object.entries(t)){let a=e[n];a===void 0?e[n]=i:Array.isArray(a)&&Array.isArray(i)&&(i.push(...a),e[n]=i)}let r=e.$cstNode;return r&&(r.astNode=void 0,e.$cstNode=void 0),e}get definitionErrors(){return this.wrapper.definitionErrors}},yLe=class{static{s(this,"AbstractParserErrorMessageProvider")}static{E(this,"AbstractParserErrorMessageProvider")}buildMismatchTokenMessage(e){return hv.buildMismatchTokenMessage(e)}buildNotAllInputParsedMessage(e){return hv.buildNotAllInputParsedMessage(e)}buildNoViableAltMessage(e){return hv.buildNoViableAltMessage(e)}buildEarlyExitMessage(e){return hv.buildEarlyExitMessage(e)}},yH=class extends yLe{static{s(this,"LangiumParserErrorMessageProvider")}static{E(this,"LangiumParserErrorMessageProvider")}buildMismatchTokenMessage({expected:e,actual:t}){return`Expecting ${e.LABEL?"`"+e.LABEL+"`":e.name.endsWith(":KW")?`keyword '${e.name.substring(0,e.name.length-3)}'`:`token of type '${e.name}'`} but found \`${t.image}\`.`}buildNotAllInputParsedMessage({firstRedundant:e}){return`Expecting end of file but found \`${e.image}\`.`}},vLe=class extends gH{static{s(this,"LangiumCompletionParser")}static{E(this,"LangiumCompletionParser")}constructor(){super(...arguments),this.tokens=[],this.elementStack=[],this.lastElementStack=[],this.nextTokenIndex=0,this.stackSize=0}action(){}construct(){}parse(e){this.resetState();let t=this.lexer.tokenize(e,{mode:"partial"});return this.tokens=t.tokens,this.wrapper.input=[...this.tokens],this.mainRule.call(this.wrapper,{}),this.unorderedGroups.clear(),{tokens:this.tokens,elementStack:[...this.lastElementStack],tokenIndex:this.nextTokenIndex}}rule(e,t){let r=this.wrapper.DEFINE_RULE(mLe(e.name),this.startImplementation(t).bind(this));return this.allRules.set(e.name,r),e.entry&&(this.mainRule=r),r}resetState(){this.elementStack=[],this.lastElementStack=[],this.nextTokenIndex=0,this.stackSize=0}startImplementation(e){return t=>{let r=this.keepStackSize();try{e(t)}finally{this.resetStackSize(r)}}}removeUnexpectedElements(){this.elementStack.splice(this.stackSize)}keepStackSize(){let e=this.elementStack.length;return this.stackSize=e,e}resetStackSize(e){this.removeUnexpectedElements(),this.stackSize=e}consume(e,t,r){this.wrapper.wrapConsume(e,t),this.isRecording()||(this.lastElementStack=[...this.elementStack,r],this.nextTokenIndex=this.currIdx+1)}subrule(e,t,r,n,i){this.before(n),this.wrapper.wrapSubrule(e,t,i),this.after(n)}before(e){this.isRecording()||this.elementStack.push(e)}after(e){if(!this.isRecording()){let t=this.elementStack.lastIndexOf(e);t>=0&&this.elementStack.splice(t)}}get currIdx(){return this.wrapper.currIdx}},Rkt={recoveryEnabled:!0,nodeLocationTracking:"full",skipValidations:!0,errorMessageProvider:new yH},xLe=class extends dTt{static{s(this,"ChevrotainWrapper")}static{E(this,"ChevrotainWrapper")}constructor(e,t){let r=t&&"maxLookahead"in t;super(e,{...Rkt,lookaheadStrategy:r?new Uq({maxLookahead:t.maxLookahead}):new Ekt({logging:t.skipValidations?()=>{}:void 0}),...t})}get IS_RECORDING(){return this.RECORDING_PHASE}DEFINE_RULE(e,t,r){return this.RULE(e,t,r)}wrapSelfAnalysis(){this.performSelfAnalysis()}wrapConsume(e,t){return this.consume(e,t,void 0)}wrapSubrule(e,t,r){return this.subrule(e,t,{ARGS:[r]})}wrapOr(e,t){this.or(e,t)}wrapOption(e,t){this.option(e,t)}wrapMany(e,t){this.many(e,t)}wrapAtLeastOne(e,t){this.atLeastOne(e,t)}rule(e){return e.call(this,{})}},_kt=class extends xLe{static{s(this,"ProfilerWrapper")}static{E(this,"ProfilerWrapper")}constructor(e,t,r){super(e,t),this.task=r}rule(e){this.task.start(),this.task.startSubTask(this.ruleName(e));try{return super.rule(e)}finally{this.task.stopSubTask(this.ruleName(e)),this.task.stop()}}ruleName(e){return e.ruleName}subrule(e,t,r){this.task.startSubTask(this.ruleName(t));try{return super.subrule(e,t,r)}finally{this.task.stopSubTask(this.ruleName(t))}}};s(Q6,"createParser");E(Q6,"createParser");s(bLe,"buildRules");E(bLe,"buildRules");s(TLe,"buildInfixRule");E(TLe,"buildInfixRule");s(vp,"buildElement");E(vp,"buildElement");s(CLe,"buildAction");E(CLe,"buildAction");s(SLe,"buildRuleCall");E(SLe,"buildRuleCall");s(wLe,"buildRuleCallPredicate");E(wLe,"buildRuleCallPredicate");s(Tc,"buildPredicate");E(Tc,"buildPredicate");s(kLe,"buildAlternatives");E(kLe,"buildAlternatives");s(ELe,"buildUnorderedGroup");E(ELe,"buildUnorderedGroup");s(ALe,"buildGroup");E(ALe,"buildGroup");s(oS,"getGuardCondition");E(oS,"getGuardCondition");s(vH,"buildCrossReference");E(vH,"buildCrossReference");s(RLe,"buildKeyword");E(RLe,"buildKeyword");s(xH,"wrap");E(xH,"wrap");s(J6,"getRule");E(J6,"getRule");s(_Le,"getRuleName");E(_Le,"getRuleName");s(HR,"getToken");E(HR,"getToken");s(bH,"createCompletionParser");E(bH,"createCompletionParser");s(TH,"createLangiumParser");E(TH,"createLangiumParser");s(CH,"prepareLangiumParser");E(CH,"prepareLangiumParser");e_=class{static{s(this,"DefaultTokenBuilder")}static{E(this,"DefaultTokenBuilder")}constructor(){this.diagnostics=[]}buildTokens(e,t){let r=zn(f6(e,!1)),n=this.buildTerminalTokens(r),i=this.buildKeywordTokens(r,n,t);return i.push(...n),i}flushLexingReport(e){return{diagnostics:this.popDiagnostics()}}popDiagnostics(){let e=[...this.diagnostics];return this.diagnostics=[],e}buildTerminalTokens(e){return e.filter(Ol).filter(t=>!t.fragment).map(t=>this.buildTerminalToken(t)).toArray()}buildTerminalToken(e){let t=mS(e),r=this.requiresCustomPattern(t)?this.regexPatternFunction(t):t,n={name:e.name,PATTERN:r};return typeof r=="function"&&(n.LINE_BREAKS=!0),e.hidden&&(n.GROUP=d6(t)?Xs.SKIPPED:"hidden"),n}requiresCustomPattern(e){return!!(e.flags.includes("u")||e.flags.includes("s"))}regexPatternFunction(e){let t=new RegExp(e,e.flags+"y");return(r,n)=>(t.lastIndex=n,t.exec(r))}buildKeywordTokens(e,t,r){return e.filter(t0).flatMap(n=>nd(n).filter(Xh)).distinct(n=>n.value).toArray().sort((n,i)=>i.value.length-n.value.length).map(n=>this.buildKeywordToken(n,t,!!r?.caseInsensitive))}buildKeywordToken(e,t,r){let n=this.buildKeywordPattern(e,r),i={name:e.value,PATTERN:n,LONGER_ALT:this.findLongerAlt(e,t)};return typeof n=="function"&&(i.LINE_BREAKS=!0),i}buildKeywordPattern(e,t){return t?new RegExp(Dv(e.value),"i"):e.value}findLongerAlt(e,t){return t.reduce((r,n)=>{let i=n?.PATTERN;return i?.source&&YW("^"+i.source+"$",e.value)&&r.push(n),r},[])}},SH=class{static{s(this,"DefaultValueConverter")}static{E(this,"DefaultValueConverter")}convert(e,t){let r=t.grammarSource;if(r0(r)&&(r=QW(r)),Kh(r)){let n=r.rule.ref;if(!n)throw new Error("This cst node was not parsed by a rule.");return this.runConverter(n,e,t)}return e}runConverter(e,t,r){switch(e.name.toUpperCase()){case"INT":return Nu.convertInt(t);case"STRING":return Nu.convertString(t);case"ID":return Nu.convertID(t)}switch(oq(e)?.toLowerCase()){case"number":return Nu.convertNumber(t);case"boolean":return Nu.convertBoolean(t);case"bigint":return Nu.convertBigint(t);case"date":return Nu.convertDate(t);default:return t}}};(function(e){function t(h){let d="";for(let f=1;f{this.resolve=r=>(e(r),this),this.reject=r=>(t(r),this)})}},ZSe=class nV{static{s(this,"_FullTextDocument")}static{E(this,"FullTextDocument")}constructor(t,r,n,i){this._uri=t,this._languageId=r,this._version=n,this._content=i,this._lineOffsets=void 0}get uri(){return this._uri}get languageId(){return this._languageId}get version(){return this._version}getText(t){if(t){let r=this.offsetAt(t.start),n=this.offsetAt(t.end);return this._content.substring(r,n)}return this._content}update(t,r){for(let n of t)if(nV.isIncremental(n)){let i=EH(n.range),a=this.offsetAt(i.start),o=this.offsetAt(i.end);this._content=this._content.substring(0,a)+n.text+this._content.substring(o,this._content.length);let l=Math.max(i.start.line,0),u=Math.max(i.end.line,0),h=this._lineOffsets,d=iV(n.text,!1,a);if(u-l===d.length)for(let p=0,m=d.length;pt?i=o:n=o+1}let a=n-1;return t=this.ensureBeforeEOL(t,r[a]),{line:a,character:t-r[a]}}offsetAt(t){let r=this.getLineOffsets();if(t.line>=r.length)return this._content.length;if(t.line<0)return 0;let n=r[t.line];if(t.character<=0)return n;let i=t.line+1r&&kH(this._content.charCodeAt(t-1));)t--;return t}get lineCount(){return this.getLineOffsets().length}static isIncremental(t){let r=t;return r!=null&&typeof r.text=="string"&&r.range!==void 0&&(r.rangeLength===void 0||typeof r.rangeLength=="number")}static isFull(t){let r=t;return r!=null&&typeof r.text=="string"&&r.range===void 0&&r.rangeLength===void 0}};(function(e){function t(i,a,o,l){return new ZSe(i,a,o,l)}s(t,"create"),E(t,"create"),e.create=t;function r(i,a,o){if(i instanceof ZSe)return i.update(a,o),i;throw new Error("TextDocument.update: document must be created by TextDocument.create")}s(r,"update"),E(r,"update"),e.update=r;function n(i,a){let o=i.getText(),l=YR(a.map(DLe),(d,f)=>{let p=d.range.start.line-f.range.start.line;return p===0?d.range.start.character-f.range.start.character:p}),u=0,h=[];for(let d of l){let f=i.offsetAt(d.range.start);if(fu&&h.push(o.substring(u,f)),d.newText.length&&h.push(d.newText),u=i.offsetAt(d.range.end)}return h.push(o.substr(u)),h.join("")}s(n,"applyEdits"),E(n,"applyEdits"),e.applyEdits=n})(UR||(UR={}));s(YR,"mergeSort");E(YR,"mergeSort");s(iV,"computeLineOffsets");E(iV,"computeLineOffsets");s(kH,"isEOL");E(kH,"isEOL");s(EH,"getWellformedRange");E(EH,"getWellformedRange");s(DLe,"getWellformedEdit");E(DLe,"getWellformedEdit");(()=>{"use strict";var e={975:N=>{function _(D){if(typeof D!="string")throw new TypeError("Path must be a string. Received "+JSON.stringify(D))}s(_,"e2"),E(_,"e");function A(D,L){for(var B,O="",$=0,W=-1,V=0,z=0;z<=D.length;++z){if(z2){var H=O.lastIndexOf("/");if(H!==O.length-1){H===-1?(O="",$=0):$=(O=O.slice(0,H)).length-1-O.lastIndexOf("/"),W=z,V=0;continue}}else if(O.length===2||O.length===1){O="",$=0,W=z,V=0;continue}}L&&(O.length>0?O+="/..":O="..",$=2)}else O.length>0?O+="/"+D.slice(W+1,z):O=D.slice(W+1,z),$=z-W-1;W=z,V=0}else B===46&&V!==-1?++V:V=-1}return O}s(A,"r2"),E(A,"r");var I={resolve:E(function(){for(var D,L="",B=!1,O=arguments.length-1;O>=-1&&!B;O--){var $;O>=0?$=arguments[O]:(D===void 0&&(D=process.cwd()),$=D),_($),$.length!==0&&(L=$+"/"+L,B=$.charCodeAt(0)===47)}return L=A(L,!B),B?L.length>0?"/"+L:"/":L.length>0?L:"."},"resolve"),normalize:E(function(D){if(_(D),D.length===0)return".";var L=D.charCodeAt(0)===47,B=D.charCodeAt(D.length-1)===47;return(D=A(D,!L)).length!==0||L||(D="."),D.length>0&&B&&(D+="/"),L?"/"+D:D},"normalize"),isAbsolute:E(function(D){return _(D),D.length>0&&D.charCodeAt(0)===47},"isAbsolute"),join:E(function(){if(arguments.length===0)return".";for(var D,L=0;L0&&(D===void 0?D=B:D+="/"+B)}return D===void 0?".":I.normalize(D)},"join"),relative:E(function(D,L){if(_(D),_(L),D===L||(D=I.resolve(D))===(L=I.resolve(L)))return"";for(var B=1;Bz){if(L.charCodeAt(W+K)===47)return L.slice(W+K+1);if(K===0)return L.slice(W+K)}else $>z&&(D.charCodeAt(B+K)===47?H=K:K===0&&(H=0));break}var J=D.charCodeAt(B+K);if(J!==L.charCodeAt(W+K))break;J===47&&(H=K)}var ne="";for(K=B+H+1;K<=O;++K)K!==O&&D.charCodeAt(K)!==47||(ne.length===0?ne+="..":ne+="/..");return ne.length>0?ne+L.slice(W+H):(W+=H,L.charCodeAt(W)===47&&++W,L.slice(W))},"relative"),_makeLong:E(function(D){return D},"_makeLong"),dirname:E(function(D){if(_(D),D.length===0)return".";for(var L=D.charCodeAt(0),B=L===47,O=-1,$=!0,W=D.length-1;W>=1;--W)if((L=D.charCodeAt(W))===47){if(!$){O=W;break}}else $=!1;return O===-1?B?"/":".":B&&O===1?"//":D.slice(0,O)},"dirname"),basename:E(function(D,L){if(L!==void 0&&typeof L!="string")throw new TypeError('"ext" argument must be a string');_(D);var B,O=0,$=-1,W=!0;if(L!==void 0&&L.length>0&&L.length<=D.length){if(L.length===D.length&&L===D)return"";var V=L.length-1,z=-1;for(B=D.length-1;B>=0;--B){var H=D.charCodeAt(B);if(H===47){if(!W){O=B+1;break}}else z===-1&&(W=!1,z=B+1),V>=0&&(H===L.charCodeAt(V)?--V==-1&&($=B):(V=-1,$=z))}return O===$?$=z:$===-1&&($=D.length),D.slice(O,$)}for(B=D.length-1;B>=0;--B)if(D.charCodeAt(B)===47){if(!W){O=B+1;break}}else $===-1&&(W=!1,$=B+1);return $===-1?"":D.slice(O,$)},"basename"),extname:E(function(D){_(D);for(var L=-1,B=0,O=-1,$=!0,W=0,V=D.length-1;V>=0;--V){var z=D.charCodeAt(V);if(z!==47)O===-1&&($=!1,O=V+1),z===46?L===-1?L=V:W!==1&&(W=1):L!==-1&&(W=-1);else if(!$){B=V+1;break}}return L===-1||O===-1||W===0||W===1&&L===O-1&&L===B+1?"":D.slice(L,O)},"extname"),format:E(function(D){if(D===null||typeof D!="object")throw new TypeError('The "pathObject" argument must be of type Object. Received type '+typeof D);return(function(L,B){var O=B.dir||B.root,$=B.base||(B.name||"")+(B.ext||"");return O?O===B.root?O+$:O+"/"+$:$})(0,D)},"format"),parse:E(function(D){_(D);var L={root:"",dir:"",base:"",ext:"",name:""};if(D.length===0)return L;var B,O=D.charCodeAt(0),$=O===47;$?(L.root="/",B=1):B=0;for(var W=-1,V=0,z=-1,H=!0,K=D.length-1,J=0;K>=B;--K)if((O=D.charCodeAt(K))!==47)z===-1&&(H=!1,z=K+1),O===46?W===-1?W=K:J!==1&&(J=1):W!==-1&&(J=-1);else if(!H){V=K+1;break}return W===-1||z===-1||J===0||J===1&&W===z-1&&W===V+1?z!==-1&&(L.base=L.name=V===0&&$?D.slice(1,z):D.slice(V,z)):(V===0&&$?(L.name=D.slice(1,W),L.base=D.slice(1,z)):(L.name=D.slice(V,W),L.base=D.slice(V,z)),L.ext=D.slice(W,z)),V>0?L.dir=D.slice(0,V-1):$&&(L.dir="/"),L},"parse"),sep:"/",delimiter:":",win32:null,posix:null};I.posix=I,N.exports=I}},t={};function r(N){var _=t[N];if(_!==void 0)return _.exports;var A=t[N]={exports:{}};return e[N](A,A.exports,r),A.exports}s(r,"r"),E(r,"r"),r.d=(N,_)=>{for(var A in _)r.o(_,A)&&!r.o(N,A)&&Object.defineProperty(N,A,{enumerable:!0,get:_[A]})},r.o=(N,_)=>Object.prototype.hasOwnProperty.call(N,_),r.r=N=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(N,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(N,"__esModule",{value:!0})};var n={};let i;r.r(n),r.d(n,{URI:E(()=>p,"URI"),Utils:E(()=>P,"Utils")}),typeof process=="object"?i=process.platform==="win32":typeof navigator=="object"&&(i=navigator.userAgent.indexOf("Windows")>=0);let a=/^\w[\w\d+.-]*$/,o=/^\//,l=/^\/\//;function u(N,_){if(!N.scheme&&_)throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${N.authority}", path: "${N.path}", query: "${N.query}", fragment: "${N.fragment}"}`);if(N.scheme&&!a.test(N.scheme))throw new Error("[UriError]: Scheme contains illegal characters.");if(N.path){if(N.authority){if(!o.test(N.path))throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character')}else if(l.test(N.path))throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")')}}s(u,"a"),E(u,"a");let h="",d="/",f=/^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;class p{static{s(this,"l")}static{E(this,"l")}static isUri(_){return _ instanceof p||!!_&&typeof _.authority=="string"&&typeof _.fragment=="string"&&typeof _.path=="string"&&typeof _.query=="string"&&typeof _.scheme=="string"&&typeof _.fsPath=="string"&&typeof _.with=="function"&&typeof _.toString=="function"}scheme;authority;path;query;fragment;constructor(_,A,I,D,L,B=!1){typeof _=="object"?(this.scheme=_.scheme||h,this.authority=_.authority||h,this.path=_.path||h,this.query=_.query||h,this.fragment=_.fragment||h):(this.scheme=(function(O,$){return O||$?O:"file"})(_,B),this.authority=A||h,this.path=(function(O,$){switch(O){case"https":case"http":case"file":$?$[0]!==d&&($=d+$):$=d}return $})(this.scheme,I||h),this.query=D||h,this.fragment=L||h,u(this,B))}get fsPath(){return b(this,!1)}with(_){if(!_)return this;let{scheme:A,authority:I,path:D,query:L,fragment:B}=_;return A===void 0?A=this.scheme:A===null&&(A=h),I===void 0?I=this.authority:I===null&&(I=h),D===void 0?D=this.path:D===null&&(D=h),L===void 0?L=this.query:L===null&&(L=h),B===void 0?B=this.fragment:B===null&&(B=h),A===this.scheme&&I===this.authority&&D===this.path&&L===this.query&&B===this.fragment?this:new g(A,I,D,L,B)}static parse(_,A=!1){let I=f.exec(_);return I?new g(I[2]||h,S(I[4]||h),S(I[5]||h),S(I[7]||h),S(I[9]||h),A):new g(h,h,h,h,h)}static file(_){let A=h;if(i&&(_=_.replace(/\\/g,d)),_[0]===d&&_[1]===d){let I=_.indexOf(d,2);I===-1?(A=_.substring(2),_=d):(A=_.substring(2,I),_=_.substring(I)||d)}return new g("file",A,_,h,h)}static from(_){let A=new g(_.scheme,_.authority,_.path,_.query,_.fragment);return u(A,!0),A}toString(_=!1){return T(this,_)}toJSON(){return this}static revive(_){if(_){if(_ instanceof p)return _;{let A=new g(_);return A._formatted=_.external,A._fsPath=_._sep===m?_.fsPath:null,A}}return _}}let m=i?1:void 0;class g extends p{static{s(this,"d")}static{E(this,"d")}_formatted=null;_fsPath=null;get fsPath(){return this._fsPath||(this._fsPath=b(this,!1)),this._fsPath}toString(_=!1){return _?T(this,!0):(this._formatted||(this._formatted=T(this,!1)),this._formatted)}toJSON(){let _={$mid:1};return this._fsPath&&(_.fsPath=this._fsPath,_._sep=m),this._formatted&&(_.external=this._formatted),this.path&&(_.path=this.path),this.scheme&&(_.scheme=this.scheme),this.authority&&(_.authority=this.authority),this.query&&(_.query=this.query),this.fragment&&(_.fragment=this.fragment),_}}let y={58:"%3A",47:"%2F",63:"%3F",35:"%23",91:"%5B",93:"%5D",64:"%40",33:"%21",36:"%24",38:"%26",39:"%27",40:"%28",41:"%29",42:"%2A",43:"%2B",44:"%2C",59:"%3B",61:"%3D",32:"%20"};function v(N,_,A){let I,D=-1;for(let L=0;L=97&&B<=122||B>=65&&B<=90||B>=48&&B<=57||B===45||B===46||B===95||B===126||_&&B===47||A&&B===91||A&&B===93||A&&B===58)D!==-1&&(I+=encodeURIComponent(N.substring(D,L)),D=-1),I!==void 0&&(I+=N.charAt(L));else{I===void 0&&(I=N.substr(0,L));let O=y[B];O!==void 0?(D!==-1&&(I+=encodeURIComponent(N.substring(D,L)),D=-1),I+=O):D===-1&&(D=L)}}return D!==-1&&(I+=encodeURIComponent(N.substring(D))),I!==void 0?I:N}s(v,"m"),E(v,"m");function x(N){let _;for(let A=0;A1&&N.scheme==="file"?`//${N.authority}${N.path}`:N.path.charCodeAt(0)===47&&(N.path.charCodeAt(1)>=65&&N.path.charCodeAt(1)<=90||N.path.charCodeAt(1)>=97&&N.path.charCodeAt(1)<=122)&&N.path.charCodeAt(2)===58?_?N.path.substr(1):N.path[1].toLowerCase()+N.path.substr(2):N.path,i&&(A=A.replace(/\//g,"\\")),A}s(b,"v"),E(b,"v");function T(N,_){let A=_?x:v,I="",{scheme:D,authority:L,path:B,query:O,fragment:$}=N;if(D&&(I+=D,I+=":"),(L||D==="file")&&(I+=d,I+=d),L){let W=L.indexOf("@");if(W!==-1){let V=L.substr(0,W);L=L.substr(W+1),W=V.lastIndexOf(":"),W===-1?I+=A(V,!1,!1):(I+=A(V.substr(0,W),!1,!1),I+=":",I+=A(V.substr(W+1),!1,!0)),I+="@"}L=L.toLowerCase(),W=L.lastIndexOf(":"),W===-1?I+=A(L,!1,!0):(I+=A(L.substr(0,W),!1,!0),I+=L.substr(W))}if(B){if(B.length>=3&&B.charCodeAt(0)===47&&B.charCodeAt(2)===58){let W=B.charCodeAt(1);W>=65&&W<=90&&(B=`/${String.fromCharCode(W+32)}:${B.substr(3)}`)}else if(B.length>=2&&B.charCodeAt(1)===58){let W=B.charCodeAt(0);W>=65&&W<=90&&(B=`${String.fromCharCode(W+32)}:${B.substr(2)}`)}I+=A(B,!0,!1)}return O&&(I+="?",I+=A(O,!1,!1)),$&&(I+="#",I+=_?$:v($,!1,!1)),I}s(T,"b"),E(T,"b");function w(N){try{return decodeURIComponent(N)}catch{return N.length>3?N.substr(0,3)+w(N.substr(3)):N}}s(w,"C"),E(w,"C");let C=/(%[0-9A-Za-z][0-9A-Za-z])+/g;function S(N){return N.match(C)?N.replace(C,(_=>w(_))):N}s(S,"w"),E(S,"w");var k=r(975);let R=k.posix||k,M="/";var P;(function(N){N.joinPath=function(_,...A){return _.with({path:R.join(_.path,...A)})},N.resolvePath=function(_,...A){let I=_.path,D=!1;I[0]!==M&&(I=M+I,D=!0);let L=R.resolve(I,...A);return D&&L[0]===M&&!_.authority&&(L=L.substring(1)),_.with({path:L})},N.dirname=function(_){if(_.path.length===0||_.path===M)return _;let A=R.dirname(_.path);return A.length===1&&A.charCodeAt(0)===46&&(A=""),_.with({path:A})},N.basename=function(_){return R.basename(_.path)},N.extname=function(_){return R.extname(_.path)}})(P||(P={})),ILe=n})();({URI:Jo,Utils:cC}=ILe);(function(e){e.basename=cC.basename,e.dirname=cC.dirname,e.extname=cC.extname,e.joinPath=cC.joinPath,e.resolvePath=cC.resolvePath;let t=typeof process=="object"&&process?.platform==="win32";function r(o,l){return o?.toString()===l?.toString()}s(r,"equals"),E(r,"equals"),e.equals=r;function n(o,l){let u=typeof o=="string"?Jo.parse(o).path:o.path,h=typeof l=="string"?Jo.parse(l).path:l.path,d=u.split("/").filter(y=>y.length>0),f=h.split("/").filter(y=>y.length>0);if(t){let y=/^[A-Z]:$/;if(d[0]&&y.test(d[0])&&(d[0]=d[0].toLowerCase()),f[0]&&y.test(f[0])&&(f[0]=f[0].toLowerCase()),d[0]!==f[0])return h.substring(1)}let p=0;for(;p({name:n.name,uri:js.joinPath(Jo.parse(t),n.name).toString(),element:n.element})):[]}all(){return this.collectValues(this.root)}findAll(e){let t=this.getNode(js.normalize(e),!1);return t?this.collectValues(t):[]}getNode(e,t){let r=e.split("/");e.charAt(e.length-1)==="/"&&r.pop();let n=this.root;for(let i of r){let a=n.children.get(i);if(!a)if(t)a={name:i,children:new Map,parent:n},n.children.set(i,a);else return;n=a}return n}collectValues(e){let t=[];e.element&&t.push(e.element);for(let r of e.children.values())t.push(...this.collectValues(r));return t}};(function(e){e[e.Changed=0]="Changed",e[e.Parsed=1]="Parsed",e[e.IndexedContent=2]="IndexedContent",e[e.ComputedScopes=3]="ComputedScopes",e[e.Linked=4]="Linked",e[e.IndexedReferences=5]="IndexedReferences",e[e.Validated=6]="Validated"})(nn||(nn={}));MLe=class{static{s(this,"DefaultLangiumDocumentFactory")}static{E(this,"DefaultLangiumDocumentFactory")}constructor(e){this.serviceRegistry=e.ServiceRegistry,this.textDocuments=e.workspace.TextDocuments,this.fileSystemProvider=e.workspace.FileSystemProvider}async fromUri(e,t=ri.CancellationToken.None){let r=await this.fileSystemProvider.readFile(e);return this.createAsync(e,r,t)}fromTextDocument(e,t,r){return t=t??Jo.parse(e.uri),ri.CancellationToken.is(r)?this.createAsync(t,e,r):this.create(t,e,r)}fromString(e,t,r){return ri.CancellationToken.is(r)?this.createAsync(t,e,r):this.create(t,e,r)}fromModel(e,t){return this.create(t,{$model:e})}create(e,t,r){if(typeof t=="string"){let n=this.parse(e,t,r);return this.createLangiumDocument(n,e,void 0,t)}else if("$model"in t){let n={value:t.$model,parserErrors:[],lexerErrors:[]};return this.createLangiumDocument(n,e)}else{let n=this.parse(e,t.getText(),r);return this.createLangiumDocument(n,e,t)}}async createAsync(e,t,r){if(typeof t=="string"){let n=await this.parseAsync(e,t,r);return this.createLangiumDocument(n,e,void 0,t)}else{let n=await this.parseAsync(e,t.getText(),r);return this.createLangiumDocument(n,e,t)}}createLangiumDocument(e,t,r,n){let i;if(r)i={parseResult:e,uri:t,state:nn.Parsed,references:[],textDocument:r};else{let a=this.createTextDocumentGetter(t,n);i={parseResult:e,uri:t,state:nn.Parsed,references:[],get textDocument(){return a()}}}return e.value.$document=i,i}async update(e,t){let r=e.parseResult.value.$cstNode?.root.fullText,n=this.textDocuments?.get(e.uri.toString()),i=n?n.getText():await this.fileSystemProvider.readFile(e.uri);if(n)Object.defineProperty(e,"textDocument",{value:n});else{let a=this.createTextDocumentGetter(e.uri,i);Object.defineProperty(e,"textDocument",{get:a})}return r!==i&&(e.parseResult=await this.parseAsync(e.uri,i,t),e.parseResult.value.$document=e),e.state=nn.Parsed,e}parse(e,t,r){return this.serviceRegistry.getServices(e).parser.LangiumParser.parse(t,r)}parseAsync(e,t,r){return this.serviceRegistry.getServices(e).parser.AsyncParser.parse(t,r)}createTextDocumentGetter(e,t){let r=this.serviceRegistry,n;return()=>n??(n=UR.create(e.toString(),r.getServices(e).LanguageMetaData.languageId,0,t??""))}},NLe=class{static{s(this,"DefaultLangiumDocuments")}static{E(this,"DefaultLangiumDocuments")}constructor(e){this.documentTrie=new AH,this.services=e,this.langiumDocumentFactory=e.workspace.LangiumDocumentFactory,this.documentBuilder=()=>e.workspace.DocumentBuilder}get all(){return zn(this.documentTrie.all())}addDocument(e){let t=e.uri.toString();if(this.documentTrie.has(t))throw new Error(`A document with the URI '${t}' is already present.`);this.documentTrie.insert(t,e)}getDocument(e){let t=e.toString();return this.documentTrie.find(t)}getDocuments(e){let t=e.toString();return this.documentTrie.findAll(t)}async getOrCreateDocument(e,t){let r=this.getDocument(e);return r||(r=await this.langiumDocumentFactory.fromUri(e,t),this.addDocument(r),r)}createDocument(e,t,r){if(r)return this.langiumDocumentFactory.fromString(t,e,r).then(n=>(this.addDocument(n),n));{let n=this.langiumDocumentFactory.fromString(t,e);return this.addDocument(n),n}}hasDocument(e){return this.documentTrie.has(e.toString())}invalidateDocument(e){let t=e.toString(),r=this.documentTrie.find(t);return r&&this.documentBuilder().resetToState(r,nn.Changed),r}deleteDocument(e){let t=e.toString(),r=this.documentTrie.find(t);return r&&(r.state=nn.Changed,this.documentTrie.delete(t)),r}deleteDocuments(e){let t=e.toString(),r=this.documentTrie.findAll(t);for(let n of r)n.state=nn.Changed;return this.documentTrie.delete(t),r}},gg=Symbol("RefResolving"),PLe=class{static{s(this,"DefaultLinker")}static{E(this,"DefaultLinker")}constructor(e){this.reflection=e.shared.AstReflection,this.langiumDocuments=()=>e.shared.workspace.LangiumDocuments,this.scopeProvider=e.references.ScopeProvider,this.astNodeLocator=e.workspace.AstNodeLocator,this.profiler=e.shared.profilers.LangiumProfiler,this.languageId=e.LanguageMetaData.languageId}async link(e,t=ri.CancellationToken.None){if(this.profiler?.isActive("linking")){let r=this.profiler.createTask("linking",this.languageId);r.start();try{for(let n of Sc(e.parseResult.value))await qa(t),Tv(n).forEach(i=>{let a=`${n.$type}:${i.property}`;r.startSubTask(a);try{this.doLink(i,e)}finally{r.stopSubTask(a)}})}finally{r.stop()}}else for(let r of Sc(e.parseResult.value))await qa(t),Tv(r).forEach(n=>this.doLink(n,e))}doLink(e,t){let r=e.reference;if("_ref"in r&&r._ref===void 0){r._ref=gg;try{let n=this.getCandidate(e);if(Tg(n))r._ref=n;else{r._nodeDescription=n;let i=this.loadAstNode(n);r._ref=i??this.createLinkingError(e,n)}}catch(n){console.error(`An error occurred while resolving reference to '${r.$refText}':`,n);let i=n.message??String(n);r._ref={info:e,message:`An error occurred while resolving reference to '${r.$refText}': ${i}`}}t.references.push(r)}else if("_items"in r&&r._items===void 0){r._items=gg;try{let n=this.getCandidates(e),i=[];if(Tg(n))r._linkingError=n;else for(let a of n){let o=this.loadAstNode(a);o&&i.push({ref:o,$nodeDescription:a})}r._items=i}catch(n){r._linkingError={info:e,message:`An error occurred while resolving reference to '${r.$refText}': ${n}`},r._items=[]}t.references.push(r)}}unlink(e){for(let t of e.references)"_ref"in t?(t._ref=void 0,delete t._nodeDescription):"_items"in t&&(t._items=void 0,delete t._linkingError);e.references=[]}getCandidate(e){return this.scopeProvider.getScope(e).getElement(e.reference.$refText)??this.createLinkingError(e)}getCandidates(e){let r=this.scopeProvider.getScope(e).getElements(e.reference.$refText).distinct(n=>`${n.documentUri}#${n.path}`).toArray();return r.length>0?r:this.createLinkingError(e)}buildReference(e,t,r,n){let i=this,a={$refNode:r,$refText:n,_ref:void 0,get ref(){if(sa(this._ref))return this._ref;if(hW(this._nodeDescription)){let o=i.loadAstNode(this._nodeDescription);this._ref=o??i.createLinkingError({reference:a,container:e,property:t},this._nodeDescription)}else if(this._ref===void 0){this._ref=gg;let o=pv(e).$document,l=i.getLinkedNode({reference:a,container:e,property:t});if(l.error&&o&&o.state0))return this._linkingError=i.createLinkingError({reference:a,container:e,property:t})}};return a}throwCyclicReferenceError(e,t,r){throw new Error(`Cyclic reference resolution detected: ${this.astNodeLocator.getAstNodePath(e)}/${t} (symbol '${r}')`)}getLinkedNode(e){try{let t=this.getCandidate(e);if(Tg(t))return{error:t};let r=this.loadAstNode(t);return r?{node:r,descr:t}:{descr:t,error:this.createLinkingError(e,t)}}catch(t){console.error(`An error occurred while resolving reference to '${e.reference.$refText}':`,t);let r=t.message??String(t);return{error:{info:e,message:`An error occurred while resolving reference to '${e.reference.$refText}': ${r}`}}}}loadAstNode(e){if(e.node)return e.node;let t=this.langiumDocuments().getDocument(e.documentUri);if(t)return this.astNodeLocator.getAstNode(t.parseResult.value,e.path)}createLinkingError(e,t){let r=pv(e.container).$document;r&&r.stater0(t)&&t.isMulti)}findDeclarations(e){if(e){let t=rq(e),r=e.astNode;if(t&&r){let n=r[t.feature];if(Ys(n)||$u(n))return hR(n);if(Array.isArray(n)){for(let i of n)if((Ys(i)||$u(i))&&i.$refNode&&i.$refNode.offset<=e.offset&&i.$refNode.end>=e.end)return hR(i)}}if(r){let n=this.nameProvider.getNameNode(r);if(n&&(n===e||PW(e,n)))return this.getSelfNodes(r)}}return[]}getSelfNodes(e){if(this.hasMultiReference){let t=this.index.findAllReferences(e,this.nodeLocator.getAstNodePath(e)),r=this.getNodeFromReferenceDescription(t.head());if(r){for(let n of Tv(r))if($u(n.reference)&&n.reference.items.some(i=>i.ref===e))return n.reference.items.map(i=>i.ref)}return[e]}else return[e]}getNodeFromReferenceDescription(e){if(!e)return;let t=this.documents.getDocument(e.sourceUri);if(t)return this.nodeLocator.getAstNode(t.parseResult.value,e.sourcePath)}findDeclarationNodes(e){let t=this.findDeclarations(e),r=[];for(let n of t){let i=this.nameProvider.getNameNode(n)??n.$cstNode;i&&r.push(i)}return r}findReferences(e,t){let r=[];t.includeDeclaration&&r.push(...this.getSelfReferences(e));let n=this.index.findAllReferences(e,this.nodeLocator.getAstNodePath(e));return t.documentUri&&(n=n.filter(i=>js.equals(i.sourceUri,t.documentUri))),r.push(...n),zn(r)}getSelfReferences(e){let t=this.getSelfNodes(e),r=[];for(let n of t){let i=this.nameProvider.getNameNode(n);if(i){let a=Cc(n),o=this.nodeLocator.getAstNodePath(n);r.push({sourceUri:a.uri,sourcePath:o,targetUri:a.uri,targetPath:o,segment:wv(i),local:!0})}}return r}},rd=class{static{s(this,"MultiMap")}static{E(this,"MultiMap")}constructor(e){if(this.map=new Map,e)for(let[t,r]of e)this.add(t,r)}get size(){return UC.sum(zn(this.map.values()).map(e=>e.length))}clear(){this.map.clear()}delete(e,t){if(t===void 0)return this.map.delete(e);{let r=this.map.get(e);if(r){let n=r.indexOf(t);if(n>=0)return r.length===1?this.map.delete(e):r.splice(n,1),!0}return!1}}get(e){return this.map.get(e)??[]}getStream(e){let t=this.map.get(e);return t?zn(t):vv}has(e,t){if(t===void 0)return this.map.has(e);{let r=this.map.get(e);return r?r.indexOf(t)>=0:!1}}add(e,t){return this.map.has(e)?this.map.get(e).push(t):this.map.set(e,[t]),this}addAll(e,t){return this.map.has(e)?this.map.get(e).push(...t):this.map.set(e,Array.from(t)),this}forEach(e){this.map.forEach((t,r)=>t.forEach(n=>e(n,r,this)))}[Symbol.iterator](){return this.entries().iterator()}entries(){return zn(this.map.entries()).flatMap(([e,t])=>t.map(r=>[e,r]))}keys(){return zn(this.map.keys())}values(){return zn(this.map.values()).flat()}entriesGroupedByKey(){return zn(this.map.entries())}},jR=class{static{s(this,"BiMap")}static{E(this,"BiMap")}get size(){return this.map.size}constructor(e){if(this.map=new Map,this.inverse=new Map,e)for(let[t,r]of e)this.set(t,r)}clear(){this.map.clear(),this.inverse.clear()}set(e,t){return this.map.set(e,t),this.inverse.set(t,e),this}get(e){return this.map.get(e)}getKey(e){return this.inverse.get(e)}delete(e){let t=this.map.get(e);return t!==void 0?(this.map.delete(e),this.inverse.delete(t),!0):!1}},$Le=class{static{s(this,"DefaultScopeComputation")}static{E(this,"DefaultScopeComputation")}constructor(e){this.nameProvider=e.references.NameProvider,this.descriptions=e.workspace.AstNodeDescriptionProvider}async collectExportedSymbols(e,t=ri.CancellationToken.None){return this.collectExportedSymbolsForNode(e.parseResult.value,e,void 0,t)}async collectExportedSymbolsForNode(e,t,r=hS,n=ri.CancellationToken.None){let i=[];this.addExportedSymbol(e,i,t);for(let a of r(e))await qa(n),this.addExportedSymbol(a,i,t);return i}addExportedSymbol(e,t,r){let n=this.nameProvider.getName(e);n&&t.push(this.descriptions.createDescription(e,n,r))}async collectLocalSymbols(e,t=ri.CancellationToken.None){let r=e.parseResult.value,n=new rd;for(let i of nd(r))await qa(t),this.addLocalSymbol(i,e,n);return n}addLocalSymbol(e,t,r){let n=e.$container;if(n){let i=this.nameProvider.getName(e);i&&r.add(n,this.descriptions.createDescription(e,i,t))}}},aV=class{static{s(this,"StreamScope")}static{E(this,"StreamScope")}constructor(e,t,r){this.elements=e,this.outerScope=t,this.caseInsensitive=r?.caseInsensitive??!1,this.concatOuterScope=r?.concatOuterScope??!0}getAllElements(){return this.outerScope?this.elements.concat(this.outerScope.getAllElements()):this.elements}getElement(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.caseInsensitive?this.elements.find(n=>n.name.toLowerCase()===t):this.elements.find(n=>n.name===e);if(r)return r;if(this.outerScope)return this.outerScope.getElement(e)}getElements(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.caseInsensitive?this.elements.filter(n=>n.name.toLowerCase()===t):this.elements.filter(n=>n.name===e);return(this.concatOuterScope||r.isEmpty())&&this.outerScope?r.concat(this.outerScope.getElements(e)):r}},Lkt=class{static{s(this,"MapScope")}static{E(this,"MapScope")}constructor(e,t,r){this.elements=new Map,this.caseInsensitive=r?.caseInsensitive??!1,this.concatOuterScope=r?.concatOuterScope??!0;for(let n of e){let i=this.caseInsensitive?n.name.toLowerCase():n.name;this.elements.set(i,n)}this.outerScope=t}getElement(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.elements.get(t);if(r)return r;if(this.outerScope)return this.outerScope.getElement(e)}getElements(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.elements.get(t),n=r?[r]:[];return(this.concatOuterScope||n.length>0)&&this.outerScope?zn(n).concat(this.outerScope.getElements(e)):zn(n)}getAllElements(){let e=zn(this.elements.values());return this.outerScope&&(e=e.concat(this.outerScope.getAllElements())),e}},FLe=class{static{s(this,"MultiMapScope")}static{E(this,"MultiMapScope")}constructor(e,t,r){this.elements=new rd,this.caseInsensitive=r?.caseInsensitive??!1,this.concatOuterScope=r?.concatOuterScope??!0;for(let n of e){let i=this.caseInsensitive?n.name.toLowerCase():n.name;this.elements.add(i,n)}this.outerScope=t}getElement(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.elements.get(t)[0];if(r)return r;if(this.outerScope)return this.outerScope.getElement(e)}getElements(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.elements.get(t);return(this.concatOuterScope||r.length===0)&&this.outerScope?zn(r).concat(this.outerScope.getElements(e)):zn(r)}getAllElements(){let e=zn(this.elements.values());return this.outerScope&&(e=e.concat(this.outerScope.getAllElements())),e}},Dkt={getElement(){},getElements(){return vv},getAllElements(){return vv}},n_=class{static{s(this,"DisposableCache")}static{E(this,"DisposableCache")}constructor(){this.toDispose=[],this.isDisposed=!1}onDispose(e){this.toDispose.push(e)}dispose(){this.throwIfDisposed(),this.clear(),this.isDisposed=!0,this.toDispose.forEach(e=>e.dispose())}throwIfDisposed(){if(this.isDisposed)throw new Error("This cache has already been disposed")}},_H=class extends n_{static{s(this,"SimpleCache")}static{E(this,"SimpleCache")}constructor(){super(...arguments),this.cache=new Map}has(e){return this.throwIfDisposed(),this.cache.has(e)}set(e,t){this.throwIfDisposed(),this.cache.set(e,t)}get(e,t){if(this.throwIfDisposed(),this.cache.has(e))return this.cache.get(e);if(t){let r=t();return this.cache.set(e,r),r}else return}delete(e){return this.throwIfDisposed(),this.cache.delete(e)}clear(){this.throwIfDisposed(),this.cache.clear()}},i_=class extends n_{static{s(this,"ContextCache")}static{E(this,"ContextCache")}constructor(e){super(),this.cache=new Map,this.converter=e??(t=>t)}has(e,t){return this.throwIfDisposed(),this.cacheForContext(e).has(t)}set(e,t,r){this.throwIfDisposed(),this.cacheForContext(e).set(t,r)}get(e,t,r){this.throwIfDisposed();let n=this.cacheForContext(e);if(n.has(t))return n.get(t);if(r){let i=r();return n.set(t,i),i}else return}delete(e,t){return this.throwIfDisposed(),this.cacheForContext(e).delete(t)}clear(e){if(this.throwIfDisposed(),e){let t=this.converter(e);this.cache.delete(t)}else this.cache.clear()}cacheForContext(e){let t=this.converter(e),r=this.cache.get(t);return r||(r=new Map,this.cache.set(t,r)),r}},GLe=class extends i_{static{s(this,"DocumentCache")}static{E(this,"DocumentCache")}constructor(e,t){super(r=>r.toString()),t?(this.toDispose.push(e.workspace.DocumentBuilder.onDocumentPhase(t,r=>{this.clear(r.uri.toString())})),this.toDispose.push(e.workspace.DocumentBuilder.onUpdate((r,n)=>{for(let i of n)this.clear(i)}))):this.toDispose.push(e.workspace.DocumentBuilder.onUpdate((r,n)=>{let i=r.concat(n);for(let a of i)this.clear(a)}))}},LH=class extends _H{static{s(this,"WorkspaceCache")}static{E(this,"WorkspaceCache")}constructor(e,t){super(),t?(this.toDispose.push(e.workspace.DocumentBuilder.onBuildPhase(t,()=>{this.clear()})),this.toDispose.push(e.workspace.DocumentBuilder.onUpdate((r,n)=>{n.length>0&&this.clear()}))):this.toDispose.push(e.workspace.DocumentBuilder.onUpdate(()=>{this.clear()}))}},zLe=class{static{s(this,"DefaultScopeProvider")}static{E(this,"DefaultScopeProvider")}constructor(e){this.reflection=e.shared.AstReflection,this.nameProvider=e.references.NameProvider,this.descriptions=e.workspace.AstNodeDescriptionProvider,this.indexManager=e.shared.workspace.IndexManager,this.globalScopeCache=new LH(e.shared)}getScope(e){let t=[],r=this.reflection.getReferenceType(e),n=Cc(e.container).localSymbols;if(n){let a=e.container;do n.has(a)&&t.push(n.getStream(a).filter(o=>this.reflection.isSubtype(o.type,r))),a=a.$container;while(a)}let i=this.getGlobalScope(r,e);for(let a=t.length-1;a>=0;a--)i=this.createScope(t[a],i);return i}createScope(e,t,r){return new aV(zn(e),t,r)}createScopeForNodes(e,t,r){let n=zn(e).map(i=>{let a=this.nameProvider.getName(i);if(a)return this.descriptions.createDescription(i,a)}).nonNullable();return new aV(n,t,r)}getGlobalScope(e,t){return this.globalScopeCache.get(e,()=>new FLe(this.indexManager.allElements(e)))}};s(DH,"isAstNodeWithComment");E(DH,"isAstNodeWithComment");s(sV,"isIntermediateReference");E(sV,"isIntermediateReference");VLe=class{static{s(this,"DefaultJsonSerializer")}static{E(this,"DefaultJsonSerializer")}constructor(e){this.ignoreProperties=new Set(["$container","$containerProperty","$containerIndex","$document","$cstNode"]),this.langiumDocuments=e.shared.workspace.LangiumDocuments,this.astNodeLocator=e.workspace.AstNodeLocator,this.nameProvider=e.references.NameProvider,this.commentProvider=e.documentation.CommentProvider}serialize(e,t){let r=t??{},n=t?.replacer,i=E((o,l)=>this.replacer(o,l,r),"defaultReplacer"),a=n?(o,l)=>n(o,l,i):i;try{return this.currentDocument=Cc(e),JSON.stringify(e,a,t?.space)}finally{this.currentDocument=void 0}}deserialize(e,t){let r=t??{},n=JSON.parse(e);return this.linkNode(n,n,r),n}replacer(e,t,{refText:r,sourceText:n,textRegions:i,comments:a,uriConverter:o}){if(!this.ignoreProperties.has(e))if(Ys(t)){let l=t.ref,u=r?t.$refText:void 0;if(l){let h=Cc(l),d="";this.currentDocument&&this.currentDocument!==h&&(o?d=o(h.uri,l):d=h.uri.toString());let f=this.astNodeLocator.getAstNodePath(l);return{$ref:`${d}#${f}`,$refText:u}}else return{$error:t.error?.message??"Could not resolve reference",$refText:u}}else if($u(t)){let l=r?t.$refText:void 0,u=[];for(let h of t.items){let d=h.ref,f=Cc(h.ref),p="";this.currentDocument&&this.currentDocument!==f&&(o?p=o(f.uri,d):p=f.uri.toString());let m=this.astNodeLocator.getAstNodePath(d);u.push(`${p}#${m}`)}return{$refs:u,$refText:l}}else if(sa(t)){let l;if(i&&(l=this.addAstNodeRegionWithAssignmentsTo({...t}),(!e||t.$document)&&l?.$textRegion&&(l.$textRegion.documentURI=this.currentDocument?.uri.toString())),n&&!e&&(l??(l={...t}),l.$sourceText=t.$cstNode?.text),a){l??(l={...t});let u=this.commentProvider.getComment(t);u&&(l.$comment=u.replace(/\r/g,""))}return l??t}else return t}addAstNodeRegionWithAssignmentsTo(e){let t=E(r=>({offset:r.offset,end:r.end,length:r.length,range:r.range}),"createDocumentSegment");if(e.$cstNode){let r=e.$textRegion=t(e.$cstNode),n=r.assignments={};return Object.keys(e).filter(i=>!i.startsWith("$")).forEach(i=>{let a=eq(e.$cstNode,i).map(t);a.length!==0&&(n[i]=a)}),e}}linkNode(e,t,r,n,i,a){for(let[l,u]of Object.entries(e))if(Array.isArray(u))for(let h=0;h{await this.handleException(()=>e.call(t,r,n,i),"An error occurred during validation",n,r)}}async handleException(e,t,r,n){try{await e()}catch(i){if(v0(i))throw i;console.error(`${t}:`,i),i instanceof Error&&i.stack&&console.error(i.stack);let a=i instanceof Error?i.message:String(i);r("error",`${t}: ${a}`,{node:n})}}addEntry(e,t){if(e==="AstNode"){this.entries.add("AstNode",t);return}for(let r of this.reflection.getAllSubTypes(e))this.entries.add(r,t)}getChecks(e,t){let r=zn(this.entries.get(e)).concat(this.entries.get("AstNode"));return t&&(r=r.filter(n=>t.includes(n.category))),r.map(n=>n.check)}registerBeforeDocument(e,t=this){this.entriesBefore.push(this.wrapPreparationException(e,"An error occurred during set-up of the validation",t))}registerAfterDocument(e,t=this){this.entriesAfter.push(this.wrapPreparationException(e,"An error occurred during tear-down of the validation",t))}wrapPreparationException(e,t,r){return async(n,i,a,o)=>{await this.handleException(()=>e.call(r,n,i,a,o),t,i,n)}}get checksBefore(){return this.entriesBefore}get checksAfter(){return this.entriesAfter}getAllValidationCategories(e){return this.knownCategories}},HLe=Object.freeze({validateNode:!0,validateChildren:!0}),ULe=class{static{s(this,"DefaultDocumentValidator")}static{E(this,"DefaultDocumentValidator")}constructor(e){this.validationRegistry=e.validation.ValidationRegistry,this.metadata=e.LanguageMetaData,this.profiler=e.shared.profilers.LangiumProfiler,this.languageId=e.LanguageMetaData.languageId}async validateDocument(e,t={},r=ri.CancellationToken.None){let n=e.parseResult,i=[];if(await qa(r),(!t.categories||t.categories.includes("built-in"))&&(this.processLexingErrors(n,i,t),t.stopAfterLexingErrors&&i.some(a=>a.data?.code===Il.LexingError)||(this.processParsingErrors(n,i,t),t.stopAfterParsingErrors&&i.some(a=>a.data?.code===Il.ParsingError))||(this.processLinkingErrors(e,i,t),t.stopAfterLinkingErrors&&i.some(a=>a.data?.code===Il.LinkingError))))return i;try{i.push(...await this.validateAst(n.value,t,r))}catch(a){if(v0(a))throw a;console.error("An error occurred during validation:",a)}return await qa(r),i}processLexingErrors(e,t,r){let n=[...e.lexerErrors,...e.lexerReport?.diagnostics??[]];for(let i of n){let a=i.severity??"error",o={severity:zC(a),range:{start:{line:i.line-1,character:i.column-1},end:{line:i.line-1,character:i.column+i.length-1}},message:i.message,data:MH(a),source:this.getSource()};t.push(o)}}processParsingErrors(e,t,r){for(let n of e.parserErrors){let i;if(isNaN(n.token.startOffset)){if("previousToken"in n){let a=n.previousToken;if(isNaN(a.startOffset)){let o={line:0,character:0};i={start:o,end:o}}else{let o={line:a.endLine-1,character:a.endColumn};i={start:o,end:o}}}}else i=YC(n.token);if(i){let a={severity:zC("error"),range:i,message:n.message,data:qg(Il.ParsingError),source:this.getSource()};t.push(a)}}}processLinkingErrors(e,t,r){for(let n of e.references){let i=n.error;if(i){let a={node:i.info.container,range:n.$refNode?.range,property:i.info.property,index:i.info.index,data:{code:Il.LinkingError,containerType:i.info.container.$type,property:i.info.property,refText:i.info.reference.$refText}};t.push(this.toDiagnostic("error",i.message,a))}}}async validateAst(e,t,r=ri.CancellationToken.None){let n=[],i=E((a,o,l)=>{n.push(this.toDiagnostic(a,o,l))},"acceptor");return await this.validateAstBefore(e,t,i,r),await this.validateAstNodes(e,t,i,r),await this.validateAstAfter(e,t,i,r),n}async validateAstBefore(e,t,r,n=ri.CancellationToken.None){let i=this.validationRegistry.checksBefore;for(let a of i)await qa(n),await a(e,r,t.categories??[],n)}async validateAstNodes(e,t,r,n=ri.CancellationToken.None){if(this.profiler?.isActive("validating")){let i=this.profiler.createTask("validating",this.languageId);i.start();try{let a=Sc(e).iterator();for(let o of a){i.startSubTask(o.$type);let l=this.validateSingleNodeOptions(o,t);if(l.validateNode)try{let u=this.validationRegistry.getChecks(o.$type,t.categories);for(let h of u)await h(o,r,n)}finally{i.stopSubTask(o.$type)}l.validateChildren||a.prune()}}finally{i.stop()}}else{let i=Sc(e).iterator();for(let a of i){await qa(n);let o=this.validateSingleNodeOptions(a,t);if(o.validateNode){let l=this.validationRegistry.getChecks(a.$type,t.categories);for(let u of l)await u(a,r,n)}o.validateChildren||i.prune()}}}validateSingleNodeOptions(e,t){return HLe}async validateAstAfter(e,t,r,n=ri.CancellationToken.None){let i=this.validationRegistry.checksAfter;for(let a of i)await qa(n),await a(e,r,t.categories??[],n)}toDiagnostic(e,t,r){return{message:t,range:IH(r),severity:zC(e),code:r.code,codeDescription:r.codeDescription,tags:r.tags,relatedInformation:r.relatedInformation,data:r.data,source:this.getSource()}}getSource(){return this.metadata.languageId}};s(IH,"getDiagnosticRange");E(IH,"getDiagnosticRange");s(zC,"toDiagnosticSeverity");E(zC,"toDiagnosticSeverity");s(MH,"toDiagnosticData");E(MH,"toDiagnosticData");(function(e){e.LexingError="lexing-error",e.LexingWarning="lexing-warning",e.LexingInfo="lexing-info",e.LexingHint="lexing-hint",e.ParsingError="parsing-error",e.LinkingError="linking-error"})(Il||(Il={}));YLe=class{static{s(this,"DefaultAstNodeDescriptionProvider")}static{E(this,"DefaultAstNodeDescriptionProvider")}constructor(e){this.astNodeLocator=e.workspace.AstNodeLocator,this.nameProvider=e.references.NameProvider}createDescription(e,t,r){let n=r??Cc(e);t??(t=this.nameProvider.getName(e));let i=this.astNodeLocator.getAstNodePath(e);if(!t)throw new Error(`Node at path ${i} has no name.`);let a,o=E(()=>a??(a=wv(this.nameProvider.getNameNode(e)??e.$cstNode)),"nameSegmentGetter");return{node:e,name:t,get nameSegment(){return o()},selectionSegment:wv(e.$cstNode),type:e.$type,documentUri:n.uri,path:i}}},jLe=class{static{s(this,"DefaultReferenceDescriptionProvider")}static{E(this,"DefaultReferenceDescriptionProvider")}constructor(e){this.nodeLocator=e.workspace.AstNodeLocator}async createDescriptions(e,t=ri.CancellationToken.None){let r=[],n=e.parseResult.value;for(let i of Sc(n))await qa(t),Tv(i).forEach(a=>{a.reference.error||r.push(...this.createInfoDescriptions(a))});return r}createInfoDescriptions(e){let t=e.reference;if(t.error||!t.$refNode)return[];let r=[];Ys(t)&&t.$nodeDescription?r=[t.$nodeDescription]:$u(t)&&(r=t.items.map(l=>l.$nodeDescription).filter(l=>l!==void 0));let n=Cc(e.container).uri,i=this.nodeLocator.getAstNodePath(e.container),a=[],o=wv(t.$refNode);for(let l of r)a.push({sourceUri:n,sourcePath:i,targetUri:l.documentUri,targetPath:l.path,segment:o,local:js.equals(l.documentUri,n)});return a}},XLe=class{static{s(this,"DefaultAstNodeLocator")}static{E(this,"DefaultAstNodeLocator")}constructor(){this.segmentSeparator="/",this.indexSeparator="@"}getAstNodePath(e){if(e.$container){let t=this.getAstNodePath(e.$container),r=this.getPathSegment(e);return t+this.segmentSeparator+r}return""}getPathSegment({$containerProperty:e,$containerIndex:t}){if(!e)throw new Error("Missing '$containerProperty' in AST node.");return t!==void 0?e+this.indexSeparator+t:e}getAstNode(e,t){return t.split(this.segmentSeparator).reduce((n,i)=>{if(!n||i.length===0)return n;let a=i.indexOf(this.indexSeparator);if(a>0){let o=i.substring(0,a),l=parseInt(i.substring(a+1));return n[o]?.[l]}return n[i]},e)}},a_={};JR(a_,oW(_v(),1));KLe=class{static{s(this,"DefaultConfigurationProvider")}static{E(this,"DefaultConfigurationProvider")}constructor(e){this._ready=new td,this.onConfigurationSectionUpdateEmitter=new a_.Emitter,this.settings={},this.workspaceConfig=!1,this.serviceRegistry=e.ServiceRegistry}get ready(){return this._ready.promise}initialize(e){this.workspaceConfig=e.capabilities.workspace?.configuration??!1}async initialized(e){if(this.workspaceConfig){if(e.register){let t=this.serviceRegistry.all;e.register({section:t.map(r=>this.toSectionName(r.LanguageMetaData.languageId))})}if(e.fetchConfiguration){let t=this.serviceRegistry.all.map(n=>({section:this.toSectionName(n.LanguageMetaData.languageId)})),r=await e.fetchConfiguration(t);t.forEach((n,i)=>{this.updateSectionConfiguration(n.section,r[i])})}}this._ready.resolve()}updateConfiguration(e){typeof e.settings!="object"||e.settings===null||Object.entries(e.settings).forEach(([t,r])=>{this.updateSectionConfiguration(t,r),this.onConfigurationSectionUpdateEmitter.fire({section:t,configuration:r})})}updateSectionConfiguration(e,t){this.settings[e]=t}async getConfiguration(e,t){await this.ready;let r=this.toSectionName(e);if(this.settings[r])return this.settings[r][t]}toSectionName(e){return`${e}`}get onConfigurationSectionUpdate(){return this.onConfigurationSectionUpdateEmitter.event}},CA=oW(s0t(),1);(function(e){function t(r){return{dispose:E(async()=>await r(),"dispose")}}s(t,"create"),E(t,"create"),e.create=t})(Yg||(Yg={}));ZLe=class{static{s(this,"DefaultDocumentBuilder")}static{E(this,"DefaultDocumentBuilder")}constructor(e){this.updateBuildOptions={validation:{categories:["built-in","fast"]}},this.updateListeners=[],this.buildPhaseListeners=new rd,this.documentPhaseListeners=new rd,this.buildState=new Map,this.documentBuildWaiters=new Map,this.currentState=nn.Changed,this.langiumDocuments=e.workspace.LangiumDocuments,this.langiumDocumentFactory=e.workspace.LangiumDocumentFactory,this.textDocuments=e.workspace.TextDocuments,this.indexManager=e.workspace.IndexManager,this.fileSystemProvider=e.workspace.FileSystemProvider,this.workspaceManager=()=>e.workspace.WorkspaceManager,this.serviceRegistry=e.ServiceRegistry}async build(e,t={},r=ri.CancellationToken.None){for(let n of e){let i=n.uri.toString();if(n.state===nn.Validated){if(typeof t.validation=="boolean"&&t.validation)this.resetToState(n,nn.IndexedReferences);else if(typeof t.validation=="object"){let a=this.findMissingValidationCategories(n,t);a.length>0&&(this.buildState.set(i,{completed:!1,options:{validation:{categories:a}},result:this.buildState.get(i)?.result}),n.state=nn.IndexedReferences)}}else this.buildState.delete(i)}this.currentState=nn.Changed,await this.emitUpdate(e.map(n=>n.uri),[]),await this.buildDocuments(e,t,r)}async update(e,t,r=ri.CancellationToken.None){this.currentState=nn.Changed;let n=[];for(let l of t){let u=this.langiumDocuments.deleteDocuments(l);for(let h of u)n.push(h.uri),this.cleanUpDeleted(h)}let i=(await Promise.all(e.map(l=>this.findChangedUris(l)))).flat();for(let l of i){let u=this.langiumDocuments.getDocument(l);u===void 0&&(u=this.langiumDocumentFactory.fromModel({$type:"INVALID"},l),u.state=nn.Changed,this.langiumDocuments.addDocument(u)),this.resetToState(u,nn.Changed)}let a=zn(i).concat(n).map(l=>l.toString()).toSet();this.langiumDocuments.all.filter(l=>!a.has(l.uri.toString())&&this.shouldRelink(l,a)).forEach(l=>this.resetToState(l,nn.ComputedScopes)),await this.emitUpdate(i,n),await qa(r);let o=this.sortDocuments(this.langiumDocuments.all.filter(l=>l.state=1}findMissingValidationCategories(e,t){let r=this.buildState.get(e.uri.toString()),n=this.serviceRegistry.getServices(e.uri).validation.ValidationRegistry.getAllValidationCategories(e),i=r?.result?.validationChecks?new Set(r?.result?.validationChecks):r?.completed?n:new Set,a=t===void 0||t.validation===!0?n:typeof t.validation=="object"?t.validation.categories??n:[];return zn(a).filter(o=>!i.has(o)).toArray()}async findChangedUris(e){if(this.langiumDocuments.getDocument(e)??this.textDocuments?.get(e))return[e];try{let r=await this.fileSystemProvider.stat(e);if(r.isDirectory)return await this.workspaceManager().searchFolder(e);if(this.workspaceManager().shouldIncludeEntry(r))return[e]}catch{}return[]}async emitUpdate(e,t){await Promise.all(this.updateListeners.map(r=>r(e,t)))}sortDocuments(e){let t=0,r=e.length-1;for(;t=0&&!this.hasTextDocument(e[r]);)r--;tr.error!==void 0)?!0:this.indexManager.isAffected(e,t)}onUpdate(e){return this.updateListeners.push(e),Yg.create(()=>{let t=this.updateListeners.indexOf(e);t>=0&&this.updateListeners.splice(t,1)})}resetToState(e,t){switch(t){case nn.Changed:case nn.Parsed:this.indexManager.removeContent(e.uri);case nn.IndexedContent:e.localSymbols=void 0;case nn.ComputedScopes:this.serviceRegistry.getServices(e.uri).references.Linker.unlink(e);case nn.Linked:this.indexManager.removeReferences(e.uri);case nn.IndexedReferences:e.diagnostics=void 0,this.buildState.delete(e.uri.toString());case nn.Validated:}e.state>t&&(e.state=t)}cleanUpDeleted(e){this.buildState.delete(e.uri.toString()),this.indexManager.remove(e.uri),e.state=nn.Changed}async buildDocuments(e,t,r){this.prepareBuild(e,t),await this.runCancelable(e,nn.Parsed,r,a=>this.langiumDocumentFactory.update(a,r)),await this.runCancelable(e,nn.IndexedContent,r,a=>this.indexManager.updateContent(a,r)),await this.runCancelable(e,nn.ComputedScopes,r,async a=>{let o=this.serviceRegistry.getServices(a.uri).references.ScopeComputation;a.localSymbols=await o.collectLocalSymbols(a,r)});let n=e.filter(a=>this.shouldLink(a));await this.runCancelable(n,nn.Linked,r,a=>this.serviceRegistry.getServices(a.uri).references.Linker.link(a,r)),await this.runCancelable(n,nn.IndexedReferences,r,a=>this.indexManager.updateReferences(a,r));let i=e.filter(a=>this.shouldValidate(a)?!0:(this.markAsCompleted(a),!1));await this.runCancelable(i,nn.Validated,r,async a=>{await this.validate(a,r),this.markAsCompleted(a)})}markAsCompleted(e){let t=this.buildState.get(e.uri.toString());t&&(t.completed=!0)}prepareBuild(e,t){for(let r of e){let n=r.uri.toString(),i=this.buildState.get(n);(!i||i.completed)&&this.buildState.set(n,{completed:!1,options:t,result:i?.result})}}async runCancelable(e,t,r,n){for(let a of e)a.statea.state===t);await this.notifyBuildPhase(i,t,r),this.currentState=t}onBuildPhase(e,t){return this.buildPhaseListeners.add(e,t),Yg.create(()=>{this.buildPhaseListeners.delete(e,t)})}onDocumentPhase(e,t){return this.documentPhaseListeners.add(e,t),Yg.create(()=>{this.documentPhaseListeners.delete(e,t)})}waitUntil(e,t,r){let n;return t&&"path"in t?n=t:r=t,r??(r=ri.CancellationToken.None),n?this.awaitDocumentState(e,n,r):this.awaitBuilderState(e,r)}awaitDocumentState(e,t,r){let n=this.langiumDocuments.getDocument(t);if(n){if(n.state>=e)return Promise.resolve(t);if(r.isCancellationRequested)return Promise.reject(Bu);if(this.currentState>=e&&e>n.state)return Promise.reject(new CA.ResponseError(CA.LSPErrorCodes.RequestFailed,`Document state of ${t.toString()} is ${nn[n.state]}, requiring ${nn[e]}, but workspace state is already ${nn[this.currentState]}. Returning undefined.`))}else return Promise.reject(new CA.ResponseError(CA.LSPErrorCodes.ServerCancelled,`No document found for URI: ${t.toString()}`));return new Promise((i,a)=>{let o=this.onDocumentPhase(e,u=>{js.equals(u.uri,t)&&(o.dispose(),l.dispose(),i(u.uri))}),l=r.onCancellationRequested(()=>{o.dispose(),l.dispose(),a(Bu)})})}awaitBuilderState(e,t){return this.currentState>=e?Promise.resolve():t.isCancellationRequested?Promise.reject(Bu):new Promise((r,n)=>{let i=this.onBuildPhase(e,()=>{i.dispose(),a.dispose(),r()}),a=t.onCancellationRequested(()=>{i.dispose(),a.dispose(),n(Bu)})})}async notifyDocumentPhase(e,t,r){let i=this.documentPhaseListeners.get(t).slice();for(let a of i)try{await qa(r),await a(e,r)}catch(o){if(!v0(o))throw o}}async notifyBuildPhase(e,t,r){if(e.length===0)return;let i=this.buildPhaseListeners.get(t).slice();for(let a of i)await qa(r),await a(e,r)}shouldLink(e){return this.getBuildOptions(e).eagerLinking??!0}shouldValidate(e){return!!this.getBuildOptions(e).validation}async validate(e,t){let r=this.serviceRegistry.getServices(e.uri).validation.DocumentValidator,n=this.getBuildOptions(e),i=typeof n.validation=="object"?{...n.validation}:{};i.categories=this.findMissingValidationCategories(e,n);let a=await r.validateDocument(e,i,t);e.diagnostics?e.diagnostics.push(...a):e.diagnostics=a;let o=this.buildState.get(e.uri.toString());o&&(o.result??(o.result={}),o.result.validationChecks?o.result.validationChecks=zn(o.result.validationChecks).concat(i.categories).distinct().toArray():o.result.validationChecks=[...i.categories])}getBuildOptions(e){return this.buildState.get(e.uri.toString())?.options??{}}},QLe=class{static{s(this,"DefaultIndexManager")}static{E(this,"DefaultIndexManager")}constructor(e){this.symbolIndex=new Map,this.symbolByTypeIndex=new i_,this.referenceIndex=new Map,this.documents=e.workspace.LangiumDocuments,this.serviceRegistry=e.ServiceRegistry,this.astReflection=e.AstReflection}findAllReferences(e,t){let r=Cc(e).uri,n=[];return this.referenceIndex.forEach(i=>{i.forEach(a=>{js.equals(a.targetUri,r)&&a.targetPath===t&&n.push(a)})}),zn(n)}allElements(e,t){let r=zn(this.symbolIndex.keys());return t&&(r=r.filter(n=>!t||t.has(n))),r.map(n=>this.getFileDescriptions(n,e)).flat()}getFileDescriptions(e,t){return t?this.symbolByTypeIndex.get(e,t,()=>(this.symbolIndex.get(e)??[]).filter(i=>this.astReflection.isSubtype(i.type,t))):this.symbolIndex.get(e)??[]}remove(e){this.removeContent(e),this.removeReferences(e)}removeContent(e){let t=e.toString();this.symbolIndex.delete(t),this.symbolByTypeIndex.clear(t)}removeReferences(e){let t=e.toString();this.referenceIndex.delete(t)}async updateContent(e,t=ri.CancellationToken.None){let n=await this.serviceRegistry.getServices(e.uri).references.ScopeComputation.collectExportedSymbols(e,t),i=e.uri.toString();this.symbolIndex.set(i,n),this.symbolByTypeIndex.clear(i)}async updateReferences(e,t=ri.CancellationToken.None){let n=await this.serviceRegistry.getServices(e.uri).workspace.ReferenceDescriptionProvider.createDescriptions(e,t);this.referenceIndex.set(e.uri.toString(),n)}isAffected(e,t){let r=this.referenceIndex.get(e.uri.toString());return r?r.some(n=>!n.local&&t.has(n.targetUri.toString())):!1}},JLe=class{static{s(this,"DefaultWorkspaceManager")}static{E(this,"DefaultWorkspaceManager")}constructor(e){this.initialBuildOptions={},this._ready=new td,this.serviceRegistry=e.ServiceRegistry,this.langiumDocuments=e.workspace.LangiumDocuments,this.documentBuilder=e.workspace.DocumentBuilder,this.fileSystemProvider=e.workspace.FileSystemProvider,this.mutex=e.workspace.WorkspaceLock}get ready(){return this._ready.promise}get workspaceFolders(){return this.folders}initialize(e){this.folders=e.workspaceFolders??void 0}initialized(e){return this.mutex.write(t=>this.initializeWorkspace(this.folders??[],t))}async initializeWorkspace(e,t=ri.CancellationToken.None){let r=await this.performStartup(e);await qa(t),await this.documentBuilder.build(r,this.initialBuildOptions,t)}async performStartup(e){let t=[],r=E(a=>{t.push(a),this.langiumDocuments.hasDocument(a.uri)||this.langiumDocuments.addDocument(a)},"collector");await this.loadAdditionalDocuments(e,r);let n=[];await Promise.all(e.map(a=>this.getRootFolder(a)).map(async a=>this.traverseFolder(a,n)));let i=zn(n).distinct(a=>a.toString()).filter(a=>!this.langiumDocuments.hasDocument(a));return await this.loadWorkspaceDocuments(i,r),this._ready.resolve(),t}async loadWorkspaceDocuments(e,t){await Promise.all(e.map(async r=>{let n=await this.langiumDocuments.getOrCreateDocument(r);t(n)}))}loadAdditionalDocuments(e,t){return Promise.resolve()}getRootFolder(e){return Jo.parse(e.uri)}async traverseFolder(e,t){try{let r=await this.fileSystemProvider.readDirectory(e);await Promise.all(r.map(async n=>{this.shouldIncludeEntry(n)&&(n.isDirectory?await this.traverseFolder(n.uri,t):n.isFile&&t.push(n.uri))}))}catch(r){console.error("Failure to read directory content of "+e.toString(!0),r)}}async searchFolder(e){let t=[];return await this.traverseFolder(e,t),t}shouldIncludeEntry(e){let t=js.basename(e.uri);return t.startsWith(".")?!1:e.isDirectory?t!=="node_modules"&&t!=="out":e.isFile?this.serviceRegistry.hasServices(e.uri):!1}},eDe=class{static{s(this,"DefaultLexerErrorMessageProvider")}static{E(this,"DefaultLexerErrorMessageProvider")}buildUnexpectedCharactersMessage(e,t,r,n,i){return Fz.buildUnexpectedCharactersMessage(e,t,r,n,i)}buildUnableToPopLexerModeMessage(e){return Fz.buildUnableToPopLexerModeMessage(e)}},NH={mode:"full"},PH=class{static{s(this,"DefaultLexer")}static{E(this,"DefaultLexer")}constructor(e){this.errorMessageProvider=e.parser.LexerErrorMessageProvider,this.tokenBuilder=e.parser.TokenBuilder;let t=this.tokenBuilder.buildTokens(e.Grammar,{caseInsensitive:e.LanguageMetaData.caseInsensitive});this.tokenTypes=this.toTokenTypeDictionary(t);let r=KR(t)?Object.values(t):t,n=e.LanguageMetaData.mode==="production";this.chevrotainLexer=new Xs(r,{positionTracking:"full",skipValidations:n,errorMessageProvider:this.errorMessageProvider})}get definition(){return this.tokenTypes}tokenize(e,t=NH){let r=this.chevrotainLexer.tokenize(e);return{tokens:r.tokens,errors:r.errors,hidden:r.groups.hidden??[],report:this.tokenBuilder.flushLexingReport?.(e)}}toTokenTypeDictionary(e){if(KR(e))return e;let t=o_(e)?Object.values(e.modes).flat():e,r={};return t.forEach(n=>r[n.name]=n),r}};s(s_,"isTokenTypeArray");E(s_,"isTokenTypeArray");s(o_,"isIMultiModeLexerDefinition");E(o_,"isIMultiModeLexerDefinition");s(KR,"isTokenTypeDictionary");E(KR,"isTokenTypeDictionary");cS();s(OH,"parseJSDoc");E(OH,"parseJSDoc");s(BH,"isJSDoc");E(BH,"isJSDoc");s($H,"getLines");E($H,"getLines");QSe=/\s*(@([\p{L}][\p{L}\p{N}]*)?)/uy,Ikt=/\{(@[\p{L}][\p{L}\p{N}]*)(\s*)([^\r\n}]+)?\}/gu;s(tDe,"tokenize");E(tDe,"tokenize");s(rDe,"buildInlineTokens");E(rDe,"buildInlineTokens");Mkt=/\S/,Nkt=/\s*$/;s(ZR,"skipWhitespace");E(ZR,"skipWhitespace");s(nDe,"lastCharacter");E(nDe,"lastCharacter");s(iDe,"parseJSDocComment");E(iDe,"parseJSDocComment");s(aDe,"parseJSDocElement");E(aDe,"parseJSDocElement");s(sDe,"appendEmptyLine");E(sDe,"appendEmptyLine");s(FH,"parseJSDocText");E(FH,"parseJSDocText");s(oDe,"parseJSDocInline");E(oDe,"parseJSDocInline");s(GH,"parseJSDocTag");E(GH,"parseJSDocTag");s(zH,"parseJSDocLine");E(zH,"parseJSDocLine");s(l_,"normalizeOptions");E(l_,"normalizeOptions");s(tR,"normalizeOption");E(tR,"normalizeOption");JSe=class{static{s(this,"JSDocCommentImpl")}static{E(this,"JSDocCommentImpl")}constructor(e,t){this.elements=e,this.range=t}getTag(e){return this.getAllTags().find(t=>t.name===e)}getTags(e){return this.getAllTags().filter(t=>t.name===e)}getAllTags(){return this.elements.filter(e=>"name"in e)}toString(){let e="";for(let t of this.elements)if(e.length===0)e=t.toString();else{let r=t.toString();e+=lV(e)+r}return e.trim()}toMarkdown(e){let t="";for(let r of this.elements)if(t.length===0)t=r.toMarkdown(e);else{let n=r.toMarkdown(e);t+=lV(t)+n}return t.trim()}},mG=class{static{s(this,"JSDocTagImpl")}static{E(this,"JSDocTagImpl")}constructor(e,t,r,n){this.name=e,this.content=t,this.inline=r,this.range=n}toString(){let e=`@${this.name}`,t=this.content.toString();return this.content.inlines.length===1?e=`${e} ${t}`:this.content.inlines.length>1&&(e=`${e} +${t}`),this.inline?`{${e}}`:e}toMarkdown(e){return e?.renderTag?.(this)??this.toMarkdownDefault(e)}toMarkdownDefault(e){let t=this.content.toMarkdown(e);if(this.inline){let i=lDe(this.name,t,e??{});if(typeof i=="string")return i}let r="";e?.tag==="italic"||e?.tag===void 0?r="*":e?.tag==="bold"?r="**":e?.tag==="bold-italic"&&(r="***");let n=`${r}@${this.name}${r}`;return this.content.inlines.length===1?n=`${n} \u2014 ${t}`:this.content.inlines.length>1&&(n=`${n} +${t}`),this.inline?`{${n}}`:n}};s(lDe,"renderInlineTag");E(lDe,"renderInlineTag");s(cDe,"renderLinkDefault");E(cDe,"renderLinkDefault");oV=class{static{s(this,"JSDocTextImpl")}static{E(this,"JSDocTextImpl")}constructor(e,t){this.inlines=e,this.range=t}toString(){let e="";for(let t=0;tr.range.start.line&&(e+=` +`)}return e}toMarkdown(e){let t="";for(let r=0;rn.range.start.line&&(t+=` +`)}return t}},uDe=class{static{s(this,"JSDocLineImpl")}static{E(this,"JSDocLineImpl")}constructor(e,t){this.text=e,this.range=t}toString(){return this.text}toMarkdown(){return this.text}};s(lV,"fillNewlines");E(lV,"fillNewlines");hDe=class{static{s(this,"JSDocDocumentationProvider")}static{E(this,"JSDocDocumentationProvider")}constructor(e){this.indexManager=e.shared.workspace.IndexManager,this.commentProvider=e.documentation.CommentProvider}getDocumentation(e){let t=this.commentProvider.getComment(e);if(t&&BH(t))return OH(t).toMarkdown({renderLink:E((n,i)=>this.documentationLinkRenderer(e,n,i),"renderLink"),renderTag:E(n=>this.documentationTagRenderer(e,n),"renderTag")})}documentationLinkRenderer(e,t,r){let n=this.findNameInLocalSymbols(e,t)??this.findNameInGlobalScope(e,t);if(n&&n.nameSegment){let i=n.nameSegment.range.start.line+1,a=n.nameSegment.range.start.character+1,o=n.documentUri.with({fragment:`L${i},${a}`});return`[${r}](${o.toString()})`}else return}documentationTagRenderer(e,t){}findNameInLocalSymbols(e,t){let n=Cc(e).localSymbols;if(!n)return;let i=e;do{let o=n.getStream(i).find(l=>l.name===t);if(o)return o;i=i.$container}while(i)}findNameInGlobalScope(e,t){return this.indexManager.allElements().find(n=>n.name===t)}},dDe=class{static{s(this,"DefaultCommentProvider")}static{E(this,"DefaultCommentProvider")}constructor(e){this.grammarConfig=()=>e.parser.GrammarConfig}getComment(e){return DH(e)?e.$comment:FW(e.$cstNode,this.grammarConfig().multilineCommentRules)?.text}},fDe=class{static{s(this,"DefaultAsyncParser")}static{E(this,"DefaultAsyncParser")}constructor(e){this.syncParser=e.parser.LangiumParser}parse(e,t){return Promise.resolve(this.syncParser.parse(e))}},Pkt=class{static{s(this,"AbstractThreadedAsyncParser")}static{E(this,"AbstractThreadedAsyncParser")}constructor(e){this.threadCount=8,this.terminationDelay=200,this.workerPool=[],this.queue=[],this.hydrator=e.serializer.Hydrator}initializeWorkers(){for(;this.workerPool.length{if(this.queue.length>0){let t=this.queue.shift();t&&(e.lock(),t.resolve(e))}}),this.workerPool.push(e)}}async parse(e,t){let r=await this.acquireParserWorker(t),n=new td,i,a=t.onCancellationRequested(()=>{i=setTimeout(()=>{this.terminateWorker(r)},this.terminationDelay)});return r.parse(e).then(o=>{let l=this.hydrator.hydrate(o);n.resolve(l)}).catch(o=>{n.reject(o)}).finally(()=>{a.dispose(),clearTimeout(i)}),n.promise}terminateWorker(e){e.terminate();let t=this.workerPool.indexOf(e);t>=0&&this.workerPool.splice(t,1)}async acquireParserWorker(e){this.initializeWorkers();for(let r of this.workerPool)if(r.ready)return r.lock(),r;let t=new td;return e.onCancellationRequested(()=>{let r=this.queue.indexOf(t);r>=0&&this.queue.splice(r,1),t.reject(Bu)}),this.queue.push(t),t.promise}},Okt=class{static{s(this,"ParserWorker")}static{E(this,"ParserWorker")}get ready(){return this._ready}get onReady(){return this.onReadyEmitter.event}constructor(e,t,r,n){this.onReadyEmitter=new a_.Emitter,this.deferred=new td,this._ready=!0,this._parsing=!1,this.sendMessage=e,this._terminate=n,t(i=>{let a=i;this.deferred.resolve(a),this.unlock()}),r(i=>{this.deferred.reject(i),this.unlock()})}terminate(){this.deferred.reject(Bu),this._terminate()}lock(){this._ready=!1}unlock(){this._parsing=!1,this._ready=!0,this.onReadyEmitter.fire()}parse(e){if(this._parsing)throw new Error("Parser worker is busy");return this._parsing=!0,this.deferred=new td,this.sendMessage(e),this.deferred.promise}},pDe=class{static{s(this,"DefaultWorkspaceLock")}static{E(this,"DefaultWorkspaceLock")}constructor(){this.previousTokenSource=new ri.CancellationTokenSource,this.writeQueue=[],this.readQueue=[],this.done=!0}write(e){this.cancelWrite();let t=r_();return this.previousTokenSource=t,this.enqueue(this.writeQueue,e,t.token)}read(e){return this.enqueue(this.readQueue,e)}enqueue(e,t,r=ri.CancellationToken.None){let n=new td,i={action:t,deferred:n,cancellationToken:r};return e.push(i),this.performNextOperation(),n.promise}async performNextOperation(){if(!this.done)return;let e=[];if(this.writeQueue.length>0)e.push(this.writeQueue.shift());else if(this.readQueue.length>0)e.push(...this.readQueue.splice(0,this.readQueue.length));else return;this.done=!1,await Promise.all(e.map(async({action:t,deferred:r,cancellationToken:n})=>{try{let i=await Promise.resolve().then(()=>t(n));r.resolve(i)}catch(i){v0(i)?r.resolve(void 0):r.reject(i)}})),this.done=!0,this.performNextOperation()}cancelWrite(){this.previousTokenSource.cancel()}},mDe=class{static{s(this,"DefaultHydrator")}static{E(this,"DefaultHydrator")}constructor(e){this.grammarElementIdMap=new jR,this.tokenTypeIdMap=new jR,this.grammar=e.Grammar,this.lexer=e.parser.Lexer,this.linker=e.references.Linker}dehydrate(e){return{lexerErrors:e.lexerErrors,lexerReport:e.lexerReport?this.dehydrateLexerReport(e.lexerReport):void 0,parserErrors:e.parserErrors.map(t=>({...t,message:t.message})),value:this.dehydrateAstNode(e.value,this.createDehyrationContext(e.value))}}dehydrateLexerReport(e){return e}createDehyrationContext(e){let t=new Map,r=new Map;for(let n of Sc(e))t.set(n,{});if(e.$cstNode)for(let n of Sv(e.$cstNode))r.set(n,{});return{astNodes:t,cstNodes:r}}dehydrateAstNode(e,t){let r=t.astNodes.get(e);r.$type=e.$type,r.$containerIndex=e.$containerIndex,r.$containerProperty=e.$containerProperty,e.$cstNode!==void 0&&(r.$cstNode=this.dehydrateCstNode(e.$cstNode,t));for(let[n,i]of Object.entries(e))if(!n.startsWith("$"))if(Array.isArray(i)){let a=[];r[n]=a;for(let o of i)sa(o)?a.push(this.dehydrateAstNode(o,t)):Ys(o)?a.push(this.dehydrateReference(o,t)):a.push(o)}else sa(i)?r[n]=this.dehydrateAstNode(i,t):Ys(i)?r[n]=this.dehydrateReference(i,t):i!==void 0&&(r[n]=i);return r}dehydrateReference(e,t){let r={};return r.$refText=e.$refText,e.$refNode&&(r.$refNode=t.cstNodes.get(e.$refNode)),r}dehydrateCstNode(e,t){let r=t.cstNodes.get(e);return r6(e)?r.fullText=e.fullText:r.grammarSource=this.getGrammarElementId(e.grammarSource),r.hidden=e.hidden,r.astNode=t.astNodes.get(e.astNode),Yh(e)?r.content=e.content.map(n=>this.dehydrateCstNode(n,t)):Jg(e)&&(r.tokenType=e.tokenType.name,r.offset=e.offset,r.length=e.length,r.startLine=e.range.start.line,r.startColumn=e.range.start.character,r.endLine=e.range.end.line,r.endColumn=e.range.end.character),r}hydrate(e){let t=e.value,r=this.createHydrationContext(t);return"$cstNode"in t&&this.hydrateCstNode(t.$cstNode,r),{lexerErrors:e.lexerErrors,lexerReport:e.lexerReport,parserErrors:e.parserErrors,value:this.hydrateAstNode(t,r)}}createHydrationContext(e){let t=new Map,r=new Map;for(let i of Sc(e))t.set(i,{});let n;if(e.$cstNode)for(let i of Sv(e.$cstNode)){let a;"fullText"in i?(a=new mH(i.fullText),n=a):"content"in i?a=new Z6:"tokenType"in i&&(a=this.hydrateCstLeafNode(i)),a&&(r.set(i,a),a.root=n)}return{astNodes:t,cstNodes:r}}hydrateAstNode(e,t){let r=t.astNodes.get(e);r.$type=e.$type,r.$containerIndex=e.$containerIndex,r.$containerProperty=e.$containerProperty,e.$cstNode&&(r.$cstNode=t.cstNodes.get(e.$cstNode));for(let[n,i]of Object.entries(e))if(!n.startsWith("$"))if(Array.isArray(i)){let a=[];r[n]=a;for(let o of i)sa(o)?a.push(this.setParent(this.hydrateAstNode(o,t),r)):Ys(o)?a.push(this.hydrateReference(o,r,n,t)):a.push(o)}else sa(i)?r[n]=this.setParent(this.hydrateAstNode(i,t),r):Ys(i)?r[n]=this.hydrateReference(i,r,n,t):i!==void 0&&(r[n]=i);return r}setParent(e,t){return e.$container=t,e}hydrateReference(e,t,r,n){return this.linker.buildReference(t,r,n.cstNodes.get(e.$refNode),e.$refText)}hydrateCstNode(e,t,r=0){let n=t.cstNodes.get(e);if(typeof e.grammarSource=="number"&&(n.grammarSource=this.getGrammarElement(e.grammarSource)),n.astNode=t.astNodes.get(e.astNode),Yh(n))for(let i of e.content){let a=this.hydrateCstNode(i,t,r++);n.content.push(a)}return n}hydrateCstLeafNode(e){let t=this.getTokenType(e.tokenType),r=e.offset,n=e.length,i=e.startLine,a=e.startColumn,o=e.endLine,l=e.endColumn,u=e.hidden;return new WR(r,n,{start:{line:i,character:a},end:{line:o,character:l}},t,u)}getTokenType(e){return this.lexer.definition[e]}getGrammarElementId(e){if(e)return this.grammarElementIdMap.size===0&&this.createGrammarElementIdMap(),this.grammarElementIdMap.get(e)}getGrammarElement(e){return this.grammarElementIdMap.size===0&&this.createGrammarElementIdMap(),this.grammarElementIdMap.getKey(e)}createGrammarElementIdMap(){let e=0;for(let t of Sc(this.grammar))n6(t)&&this.grammarElementIdMap.set(t,e++)}};s(yn,"createDefaultCoreModule");E(yn,"createDefaultCoreModule");s(vn,"createDefaultSharedCoreModule");E(vn,"createDefaultSharedCoreModule");(function(e){e.merge=(t,r)=>Rv(Rv({},t),r)})(cV||(cV={}));s($r,"inject");E($r,"inject");gDe=Symbol("isProxy");s(VH,"eagerLoad");E(VH,"eagerLoad");s(WH,"_inject");E(WH,"_inject");ewe=Symbol();s(uV,"_resolve");E(uV,"_resolve");s(Rv,"_merge");E(Rv,"_merge");hV={indentTokenName:"INDENT",dedentTokenName:"DEDENT",whitespaceTokenName:"WS",ignoreIndentationDelimiters:[]};(function(e){e.REGULAR="indentation-sensitive",e.IGNORE_INDENTATION="ignore-indentation"})(Hg||(Hg={}));yDe=class extends e_{static{s(this,"IndentationAwareTokenBuilder")}static{E(this,"IndentationAwareTokenBuilder")}constructor(e=hV){super(),this.indentationStack=[0],this.whitespaceRegExp=/[ \t]+/y,this.options={...hV,...e},this.indentTokenType=gv({name:this.options.indentTokenName,pattern:this.indentMatcher.bind(this),line_breaks:!1}),this.dedentTokenType=gv({name:this.options.dedentTokenName,pattern:this.dedentMatcher.bind(this),line_breaks:!1})}buildTokens(e,t){let r=super.buildTokens(e,t);if(!s_(r))throw new Error("Invalid tokens built by default builder");let{indentTokenName:n,dedentTokenName:i,whitespaceTokenName:a,ignoreIndentationDelimiters:o}=this.options,l,u,h,d=[];for(let f of r){for(let[p,m]of o)f.name===p?f.PUSH_MODE=Hg.IGNORE_INDENTATION:f.name===m&&(f.POP_MODE=!0);f.name===i?l=f:f.name===n?u=f:f.name===a?h=f:d.push(f)}if(!l||!u||!h)throw new Error("Some indentation/whitespace tokens not found!");return o.length>0?{modes:{[Hg.REGULAR]:[l,u,...d,h],[Hg.IGNORE_INDENTATION]:[...d,h]},defaultMode:Hg.REGULAR}:[l,u,h,...d]}flushLexingReport(e){return{...super.flushLexingReport(e),remainingDedents:this.flushRemainingDedents(e)}}isStartOfLine(e,t){return t===0||`\r +`.includes(e[t-1])}matchWhitespace(e,t,r,n){this.whitespaceRegExp.lastIndex=t;let i=this.whitespaceRegExp.exec(e);return{currIndentLevel:i?.[0].length??0,prevIndentLevel:this.indentationStack.at(-1),match:i}}createIndentationTokenInstance(e,t,r,n){let i=this.getLineNumber(t,n);return ES(e,r,n,n+r.length,i,i,1,r.length)}getLineNumber(e,t){return e.substring(0,t).split(/\r\n|\r|\n/).length}indentMatcher(e,t,r,n){if(!this.isStartOfLine(e,t))return null;let{currIndentLevel:i,prevIndentLevel:a,match:o}=this.matchWhitespace(e,t,r,n);return i<=a?null:(this.indentationStack.push(i),o)}dedentMatcher(e,t,r,n){if(!this.isStartOfLine(e,t))return null;let{currIndentLevel:i,prevIndentLevel:a,match:o}=this.matchWhitespace(e,t,r,n);if(i>=a)return null;let l=this.indentationStack.lastIndexOf(i);if(l===-1)return this.diagnostics.push({severity:"error",message:`Invalid dedent level ${i} at offset: ${t}. Current indentation stack: ${this.indentationStack}`,offset:t,length:o?.[0]?.length??0,line:this.getLineNumber(e,t),column:1}),null;let u=this.indentationStack.length-l-1,h=e.substring(0,t).match(/[\r\n]+$/)?.[0].length??1;for(let d=0;d1;)t.push(this.createIndentationTokenInstance(this.dedentTokenType,e,"",e.length)),this.indentationStack.pop();return this.indentationStack=[0],t}},Bkt=class extends PH{static{s(this,"IndentationAwareLexer")}static{E(this,"IndentationAwareLexer")}constructor(e){if(super(e),e.parser.TokenBuilder instanceof yDe)this.indentationTokenBuilder=e.parser.TokenBuilder;else throw new Error("IndentationAwareLexer requires an accompanying IndentationAwareTokenBuilder")}tokenize(e,t=NH){let r=super.tokenize(e),n=r.report;t?.mode==="full"&&r.tokens.push(...n.remainingDedents),n.remainingDedents=[];let{indentTokenType:i,dedentTokenType:a}=this.indentationTokenBuilder,o=i.tokenTypeIdx,l=a.tokenTypeIdx,u=[],h=r.tokens.length-1;for(let d=0;d=0&&u.push(r.tokens[h]),r.tokens=u,r}},qH={};xp(qH,{AstUtils:s(()=>fW,"AstUtils"),BiMap:s(()=>jR,"BiMap"),Cancellation:s(()=>ri,"Cancellation"),ContextCache:s(()=>i_,"ContextCache"),CstUtils:s(()=>uW,"CstUtils"),DONE_RESULT:s(()=>Us,"DONE_RESULT"),Deferred:s(()=>td,"Deferred"),Disposable:s(()=>Yg,"Disposable"),DisposableCache:s(()=>n_,"DisposableCache"),DocumentCache:s(()=>GLe,"DocumentCache"),EMPTY_STREAM:s(()=>vv,"EMPTY_STREAM"),ErrorWithLocation:s(()=>u6,"ErrorWithLocation"),GrammarUtils:s(()=>WW,"GrammarUtils"),MultiMap:s(()=>rd,"MultiMap"),OperationCancelled:s(()=>Bu,"OperationCancelled"),Reduction:s(()=>UC,"Reduction"),RegExpUtils:s(()=>HW,"RegExpUtils"),SimpleCache:s(()=>_H,"SimpleCache"),StreamImpl:s(()=>Ou,"StreamImpl"),TreeStreamImpl:s(()=>xv,"TreeStreamImpl"),URI:s(()=>Jo,"URI"),UriTrie:s(()=>AH,"UriTrie"),UriUtils:s(()=>js,"UriUtils"),WorkspaceCache:s(()=>LH,"WorkspaceCache"),assertCondition:s(()=>qW,"assertCondition"),assertUnreachable:s(()=>bp,"assertUnreachable"),delayNextTick:s(()=>t_,"delayNextTick"),interruptAndCheck:s(()=>qa,"interruptAndCheck"),isOperationCancelled:s(()=>v0,"isOperationCancelled"),loadGrammarFromJson:s(()=>Ha,"loadGrammarFromJson"),setInterruptionPeriod:s(()=>wH,"setInterruptionPeriod"),startCancelableOperation:s(()=>r_,"startCancelableOperation"),stream:s(()=>zn,"stream")});JR(qH,a_);vDe=class{static{s(this,"EmptyFileSystemProvider")}static{E(this,"EmptyFileSystemProvider")}stat(e){throw new Error("No file system is available.")}statSync(e){throw new Error("No file system is available.")}async exists(){return!1}existsSync(){return!1}readBinary(){throw new Error("No file system is available.")}readBinarySync(){throw new Error("No file system is available.")}readFile(){throw new Error("No file system is available.")}readFileSync(){throw new Error("No file system is available.")}async readDirectory(){return[]}readDirectorySync(){return[]}},Cn={fileSystemProvider:E(()=>new vDe,"fileSystemProvider")},$kt={Grammar:E(()=>{},"Grammar"),LanguageMetaData:E(()=>({caseInsensitive:!1,fileExtensions:[".langium"],languageId:"langium"}),"LanguageMetaData")},Fkt={AstReflection:E(()=>new NW,"AstReflection")};s(xDe,"createMinimalGrammarServices");E(xDe,"createMinimalGrammarServices");s(Ha,"loadGrammarFromJson");E(Ha,"loadGrammarFromJson");JR(_we,qH);Gkt=class{static{s(this,"DefaultLangiumProfiler")}static{E(this,"DefaultLangiumProfiler")}constructor(e){this.activeCategories=new Set,this.allCategories=new Set(["validating","parsing","linking"]),this.activeCategories=e??new Set(this.allCategories),this.records=new rd}isActive(e){return this.activeCategories.has(e)}start(...e){e?e.forEach(t=>this.activeCategories.add(t)):this.activeCategories=new Set(this.allCategories)}stop(...e){e?e.forEach(t=>this.activeCategories.delete(t)):this.activeCategories.clear()}createTask(e,t){if(!this.isActive(e))throw new Error(`Category "${e}" is not active.`);return console.log(`Creating profiling task for '${e}.${t}'.`),new bDe(r=>this.records.add(e,this.dumpRecord(e,r)),t)}dumpRecord(e,t){console.info(`Task ${e}.${t.identifier} executed in ${t.duration.toFixed(2)}ms and ended at ${t.date.toISOString()}`);let r=[];for(let a of t.entries.keys()){let o=t.entries.get(a),l=o.reduce((u,h)=>u+h);r.push({name:`${t.identifier}.${a}`,count:o.length,duration:l})}let n=t.duration-r.map(a=>a.duration).reduce((a,o)=>a+o,0);r.push({name:t.identifier,count:1,duration:n}),r.sort((a,o)=>o.duration-a.duration);function i(a){return Math.round(100*a)/100}return s(i,"Round"),E(i,"Round"),console.table(r.map(a=>({Element:a.name,Count:a.count,"Self %":i(100*a.duration/t.duration),"Time (ms)":i(a.duration)}))),t}getRecords(...e){return e.length===0?this.records.values():this.records.entries().filter(t=>e.some(r=>r===t[0])).flatMap(t=>t[1])}},bDe=class{static{s(this,"ProfilingTask")}static{E(this,"ProfilingTask")}constructor(e,t){this.stack=[],this.entries=new rd,this.addRecord=e,this.identifier=t}start(){if(this.startTime!==void 0)throw new Error(`Task "${this.identifier}" is already started.`);this.startTime=performance.now()}stop(){if(this.startTime===void 0)throw new Error(`Task "${this.identifier}" was not started.`);if(this.stack.length!==0)throw new Error(`Task "${this.identifier}" cannot be stopped before sub-task(s): ${this.stack.map(t=>t.id).join(", ")}.`);let e={identifier:this.identifier,date:new Date,duration:performance.now()-this.startTime,entries:this.entries};this.addRecord(e),this.startTime=void 0,this.entries.clear()}startSubTask(e){this.stack.push({id:e,start:performance.now(),content:0})}stopSubTask(e){let t=this.stack.pop();if(!t)throw new Error(`Task "${this.identifier}.${e}" was not started.`);if(t.id!==e)throw new Error(`Sub-Task "${t.id}" is not already stopped.`);let r=performance.now()-t.start;this.stack.at(-1)!==void 0&&(this.stack[this.stack.length-1].content+=r);let n=r-t.content;this.entries.add(e,n)}};(e=>{e.Terminals={ARROW_DIRECTION:/L|R|T|B/,ARROW_GROUP:/\{group\}/,ARROW_INTO:/<|>/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,ID:/[\w]([-\w]*\w)?/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,ARCH_ICON:/\([\w-:]+\)/,ARCH_TITLE:/\[(?:"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|[^\[\]\r\n]+)\]/}})(dV||(dV={}));(e=>{e.Terminals={DOMAIN_NAME:/complex|complicated|clear|chaotic|confusion/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(fV||(fV={}));(e=>{e.Terminals={EM_ID:/[_a-zA-Z][\w_]*/,EM_FID:/\d{1,3}/,EM_DATA_INLINE:/\{(.*)\}|"(.*)"|'(.*)'/,EM_DATA_BLOCK:/\{[\t ]*\r?\n(?:[\S\s]*?\r?\n)?\}(?:\r?\n|(?!\S))/,EM_ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,EM_ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,EM_TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,EM_WS:/\s+/,EM_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,EM_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,EM_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,EM_ML_COMMENT:/\/\*[\s\S]*?\*\//,EM_SL_COMMENT:/\/\/[^\n\r]*/}})(pV||(pV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,INT:/0|[1-9][0-9]*(?!\.)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,REFERENCE:/\w([-\./\w]*[-\w])?/}})(mV||(mV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(gV||(gV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,INT:/0|[1-9][0-9]*(?!\.)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(yV||(yV={}));(e=>{e.Terminals={NUMBER_PIE:/(?:-?[0-9]+\.[0-9]+(?!\.))|(?:-?(0|[1-9][0-9]*)(?!\.))/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(vV||(vV={}));(e=>{e.Terminals={GRATICULE:/circle|polygon/,BOOLEAN:/true|false/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,NUMBER:/(?:[0-9]+\.[0-9]+(?!\.))|(?:0|[1-9][0-9]*(?!\.))/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,ID:/[\w]([-\w]*\w)?/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(xV||(xV={}));(e=>{e.Terminals={TITLE:/title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,ACC_TITLE:/accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,ACC_DESCR:/accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ABNF_RULENAME:/[A-Za-z][A-Za-z0-9-]*/,ABNF_STRING:/"[^"]*"/,ABNF_NUMVAL:/%[xXdDbB][0-9A-Fa-f]+(?:-[0-9A-Fa-f]+|\.[0-9A-Fa-f]+)*/,ABNF_REPEAT:/[0-9]*\*[0-9]*/,ABNF_EXACT_REPEAT:/[0-9]+/,ABNF_WHITESPACE:/[\t \r\n]+/,ABNF_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,ABNF_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,ABNF_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,ABNF_COMMENT:/;[^\n\r]*/}})(bV||(bV={}));(e=>{e.Terminals={TITLE:/title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,ACC_TITLE:/accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,ACC_DESCR:/accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,EBNF_ID:/[A-Z_a-z][\w-]*/,EBNF_STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,EBNF_SPECIAL_SEQUENCE:/\?(?=[^?;]*[^?\s;][^?;]*\?)[^?;]*\?/,EBNF_WHITESPACE:/[\t \r\n]+/,EBNF_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,EBNF_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,EBNF_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,EBNF_BLOCK_COMMENT:/\/\*[\s\S]*?\*\//,EBNF_ISO_COMMENT:/\(\*[\s\S]*?\*\)/}})(TV||(TV={}));(e=>{e.Terminals={TITLE:/title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,ACC_TITLE:/accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,ACC_DESCR:/accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,RR_ID:/[A-Z_a-z][\w-]*/,RR_STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,RR_WHITESPACE:/[\t \r\n]+/,RR_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,RR_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,RR_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,RR_BLOCK_COMMENT:/\/\*[\s\S]*?\*\//}})(CV||(CV={}));(e=>{e.Terminals={TITLE:/title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,ACC_TITLE:/accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,ACC_DESCR:/accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,PEG_ID:/[A-Z_a-z][\w-]*/,PEG_STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,PEG_WHITESPACE:/[\t \r\n]+/,PEG_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,PEG_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,PEG_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,PEG_LINE_COMMENT:/#[^\n\r]*/}})(SV||(SV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,TREEMAP_KEYWORD:/treemap-beta|treemap/,CLASS_DEF:/classDef\s+([a-zA-Z_][a-zA-Z0-9_]+)(?:\s+([^;\r\n]*))?(?:;)?/,STYLE_SEPARATOR:/:::/,SEPARATOR:/:/,COMMA:/,/,INDENTATION:/[ \t]{1,}/,WS:/[ \t]+/,ML_COMMENT:/\%\%[^\n]*/,NL:/\r?\n/,ID2:/[a-zA-Z_][a-zA-Z0-9_]*/,NUMBER2:/[0-9_\.\,]+/,STRING2:/"[^"]*"|'[^']*'/}})(wV||(wV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,CLASS_ANNOTATION:/[ \t]+:::[ \t]*[A-Za-z_][\w-]*/,ICON_ANNOTATION:/[ \t]+icon\([\w-]*(?::[\w-]+)?\)/,DESC_ANNOTATION:/[ \t]+##[^\n\r]*/,INDENTATION:/[ \t]{1,}/,QUOTED_NAME:/"[^"]*"|'[^']*'/,WS:/[ \t]+/,ML_COMMENT:/\%\%[^\n]*/,NL:/\r?\n/,BARE_NAME:/(?!:::|icon\(|##)[^ \t\n\r"'](?:(?![ \t]+:::[ \t]*[A-Za-z_]|[ \t]+icon\(|[ \t]+##)[^\n\r])*/}})(kV||(kV={}));(e=>{e.Terminals={WARDLEY_NUMBER:/[0-9]+\.[0-9]+/,ARROW:/->/,LINK_PORT:/\+<>|\+>|\+|-\.->|>|\+'[^']*'<>|\+'[^']*'<|\+'[^']*'>/,LINK_LABEL:/;[^\n\r]+/,STRATEGY:/build|buy|outsource|market/,KW_WARDLEY:/wardley-beta/,KW_SIZE:/size/,KW_EVOLUTION:/evolution/,KW_ANCHOR:/anchor/,KW_COMPONENT:/component/,KW_LABEL:/label/,KW_INERTIA:/inertia/,KW_EVOLVE:/evolve/,KW_PIPELINE:/pipeline/,KW_NOTE:/note/,KW_ANNOTATIONS:/annotations/,KW_ANNOTATION:/annotation/,KW_ACCELERATOR:/accelerator/,KW_DEACCELERATOR:/deaccelerator/,NAME_WITH_SPACES:/(?!title\s|accTitle|accDescr)[A-Za-z](?:[A-Za-z0-9_()&]|-(?!>))*(?:[ \t]+[A-Za-z(](?:[A-Za-z0-9_()&]|-(?!>))*)*/,WS:/[ \t]+/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,INT:/0|[1-9][0-9]*(?!\.)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,ID:/[\w]([-\w]*\w)?/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(EV||(EV={}));eMr={...dV.Terminals,...fV.Terminals,...pV.Terminals,...mV.Terminals,...gV.Terminals,...yV.Terminals,...vV.Terminals,...xV.Terminals,...bV.Terminals,...TV.Terminals,...CV.Terminals,...SV.Terminals,...kV.Terminals,...wV.Terminals,...EV.Terminals},AV={$type:"AbnfAlternation",alternatives:"alternatives"},RV={$type:"AbnfConcatenation",elements:"elements"},rR={$type:"AbnfElement",primary:"primary",repeat:"repeat"},_V={$type:"AbnfGroup",element:"element"},LV={$type:"AbnfNumVal",value:"value"},DV={$type:"AbnfOptionalGroup",element:"element"},yg={$type:"AbnfPrimary"},nR={$type:"AbnfRule",definition:"definition",name:"name"},IV={$type:"AbnfRuleName",name:"name"},MV={$type:"AbnfStringLiteral",value:"value"},SA={$type:"Accelerator",name:"name",x:"x",y:"y"},gG={$type:"Alignment",direction:"direction",members:"members"},wA={$type:"Anchor",evolution:"evolution",name:"name",visibility:"visibility"},uC={$type:"Annotation",number:"number",text:"text",x:"x",y:"y"},yG={$type:"Annotations",x:"x",y:"y"},xc={$type:"Architecture",accDescr:"accDescr",accTitle:"accTitle",alignments:"alignments",edges:"edges",groups:"groups",junctions:"junctions",services:"services",title:"title"};s(TDe,"isArchitecture");E(TDe,"isArchitecture");kA={$type:"Axis",label:"label",name:"name"},VC={$type:"Branch",name:"name",order:"order"};s(CDe,"isBranch");E(CDe,"isBranch");twe={$type:"Checkout",branch:"branch"},EA={$type:"CherryPicking",id:"id",parent:"parent",tags:"tags"},vG={$type:"ClassDefStatement",className:"className",styleText:"styleText"},Mg={$type:"Commit",id:"id",message:"message",tags:"tags",type:"type"};s(SDe,"isCommit");E(SDe,"isCommit");AA={$type:"Common",accDescr:"accDescr",accTitle:"accTitle",title:"title"},cg={$type:"Component",decorator:"decorator",evolution:"evolution",inertia:"inertia",label:"label",name:"name",visibility:"visibility"},RA={$type:"Curve",entries:"entries",label:"label",name:"name"},hp={$type:"Cynefin",accDescr:"accDescr",accTitle:"accTitle",domains:"domains",title:"title",transitions:"transitions"};s(wDe,"isCynefin");E(wDe,"isCynefin");_A={$type:"Deaccelerator",name:"name",x:"x",y:"y"},rwe={$type:"Decorator",strategy:"strategy"},Y1={$type:"Direction",accDescr:"accDescr",accTitle:"accTitle",dir:"dir",statements:"statements",title:"title"},WC={$type:"DomainBlock",domain:"domain",items:"items"};s(kDe,"isDomainBlock");E(kDe,"isDomainBlock");QR={$type:"DomainItem",label:"label"};s(EDe,"isDomainItem");E(EDe,"isDomainItem");NV={$type:"EbnfChoice",alternatives:"alternatives"},PV={$type:"EbnfExceptionPostfix",except:"except"},OV={$type:"EbnfGroup",element:"element"},BV={$type:"EbnfNonTerminal",name:"name"},$V={$type:"EbnfOneOrMorePostfix",operator:"operator"},FV={$type:"EbnfOptional",element:"element"},GV={$type:"EbnfOptionalPostfix",operator:"operator"},tv={$type:"EbnfPostfix"},np={$type:"EbnfPrimary"},zV={$type:"EbnfRepetition",element:"element"},iR={$type:"EbnfRule",definition:"definition",name:"name"},VV={$type:"EbnfSequence",elements:"elements"},WV={$type:"EbnfSpecial",text:"text"},aR={$type:"EbnfTerm",base:"base",postfixes:"postfixes"},qV={$type:"EbnfTerminal",value:"value"},HV={$type:"EbnfZeroOrMorePostfix",operator:"operator"},_u={$type:"Edge",lhsDir:"lhsDir",lhsGroup:"lhsGroup",lhsId:"lhsId",lhsInto:"lhsInto",rhsDir:"rhsDir",rhsGroup:"rhsGroup",rhsId:"rhsId",rhsInto:"rhsInto",title:"title"},vg={$type:"EmDataEntity",dataBlockValue:"dataBlockValue",dataType:"dataType",name:"name"},ip={$type:"EmFrame"},hC={$type:"EmGwt",givenStatements:"givenStatements",sourceFrame:"sourceFrame",thenStatements:"thenStatements",whenStatements:"whenStatements"},nwe={$type:"EmGwtStatement",entityIdentifier:"entityIdentifier"},xG={$type:"EmModelEntity",name:"name"};s(ADe,"isEmModelEntityType");E(ADe,"isEmModelEntityType");LA={$type:"EmNoteEntity",dataBlockValue:"dataBlockValue",dataType:"dataType",sourceFrame:"sourceFrame"},Wh={$type:"EmResetFrame",dataInlineValue:"dataInlineValue",dataReference:"dataReference",dataType:"dataType",entityIdentifier:"entityIdentifier",modelEntityType:"modelEntityType",name:"name",sourceFrames:"sourceFrames"};s(c_,"isEmResetFrame");E(c_,"isEmResetFrame");rp={$type:"EmTimeFrame",dataInlineValue:"dataInlineValue",dataReference:"dataReference",dataType:"dataType",entityIdentifier:"entityIdentifier",modelEntityType:"modelEntityType",name:"name",sourceFrames:"sourceFrames"},bG={$type:"Entry",axis:"axis",value:"value"},Du={$type:"EventModel",accDescr:"accDescr",accTitle:"accTitle",dataEntities:"dataEntities",frames:"frames",gwtEntities:"gwtEntities",modelEntities:"modelEntities",noteEntities:"noteEntities",title:"title"},iwe={$type:"Evolution",stages:"stages"},DA={$type:"EvolutionStage",boundary:"boundary",name:"name",secondName:"secondName"},TG={$type:"Evolve",component:"component",target:"target"},dp={$type:"GitGraph",accDescr:"accDescr",accTitle:"accTitle",statements:"statements",title:"title"};s(RDe,"isGitGraph");E(RDe,"isGitGraph");dC={$type:"Group",icon:"icon",id:"id",in:"in",title:"title"},dv={$type:"Info",accDescr:"accDescr",accTitle:"accTitle",title:"title"};s(_De,"isInfo");E(_De,"isInfo");fC={$type:"Item",classSelector:"classSelector",name:"name"},CG={$type:"Junction",id:"id",in:"in"},pC={$type:"Label",negX:"negX",negY:"negY",offsetX:"offsetX",offsetY:"offsetY"},IA={$type:"Leaf",classSelector:"classSelector",name:"name",value:"value"},ug={$type:"Link",arrow:"arrow",from:"from",fromPort:"fromPort",linkLabel:"linkLabel",to:"to",toPort:"toPort"},Ng={$type:"Merge",branch:"branch",id:"id",tags:"tags",type:"type"};s(LDe,"isMerge");E(LDe,"isMerge");MA={$type:"Note",evolution:"evolution",text:"text",visibility:"visibility"},SG={$type:"Option",name:"name",value:"value"},Pg={$type:"Packet",accDescr:"accDescr",accTitle:"accTitle",blocks:"blocks",title:"title"};s(DDe,"isPacket");E(DDe,"isPacket");Og={$type:"PacketBlock",bits:"bits",end:"end",label:"label",start:"start"};s(IDe,"isPacketBlock");E(IDe,"isPacketBlock");UV={$type:"PegAny",dot:"dot"},YV={$type:"PegGroup",element:"element"},jV={$type:"PegIdentifier",name:"name"},XV={$type:"PegLiteral",value:"value"},KV={$type:"PegOrderedChoice",alternatives:"alternatives"},sR={$type:"PegPrefix",operator:"operator",suffix:"suffix"},rv={$type:"PegPrimary"},oR={$type:"PegRule",definition:"definition",name:"name"},ZV={$type:"PegSequence",elements:"elements"},lR={$type:"PegSuffix",operator:"operator",primary:"primary"},fp={$type:"Pie",accDescr:"accDescr",accTitle:"accTitle",sections:"sections",showData:"showData",title:"title"};s(MDe,"isPie");E(MDe,"isPie");qC={$type:"PieSection",label:"label",value:"value"};s(NDe,"isPieSection");E(NDe,"isPieSection");wG={$type:"Pipeline",components:"components",parent:"parent"},NA={$type:"PipelineComponent",evolution:"evolution",label:"label",name:"name"},ap={$type:"Radar",accDescr:"accDescr",accTitle:"accTitle",axes:"axes",curves:"curves",options:"options",title:"title"},Bg={$type:"Railroad",accDescr:"accDescr",accTitle:"accTitle",rules:"rules",title:"title"};s(PDe,"isRailroad");E(PDe,"isRailroad");$g={$type:"RailroadAbnf",accDescr:"accDescr",accTitle:"accTitle",rules:"rules",title:"title"};s(ODe,"isRailroadAbnf");E(ODe,"isRailroadAbnf");QV={$type:"RailroadChoiceExpr",alternatives:"alternatives"},Fg={$type:"RailroadEbnf",accDescr:"accDescr",accTitle:"accTitle",rules:"rules",title:"title"};s(BDe,"isRailroadEbnf");E(BDe,"isRailroadEbnf");Iu={$type:"RailroadExpression"},JV={$type:"RailroadNonTerminalExpr",name:"name"},eW={$type:"RailroadOneOrMoreExpr",element:"element"},tW={$type:"RailroadOptionalExpr",element:"element"},Gg={$type:"RailroadPeg",accDescr:"accDescr",accTitle:"accTitle",rules:"rules",title:"title"};s($De,"isRailroadPeg");E($De,"isRailroadPeg");cR={$type:"RailroadRule",definition:"definition",name:"name"},rW={$type:"RailroadSequenceExpr",elements:"elements"},nW={$type:"RailroadSpecialExpr",text:"text"},iW={$type:"RailroadTerminalExpr",value:"value"},aW={$type:"RailroadZeroOrMoreExpr",element:"element"},kG={$type:"Section",classSelector:"classSelector",name:"name"},j1={$type:"Service",icon:"icon",iconText:"iconText",id:"id",in:"in",title:"title"},EG={$type:"Size",height:"height",width:"width"},xg={$type:"Statement"},fv={$type:"Transition",from:"from",label:"label",to:"to"};s(FDe,"isTransition");E(FDe,"isTransition");zg={$type:"Treemap",accDescr:"accDescr",accTitle:"accTitle",title:"title",TreemapRows:"TreemapRows"};s(GDe,"isTreemap");E(GDe,"isTreemap");AG={$type:"TreemapRow",indent:"indent",item:"item"},bg={$type:"TreeNode",classAnnotation:"classAnnotation",descAnnotation:"descAnnotation",iconAnnotation:"iconAnnotation",indent:"indent",name:"name"},nv={$type:"TreeView",accDescr:"accDescr",accTitle:"accTitle",nodes:"nodes",title:"title"},Va={$type:"Wardley",accDescr:"accDescr",accelerators:"accelerators",accTitle:"accTitle",anchors:"anchors",annotation:"annotation",annotations:"annotations",components:"components",deaccelerators:"deaccelerators",evolution:"evolution",evolves:"evolves",links:"links",notes:"notes",pipelines:"pipelines",size:"size",title:"title"};s(zDe,"isWardley");E(zDe,"isWardley");VDe=class extends dW{static{s(this,"MermaidAstReflection")}constructor(){super(...arguments),this.types={AbnfAlternation:{name:AV.$type,properties:{alternatives:{name:AV.alternatives,defaultValue:[]}},superTypes:[]},AbnfConcatenation:{name:RV.$type,properties:{elements:{name:RV.elements,defaultValue:[]}},superTypes:[]},AbnfElement:{name:rR.$type,properties:{primary:{name:rR.primary},repeat:{name:rR.repeat}},superTypes:[]},AbnfGroup:{name:_V.$type,properties:{element:{name:_V.element}},superTypes:[yg.$type]},AbnfNumVal:{name:LV.$type,properties:{value:{name:LV.value}},superTypes:[yg.$type]},AbnfOptionalGroup:{name:DV.$type,properties:{element:{name:DV.element}},superTypes:[yg.$type]},AbnfPrimary:{name:yg.$type,properties:{},superTypes:[]},AbnfRule:{name:nR.$type,properties:{definition:{name:nR.definition},name:{name:nR.name}},superTypes:[]},AbnfRuleName:{name:IV.$type,properties:{name:{name:IV.name}},superTypes:[yg.$type]},AbnfStringLiteral:{name:MV.$type,properties:{value:{name:MV.value}},superTypes:[yg.$type]},Accelerator:{name:SA.$type,properties:{name:{name:SA.name},x:{name:SA.x},y:{name:SA.y}},superTypes:[]},Alignment:{name:gG.$type,properties:{direction:{name:gG.direction},members:{name:gG.members,defaultValue:[]}},superTypes:[]},Anchor:{name:wA.$type,properties:{evolution:{name:wA.evolution},name:{name:wA.name},visibility:{name:wA.visibility}},superTypes:[]},Annotation:{name:uC.$type,properties:{number:{name:uC.number},text:{name:uC.text},x:{name:uC.x},y:{name:uC.y}},superTypes:[]},Annotations:{name:yG.$type,properties:{x:{name:yG.x},y:{name:yG.y}},superTypes:[]},Architecture:{name:xc.$type,properties:{accDescr:{name:xc.accDescr},accTitle:{name:xc.accTitle},alignments:{name:xc.alignments,defaultValue:[]},edges:{name:xc.edges,defaultValue:[]},groups:{name:xc.groups,defaultValue:[]},junctions:{name:xc.junctions,defaultValue:[]},services:{name:xc.services,defaultValue:[]},title:{name:xc.title}},superTypes:[]},Axis:{name:kA.$type,properties:{label:{name:kA.label},name:{name:kA.name}},superTypes:[]},Branch:{name:VC.$type,properties:{name:{name:VC.name},order:{name:VC.order}},superTypes:[xg.$type]},Checkout:{name:twe.$type,properties:{branch:{name:twe.branch}},superTypes:[xg.$type]},CherryPicking:{name:EA.$type,properties:{id:{name:EA.id},parent:{name:EA.parent},tags:{name:EA.tags,defaultValue:[]}},superTypes:[xg.$type]},ClassDefStatement:{name:vG.$type,properties:{className:{name:vG.className},styleText:{name:vG.styleText}},superTypes:[]},Commit:{name:Mg.$type,properties:{id:{name:Mg.id},message:{name:Mg.message},tags:{name:Mg.tags,defaultValue:[]},type:{name:Mg.type}},superTypes:[xg.$type]},Common:{name:AA.$type,properties:{accDescr:{name:AA.accDescr},accTitle:{name:AA.accTitle},title:{name:AA.title}},superTypes:[]},Component:{name:cg.$type,properties:{decorator:{name:cg.decorator},evolution:{name:cg.evolution},inertia:{name:cg.inertia,defaultValue:!1},label:{name:cg.label},name:{name:cg.name},visibility:{name:cg.visibility}},superTypes:[]},Curve:{name:RA.$type,properties:{entries:{name:RA.entries,defaultValue:[]},label:{name:RA.label},name:{name:RA.name}},superTypes:[]},Cynefin:{name:hp.$type,properties:{accDescr:{name:hp.accDescr},accTitle:{name:hp.accTitle},domains:{name:hp.domains,defaultValue:[]},title:{name:hp.title},transitions:{name:hp.transitions,defaultValue:[]}},superTypes:[]},Deaccelerator:{name:_A.$type,properties:{name:{name:_A.name},x:{name:_A.x},y:{name:_A.y}},superTypes:[]},Decorator:{name:rwe.$type,properties:{strategy:{name:rwe.strategy}},superTypes:[]},Direction:{name:Y1.$type,properties:{accDescr:{name:Y1.accDescr},accTitle:{name:Y1.accTitle},dir:{name:Y1.dir},statements:{name:Y1.statements,defaultValue:[]},title:{name:Y1.title}},superTypes:[dp.$type]},DomainBlock:{name:WC.$type,properties:{domain:{name:WC.domain},items:{name:WC.items,defaultValue:[]}},superTypes:[]},DomainItem:{name:QR.$type,properties:{label:{name:QR.label}},superTypes:[]},EbnfChoice:{name:NV.$type,properties:{alternatives:{name:NV.alternatives,defaultValue:[]}},superTypes:[]},EbnfExceptionPostfix:{name:PV.$type,properties:{except:{name:PV.except}},superTypes:[tv.$type]},EbnfGroup:{name:OV.$type,properties:{element:{name:OV.element}},superTypes:[np.$type]},EbnfNonTerminal:{name:BV.$type,properties:{name:{name:BV.name}},superTypes:[np.$type]},EbnfOneOrMorePostfix:{name:$V.$type,properties:{operator:{name:$V.operator}},superTypes:[tv.$type]},EbnfOptional:{name:FV.$type,properties:{element:{name:FV.element}},superTypes:[np.$type]},EbnfOptionalPostfix:{name:GV.$type,properties:{operator:{name:GV.operator}},superTypes:[tv.$type]},EbnfPostfix:{name:tv.$type,properties:{},superTypes:[]},EbnfPrimary:{name:np.$type,properties:{},superTypes:[]},EbnfRepetition:{name:zV.$type,properties:{element:{name:zV.element}},superTypes:[np.$type]},EbnfRule:{name:iR.$type,properties:{definition:{name:iR.definition},name:{name:iR.name}},superTypes:[]},EbnfSequence:{name:VV.$type,properties:{elements:{name:VV.elements,defaultValue:[]}},superTypes:[]},EbnfSpecial:{name:WV.$type,properties:{text:{name:WV.text}},superTypes:[np.$type]},EbnfTerm:{name:aR.$type,properties:{base:{name:aR.base},postfixes:{name:aR.postfixes,defaultValue:[]}},superTypes:[]},EbnfTerminal:{name:qV.$type,properties:{value:{name:qV.value}},superTypes:[np.$type]},EbnfZeroOrMorePostfix:{name:HV.$type,properties:{operator:{name:HV.operator}},superTypes:[tv.$type]},Edge:{name:_u.$type,properties:{lhsDir:{name:_u.lhsDir},lhsGroup:{name:_u.lhsGroup,defaultValue:!1},lhsId:{name:_u.lhsId},lhsInto:{name:_u.lhsInto,defaultValue:!1},rhsDir:{name:_u.rhsDir},rhsGroup:{name:_u.rhsGroup,defaultValue:!1},rhsId:{name:_u.rhsId},rhsInto:{name:_u.rhsInto,defaultValue:!1},title:{name:_u.title}},superTypes:[]},EmDataEntity:{name:vg.$type,properties:{dataBlockValue:{name:vg.dataBlockValue},dataType:{name:vg.dataType},name:{name:vg.name}},superTypes:[]},EmFrame:{name:ip.$type,properties:{},superTypes:[]},EmGwt:{name:hC.$type,properties:{givenStatements:{name:hC.givenStatements,defaultValue:[]},sourceFrame:{name:hC.sourceFrame,referenceType:ip.$type},thenStatements:{name:hC.thenStatements,defaultValue:[]},whenStatements:{name:hC.whenStatements,defaultValue:[]}},superTypes:[]},EmGwtStatement:{name:nwe.$type,properties:{entityIdentifier:{name:nwe.entityIdentifier,referenceType:xG.$type}},superTypes:[]},EmModelEntity:{name:xG.$type,properties:{name:{name:xG.name}},superTypes:[]},EmNoteEntity:{name:LA.$type,properties:{dataBlockValue:{name:LA.dataBlockValue},dataType:{name:LA.dataType},sourceFrame:{name:LA.sourceFrame,referenceType:ip.$type}},superTypes:[]},EmResetFrame:{name:Wh.$type,properties:{dataInlineValue:{name:Wh.dataInlineValue},dataReference:{name:Wh.dataReference,referenceType:vg.$type},dataType:{name:Wh.dataType},entityIdentifier:{name:Wh.entityIdentifier},modelEntityType:{name:Wh.modelEntityType},name:{name:Wh.name},sourceFrames:{name:Wh.sourceFrames,defaultValue:[],referenceType:ip.$type}},superTypes:[ip.$type]},EmTimeFrame:{name:rp.$type,properties:{dataInlineValue:{name:rp.dataInlineValue},dataReference:{name:rp.dataReference,referenceType:vg.$type},dataType:{name:rp.dataType},entityIdentifier:{name:rp.entityIdentifier},modelEntityType:{name:rp.modelEntityType},name:{name:rp.name},sourceFrames:{name:rp.sourceFrames,defaultValue:[],referenceType:ip.$type}},superTypes:[ip.$type]},Entry:{name:bG.$type,properties:{axis:{name:bG.axis,referenceType:kA.$type},value:{name:bG.value}},superTypes:[]},EventModel:{name:Du.$type,properties:{accDescr:{name:Du.accDescr},accTitle:{name:Du.accTitle},dataEntities:{name:Du.dataEntities,defaultValue:[]},frames:{name:Du.frames,defaultValue:[]},gwtEntities:{name:Du.gwtEntities,defaultValue:[]},modelEntities:{name:Du.modelEntities,defaultValue:[]},noteEntities:{name:Du.noteEntities,defaultValue:[]},title:{name:Du.title}},superTypes:[]},Evolution:{name:iwe.$type,properties:{stages:{name:iwe.stages,defaultValue:[]}},superTypes:[]},EvolutionStage:{name:DA.$type,properties:{boundary:{name:DA.boundary},name:{name:DA.name},secondName:{name:DA.secondName}},superTypes:[]},Evolve:{name:TG.$type,properties:{component:{name:TG.component},target:{name:TG.target}},superTypes:[]},GitGraph:{name:dp.$type,properties:{accDescr:{name:dp.accDescr},accTitle:{name:dp.accTitle},statements:{name:dp.statements,defaultValue:[]},title:{name:dp.title}},superTypes:[]},Group:{name:dC.$type,properties:{icon:{name:dC.icon},id:{name:dC.id},in:{name:dC.in},title:{name:dC.title}},superTypes:[]},Info:{name:dv.$type,properties:{accDescr:{name:dv.accDescr},accTitle:{name:dv.accTitle},title:{name:dv.title}},superTypes:[]},Item:{name:fC.$type,properties:{classSelector:{name:fC.classSelector},name:{name:fC.name}},superTypes:[]},Junction:{name:CG.$type,properties:{id:{name:CG.id},in:{name:CG.in}},superTypes:[]},Label:{name:pC.$type,properties:{negX:{name:pC.negX,defaultValue:!1},negY:{name:pC.negY,defaultValue:!1},offsetX:{name:pC.offsetX},offsetY:{name:pC.offsetY}},superTypes:[]},Leaf:{name:IA.$type,properties:{classSelector:{name:IA.classSelector},name:{name:IA.name},value:{name:IA.value}},superTypes:[fC.$type]},Link:{name:ug.$type,properties:{arrow:{name:ug.arrow},from:{name:ug.from},fromPort:{name:ug.fromPort},linkLabel:{name:ug.linkLabel},to:{name:ug.to},toPort:{name:ug.toPort}},superTypes:[]},Merge:{name:Ng.$type,properties:{branch:{name:Ng.branch},id:{name:Ng.id},tags:{name:Ng.tags,defaultValue:[]},type:{name:Ng.type}},superTypes:[xg.$type]},Note:{name:MA.$type,properties:{evolution:{name:MA.evolution},text:{name:MA.text},visibility:{name:MA.visibility}},superTypes:[]},Option:{name:SG.$type,properties:{name:{name:SG.name},value:{name:SG.value,defaultValue:!1}},superTypes:[]},Packet:{name:Pg.$type,properties:{accDescr:{name:Pg.accDescr},accTitle:{name:Pg.accTitle},blocks:{name:Pg.blocks,defaultValue:[]},title:{name:Pg.title}},superTypes:[]},PacketBlock:{name:Og.$type,properties:{bits:{name:Og.bits},end:{name:Og.end},label:{name:Og.label},start:{name:Og.start}},superTypes:[]},PegAny:{name:UV.$type,properties:{dot:{name:UV.dot}},superTypes:[rv.$type]},PegGroup:{name:YV.$type,properties:{element:{name:YV.element}},superTypes:[rv.$type]},PegIdentifier:{name:jV.$type,properties:{name:{name:jV.name}},superTypes:[rv.$type]},PegLiteral:{name:XV.$type,properties:{value:{name:XV.value}},superTypes:[rv.$type]},PegOrderedChoice:{name:KV.$type,properties:{alternatives:{name:KV.alternatives,defaultValue:[]}},superTypes:[]},PegPrefix:{name:sR.$type,properties:{operator:{name:sR.operator},suffix:{name:sR.suffix}},superTypes:[]},PegPrimary:{name:rv.$type,properties:{},superTypes:[]},PegRule:{name:oR.$type,properties:{definition:{name:oR.definition},name:{name:oR.name}},superTypes:[]},PegSequence:{name:ZV.$type,properties:{elements:{name:ZV.elements,defaultValue:[]}},superTypes:[]},PegSuffix:{name:lR.$type,properties:{operator:{name:lR.operator},primary:{name:lR.primary}},superTypes:[]},Pie:{name:fp.$type,properties:{accDescr:{name:fp.accDescr},accTitle:{name:fp.accTitle},sections:{name:fp.sections,defaultValue:[]},showData:{name:fp.showData,defaultValue:!1},title:{name:fp.title}},superTypes:[]},PieSection:{name:qC.$type,properties:{label:{name:qC.label},value:{name:qC.value}},superTypes:[]},Pipeline:{name:wG.$type,properties:{components:{name:wG.components,defaultValue:[]},parent:{name:wG.parent}},superTypes:[]},PipelineComponent:{name:NA.$type,properties:{evolution:{name:NA.evolution},label:{name:NA.label},name:{name:NA.name}},superTypes:[]},Radar:{name:ap.$type,properties:{accDescr:{name:ap.accDescr},accTitle:{name:ap.accTitle},axes:{name:ap.axes,defaultValue:[]},curves:{name:ap.curves,defaultValue:[]},options:{name:ap.options,defaultValue:[]},title:{name:ap.title}},superTypes:[]},Railroad:{name:Bg.$type,properties:{accDescr:{name:Bg.accDescr},accTitle:{name:Bg.accTitle},rules:{name:Bg.rules,defaultValue:[]},title:{name:Bg.title}},superTypes:[]},RailroadAbnf:{name:$g.$type,properties:{accDescr:{name:$g.accDescr},accTitle:{name:$g.accTitle},rules:{name:$g.rules,defaultValue:[]},title:{name:$g.title}},superTypes:[]},RailroadChoiceExpr:{name:QV.$type,properties:{alternatives:{name:QV.alternatives,defaultValue:[]}},superTypes:[Iu.$type]},RailroadEbnf:{name:Fg.$type,properties:{accDescr:{name:Fg.accDescr},accTitle:{name:Fg.accTitle},rules:{name:Fg.rules,defaultValue:[]},title:{name:Fg.title}},superTypes:[]},RailroadExpression:{name:Iu.$type,properties:{},superTypes:[]},RailroadNonTerminalExpr:{name:JV.$type,properties:{name:{name:JV.name}},superTypes:[Iu.$type]},RailroadOneOrMoreExpr:{name:eW.$type,properties:{element:{name:eW.element}},superTypes:[Iu.$type]},RailroadOptionalExpr:{name:tW.$type,properties:{element:{name:tW.element}},superTypes:[Iu.$type]},RailroadPeg:{name:Gg.$type,properties:{accDescr:{name:Gg.accDescr},accTitle:{name:Gg.accTitle},rules:{name:Gg.rules,defaultValue:[]},title:{name:Gg.title}},superTypes:[]},RailroadRule:{name:cR.$type,properties:{definition:{name:cR.definition},name:{name:cR.name}},superTypes:[]},RailroadSequenceExpr:{name:rW.$type,properties:{elements:{name:rW.elements,defaultValue:[]}},superTypes:[Iu.$type]},RailroadSpecialExpr:{name:nW.$type,properties:{text:{name:nW.text}},superTypes:[Iu.$type]},RailroadTerminalExpr:{name:iW.$type,properties:{value:{name:iW.value}},superTypes:[Iu.$type]},RailroadZeroOrMoreExpr:{name:aW.$type,properties:{element:{name:aW.element}},superTypes:[Iu.$type]},Section:{name:kG.$type,properties:{classSelector:{name:kG.classSelector},name:{name:kG.name}},superTypes:[fC.$type]},Service:{name:j1.$type,properties:{icon:{name:j1.icon},iconText:{name:j1.iconText},id:{name:j1.id},in:{name:j1.in},title:{name:j1.title}},superTypes:[]},Size:{name:EG.$type,properties:{height:{name:EG.height},width:{name:EG.width}},superTypes:[]},Statement:{name:xg.$type,properties:{},superTypes:[]},Transition:{name:fv.$type,properties:{from:{name:fv.from},label:{name:fv.label},to:{name:fv.to}},superTypes:[]},TreeNode:{name:bg.$type,properties:{classAnnotation:{name:bg.classAnnotation},descAnnotation:{name:bg.descAnnotation},iconAnnotation:{name:bg.iconAnnotation},indent:{name:bg.indent},name:{name:bg.name}},superTypes:[]},TreeView:{name:nv.$type,properties:{accDescr:{name:nv.accDescr},accTitle:{name:nv.accTitle},nodes:{name:nv.nodes,defaultValue:[]},title:{name:nv.title}},superTypes:[]},Treemap:{name:zg.$type,properties:{accDescr:{name:zg.accDescr},accTitle:{name:zg.accTitle},title:{name:zg.title},TreemapRows:{name:zg.TreemapRows,defaultValue:[]}},superTypes:[]},TreemapRow:{name:AG.$type,properties:{indent:{name:AG.indent},item:{name:AG.item}},superTypes:[]},Wardley:{name:Va.$type,properties:{accDescr:{name:Va.accDescr},accelerators:{name:Va.accelerators,defaultValue:[]},accTitle:{name:Va.accTitle},anchors:{name:Va.anchors,defaultValue:[]},annotation:{name:Va.annotation,defaultValue:[]},annotations:{name:Va.annotations,defaultValue:[]},components:{name:Va.components,defaultValue:[]},deaccelerators:{name:Va.deaccelerators,defaultValue:[]},evolution:{name:Va.evolution},evolves:{name:Va.evolves,defaultValue:[]},links:{name:Va.links,defaultValue:[]},notes:{name:Va.notes,defaultValue:[]},pipelines:{name:Va.pipelines,defaultValue:[]},size:{name:Va.size},title:{name:Va.title}},superTypes:[]}}}static{E(this,"MermaidAstReflection")}},Ui=new VDe,zkt=E(()=>awe??(awe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"ArchitectureGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Architecture","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[],"cardinality":"*"},{"$type":"Keyword","value":"architecture-beta"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Statement","definition":{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"groups","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"services","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"junctions","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}},{"$type":"Assignment","feature":"edges","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}},{"$type":"Assignment","feature":"alignments","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"LeftPort","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":":"},{"$type":"Assignment","feature":"lhsDir","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"RightPort","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"rhsDir","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}},{"$type":"Keyword","value":":"}]},"entry":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Arrow","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]},{"$type":"Assignment","feature":"lhsInto","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"--"},{"$type":"Group","elements":[{"$type":"Keyword","value":"-"},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@30"},"arguments":[]}},{"$type":"Keyword","value":"-"}]}]},{"$type":"Assignment","feature":"rhsInto","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"Group","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"group"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Assignment","feature":"icon","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@29"},"arguments":[]},"cardinality":"?"},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@30"},"arguments":[]},"cardinality":"?"},{"$type":"Group","elements":[{"$type":"Keyword","value":"in"},{"$type":"Assignment","feature":"in","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Service","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"service"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"iconText","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]}},{"$type":"Assignment","feature":"icon","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@29"},"arguments":[]}}],"cardinality":"?"},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@30"},"arguments":[]},"cardinality":"?"},{"$type":"Group","elements":[{"$type":"Keyword","value":"in"},{"$type":"Assignment","feature":"in","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Junction","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"junction"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"in"},{"$type":"Assignment","feature":"in","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Edge","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"lhsId","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Assignment","feature":"lhsGroup","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]},{"$type":"Assignment","feature":"rhsId","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Assignment","feature":"rhsGroup","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Alignment","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"align"},{"$type":"Assignment","feature":"direction","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"row"},{"$type":"Keyword","value":"column"}]}},{"$type":"Assignment","feature":"members","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Assignment","feature":"members","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]},"cardinality":"+"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"ARROW_DIRECTION","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"L"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"R"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"T"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"B"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ARROW_GROUP","definition":{"$type":"RegexToken","regex":"/\\\\{group\\\\}/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ARROW_INTO","definition":{"$type":"RegexToken","regex":"/<|>/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@19"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@20"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","name":"ARCH_ICON","definition":{"$type":"RegexToken","regex":"/\\\\([\\\\w-:]+\\\\)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ARCH_TITLE","definition":{"$type":"RegexToken","regex":"/\\\\[(?:\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'|[^\\\\[\\\\]\\\\r\\\\n]+)\\\\]/","parenthesized":false},"fragment":false,"hidden":false}],"interfaces":[],"types":[]}`)),"ArchitectureGrammarGrammar"),Vkt=E(()=>swe??(swe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"CynefinGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Cynefin","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"cynefin-beta"},{"$type":"Keyword","value":"cynefin-beta:"}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"Assignment","feature":"domains","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"transitions","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"DomainBlock","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"domain","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"},{"$type":"Group","elements":[{"$type":"Assignment","feature":"items","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"DomainItem","definition":{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Transition","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"from","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Keyword","value":"-->"},{"$type":"Assignment","feature":"to","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":":"},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"DOMAIN_NAME","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"complex"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"complicated"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"clear"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"chaotic"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"confusion"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@11"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@12"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"CynefinGrammarGrammar"),Wkt=E(()=>owe??(owe=Ha('{"$type":"Grammar","isDeclared":true,"name":"EventModeling","interfaces":[{"$type":"Interface","name":"Common","attributes":[{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]}],"rules":[{"$type":"ParserRule","entry":true,"name":"EventModel","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"eventmodeling"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]}},{"$type":"Assignment","feature":"modelEntities","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"frames","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}},{"$type":"Assignment","feature":"dataEntities","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}},{"$type":"Assignment","feature":"noteEntities","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}},{"$type":"Assignment","feature":"gwtEntities","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmModelEntityType","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"rmo"},{"$type":"Keyword","value":"readmodel"},{"$type":"Keyword","value":"ui"},{"$type":"Keyword","value":"cmd"},{"$type":"Keyword","value":"command"},{"$type":"Keyword","value":"evt"},{"$type":"Keyword","value":"event"},{"$type":"Keyword","value":"pcr"},{"$type":"Keyword","value":"processor"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmDataType","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"json"},{"$type":"Keyword","value":"jsobj"},{"$type":"Keyword","value":"figma"},{"$type":"Keyword","value":"salt"},{"$type":"Keyword","value":"uri"},{"$type":"Keyword","value":"md"},{"$type":"Keyword","value":"html"},{"$type":"Keyword","value":"text"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"EmDataInline","definition":{"$type":"Group","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"`"},{"$type":"Assignment","feature":"dataType","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Keyword","value":"`"}],"cardinality":"?"},{"$type":"Assignment","feature":"dataInlineValue","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"EmDataBlock","definition":{"$type":"Group","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"`"},{"$type":"Assignment","feature":"dataType","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Keyword","value":"`"}],"cardinality":"?"},{"$type":"Assignment","feature":"dataBlockValue","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"QualifiedName","dataType":"string","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},{"$type":"Group","elements":[{"$type":"Keyword","value":"."},{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmTimeFrame","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"tf"},{"$type":"Keyword","value":"timeframe"}]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Assignment","feature":"modelEntityType","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"entityIdentifier","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"->>"},{"$type":"Assignment","feature":"sourceFrames","operator":"+=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@8"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}}],"cardinality":"*"},{"$type":"Group","elements":[{"$type":"Keyword","value":"[["},{"$type":"Assignment","feature":"dataReference","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@10"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"Keyword","value":"]]"}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmResetFrame","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"rf"},{"$type":"Keyword","value":"resetframe"}]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Assignment","feature":"modelEntityType","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"entityIdentifier","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"->>"},{"$type":"Assignment","feature":"sourceFrames","operator":"+=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@8"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}}],"cardinality":"*"},{"$type":"Group","elements":[{"$type":"Keyword","value":"[["},{"$type":"Assignment","feature":"dataReference","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@10"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"Keyword","value":"]]"}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmFrame","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmModelEntity","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"entity"},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmDataEntity","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"data"},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmNoteEntity","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"note"},{"$type":"Assignment","feature":"sourceFrame","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@8"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmGwt","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"gwt"},{"$type":"Assignment","feature":"sourceFrame","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@8"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"Keyword","value":"given"},{"$type":"Assignment","feature":"givenStatements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},"cardinality":"+"},{"$type":"Group","elements":[{"$type":"Keyword","value":"when"},{"$type":"Assignment","feature":"whenStatements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},"cardinality":"+"}],"cardinality":"?"},{"$type":"Keyword","value":"then"},{"$type":"Assignment","feature":"thenStatements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},"cardinality":"+"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmGwtStatement","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]},{"$type":"Assignment","feature":"entityIdentifier","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@9"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EM_EID","dataType":"string","definition":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EM_FI","dataType":"string","definition":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"EM_ID","definition":{"$type":"RegexToken","regex":"/[_a-zA-Z][\\\\w_]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_FID","definition":{"$type":"RegexToken","regex":"/\\\\d{1,3}/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_DATA_INLINE","definition":{"$type":"RegexToken","regex":"/\\\\{(.*)\\\\}|\\"(.*)\\"|\'(.*)\'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_DATA_BLOCK","definition":{"$type":"RegexToken","regex":"/\\\\{[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?\\\\}(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"EM_WS","definition":{"$type":"RegexToken","regex":"/\\\\s+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_ML_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\//","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_SL_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\/\\\\/[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"imports":[],"types":[]}')),"EventModelingGrammar"),qkt=E(()=>lwe??(lwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"GitGraphGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"GitGraph","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"gitGraph"},{"$type":"Group","elements":[{"$type":"Keyword","value":"gitGraph"},{"$type":"Keyword","value":":"}]},{"$type":"Keyword","value":"gitGraph:"},{"$type":"Group","elements":[{"$type":"Keyword","value":"gitGraph"},{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]},{"$type":"Keyword","value":":"}]}]},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]},{"$type":"Assignment","feature":"statements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Statement","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Direction","definition":{"$type":"Assignment","feature":"dir","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"LR"},{"$type":"Keyword","value":"TB"},{"$type":"Keyword","value":"BT"}]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Commit","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"commit"},{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"id:"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"msg:","cardinality":"?"},{"$type":"Assignment","feature":"message","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"tag:"},{"$type":"Assignment","feature":"tags","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"type:"},{"$type":"Assignment","feature":"type","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"NORMAL"},{"$type":"Keyword","value":"REVERSE"},{"$type":"Keyword","value":"HIGHLIGHT"}]}}]}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Branch","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"branch"},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"order:"},{"$type":"Assignment","feature":"order","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Merge","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"merge"},{"$type":"Assignment","feature":"branch","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}]}},{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"id:"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"tag:"},{"$type":"Assignment","feature":"tags","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"type:"},{"$type":"Assignment","feature":"type","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"NORMAL"},{"$type":"Keyword","value":"REVERSE"},{"$type":"Keyword","value":"HIGHLIGHT"}]}}]}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Checkout","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"checkout"},{"$type":"Keyword","value":"switch"}]},{"$type":"Assignment","feature":"branch","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"CherryPicking","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"cherry-pick"},{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"id:"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"tag:"},{"$type":"Assignment","feature":"tags","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"parent:"},{"$type":"Assignment","feature":"parent","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@14"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@15"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","name":"REFERENCE","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\\\w([-\\\\./\\\\w]*[-\\\\w])?/","parenthesized":false},"fragment":false,"hidden":false}],"interfaces":[],"types":[]}`)),"GitGraphGrammarGrammar"),Hkt=E(()=>cwe??(cwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"InfoGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Info","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[],"cardinality":"*"},{"$type":"Keyword","value":"info"},{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[],"cardinality":"*"},{"$type":"Group","elements":[{"$type":"Keyword","value":"showInfo"},{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[],"cardinality":"*"}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[],"cardinality":"?"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@7"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@8"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"InfoGrammarGrammar"),Ukt=E(()=>uwe??(uwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"PacketGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Packet","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"packet"},{"$type":"Keyword","value":"packet-beta"}]},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]},{"$type":"Assignment","feature":"blocks","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PacketBlock","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Assignment","feature":"start","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"-"},{"$type":"Assignment","feature":"end","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}}],"cardinality":"?"}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"+"},{"$type":"Assignment","feature":"bits","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}}]}]},{"$type":"Keyword","value":":"},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@8"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@9"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"PacketGrammarGrammar"),Ykt=E(()=>hwe??(hwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"PieGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Pie","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"},{"$type":"Keyword","value":"pie"},{"$type":"Assignment","feature":"showData","operator":"?=","terminal":{"$type":"Keyword","value":"showData"},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"Assignment","feature":"sections","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PieSection","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":":"},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"FLOAT_PIE","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/-?[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT_PIE","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/-?(0|[1-9][0-9]*)(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER_PIE","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@2"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@3"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@11"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@12"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"PieGrammarGrammar"),jkt=E(()=>dwe??(dwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"RadarGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Radar","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"radar-beta"},{"$type":"Keyword","value":"radar-beta:"},{"$type":"Group","elements":[{"$type":"Keyword","value":"radar-beta"},{"$type":"Keyword","value":":"}]}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]},{"$type":"Group","elements":[{"$type":"Keyword","value":"axis"},{"$type":"Assignment","feature":"axes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"axes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"curve"},{"$type":"Assignment","feature":"curves","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"curves","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}}],"cardinality":"*"}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"options","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"options","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}}],"cardinality":"*"}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Label","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}},{"$type":"Keyword","value":"]"}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"Axis","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Curve","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[],"cardinality":"?"},{"$type":"Keyword","value":"{"},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]},{"$type":"Keyword","value":"}"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Entries","definition":{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Assignment","feature":"entries","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Assignment","feature":"entries","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"}]},{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Assignment","feature":"entries","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Assignment","feature":"entries","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"}]}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"DetailedEntry","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"axis","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@2"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"Keyword","value":":","cardinality":"?"},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"NumberEntry","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Option","definition":{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"showLegend"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"ticks"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"max"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"min"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"graticule"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}}]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"GRATICULE","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"circle"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"polygon"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@15"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@16"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[{"$type":"Interface","name":"Entry","attributes":[{"$type":"TypeAttribute","name":"axis","isOptional":true,"type":{"$type":"ReferenceType","referenceType":{"$type":"SimpleType","typeRef":{"$ref":"#/rules@2"}},"isMulti":false}},{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"number"},"isOptional":false}],"superTypes":[]}],"types":[]}`)),"RadarGrammarGrammar"),Xkt=E(()=>fwe??(fwe=Ha('{"$type":"Grammar","isDeclared":true,"name":"RailroadAbnfGrammar","rules":[{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_RULENAME","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[A-Za-z][A-Za-z0-9-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"[^\\"]*\\"/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_NUMVAL","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/%[xXdDbB][0-9A-Fa-f]+(?:-[0-9A-Fa-f]+|\\\\.[0-9A-Fa-f]+)*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_REPEAT","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[0-9]*\\\\*[0-9]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_EXACT_REPEAT","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[0-9]+/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t \\\\r\\\\n]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_COMMENT","definition":{"$type":"RegexToken","regex":"/;[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"ParserRule","entry":true,"name":"RailroadAbnf","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"railroad-abnf-beta"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"},{"$type":"Assignment","feature":"rules","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfRule","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Keyword","value":"="},{"$type":"Assignment","feature":"definition","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Keyword","value":";"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfAlternation","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"/"},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfConcatenation","returnType":{"$ref":"#/interfaces@3"},"definition":{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]},"cardinality":"+"},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfElement","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"repeat","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"repeat","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}}],"cardinality":"?"},{"$type":"Assignment","feature":"primary","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfPrimary","returnType":{"$ref":"#/interfaces@5"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfStringLiteral","returnType":{"$ref":"#/interfaces@6"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfNumVal","returnType":{"$ref":"#/interfaces@7"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfRuleName","returnType":{"$ref":"#/interfaces@8"},"definition":{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfGroup","returnType":{"$ref":"#/interfaces@9"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfOptionalGroup","returnType":{"$ref":"#/interfaces@10"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Keyword","value":"]"}]},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"RailroadAbnf","attributes":[{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"rules","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@1"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfRule","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"definition","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfAlternation","attributes":[{"$type":"TypeAttribute","name":"alternatives","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@3"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfConcatenation","attributes":[{"$type":"TypeAttribute","name":"elements","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@4"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfElement","attributes":[{"$type":"TypeAttribute","name":"repeat","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"primary","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@5"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfPrimary","attributes":[],"superTypes":[]},{"$type":"Interface","name":"AbnfStringLiteral","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"AbnfNumVal","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"AbnfRuleName","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"AbnfGroup","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"AbnfOptionalGroup","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]}],"imports":[],"types":[]}')),"RailroadAbnfGrammarGrammar"),Kkt=E(()=>pwe??(pwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"RailroadEbnfGrammar","rules":[{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EBNF_ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[A-Z_a-z][\\\\w-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EBNF_STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EBNF_SPECIAL_SEQUENCE","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\\\?(?=[^?;]*[^?\\\\s;][^?;]*\\\\?)[^?;]*\\\\?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t \\\\r\\\\n]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_BLOCK_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\//","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_ISO_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\(\\\\*[\\\\s\\\\S]*?\\\\*\\\\)/","parenthesized":false},"fragment":false},{"$type":"ParserRule","entry":true,"name":"RailroadEbnf","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"railroad-ebnf-beta"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"},{"$type":"Assignment","feature":"rules","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfRule","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"="},{"$type":"Keyword","value":"::="}]},{"$type":"Assignment","feature":"definition","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":";"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfChoice","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"|"},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfSequence","returnType":{"$ref":"#/interfaces@3"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":",","cardinality":"?"},{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfTerm","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"base","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},{"$type":"Assignment","feature":"postfixes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfPrimary","returnType":{"$ref":"#/interfaces@5"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfTerminal","returnType":{"$ref":"#/interfaces@7"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfNonTerminal","returnType":{"$ref":"#/interfaces@8"},"definition":{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfSpecial","returnType":{"$ref":"#/interfaces@9"},"definition":{"$type":"Assignment","feature":"text","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfGroup","returnType":{"$ref":"#/interfaces@10"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfOptional","returnType":{"$ref":"#/interfaces@11"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":"]"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfRepetition","returnType":{"$ref":"#/interfaces@12"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"{"},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":"}"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfPostfix","returnType":{"$ref":"#/interfaces@6"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@25"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@26"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@27"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@28"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfOptionalPostfix","returnType":{"$ref":"#/interfaces@13"},"definition":{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"?"}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfZeroOrMorePostfix","returnType":{"$ref":"#/interfaces@14"},"definition":{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"*"}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfOneOrMorePostfix","returnType":{"$ref":"#/interfaces@15"},"definition":{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"+"}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfExceptionPostfix","returnType":{"$ref":"#/interfaces@16"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"-"},{"$type":"Assignment","feature":"except","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"RailroadEbnf","attributes":[{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"rules","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@1"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfRule","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"definition","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfChoice","attributes":[{"$type":"TypeAttribute","name":"alternatives","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@3"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfSequence","attributes":[{"$type":"TypeAttribute","name":"elements","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@4"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfTerm","attributes":[{"$type":"TypeAttribute","name":"base","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@5"}},"isOptional":false},{"$type":"TypeAttribute","name":"postfixes","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@6"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfPrimary","attributes":[],"superTypes":[]},{"$type":"Interface","name":"EbnfPostfix","attributes":[],"superTypes":[]},{"$type":"Interface","name":"EbnfTerminal","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfNonTerminal","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfSpecial","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"text","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfGroup","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"EbnfOptional","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"EbnfRepetition","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"EbnfOptionalPostfix","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"operator","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfZeroOrMorePostfix","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"operator","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfOneOrMorePostfix","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"operator","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfExceptionPostfix","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"except","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@5"}},"isOptional":false}]}],"imports":[],"types":[]}`)),"RailroadEbnfGrammarGrammar"),Zkt=E(()=>mwe??(mwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"RailroadGrammar","rules":[{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"RR_ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[A-Z_a-z][\\\\w-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"RR_STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"RR_WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t \\\\r\\\\n]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"RR_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"RR_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"RR_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"RR_BLOCK_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\//","parenthesized":false},"fragment":false},{"$type":"ParserRule","entry":true,"name":"Railroad","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"railroad-beta"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"},{"$type":"Assignment","feature":"rules","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadRule","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Keyword","value":"="},{"$type":"Assignment","feature":"definition","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":";"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadExpression","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadSequenceExpr","returnType":{"$ref":"#/interfaces@3"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"sequence"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}}],"cardinality":"*"},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadChoiceExpr","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"choice"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}}],"cardinality":"*"},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadOptionalExpr","returnType":{"$ref":"#/interfaces@5"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"optional"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadOneOrMoreExpr","returnType":{"$ref":"#/interfaces@6"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"oneOrMore"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadZeroOrMoreExpr","returnType":{"$ref":"#/interfaces@7"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"zeroOrMore"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadTerminalExpr","returnType":{"$ref":"#/interfaces@8"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"terminal"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadNonTerminalExpr","returnType":{"$ref":"#/interfaces@9"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"nonterminal"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadSpecialExpr","returnType":{"$ref":"#/interfaces@10"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"special"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"text","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"Railroad","attributes":[{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"rules","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@1"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"RailroadRule","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"definition","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"RailroadExpression","attributes":[],"superTypes":[]},{"$type":"Interface","name":"RailroadSequenceExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"elements","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}}},"isOptional":false}]},{"$type":"Interface","name":"RailroadChoiceExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"alternatives","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}}},"isOptional":false}]},{"$type":"Interface","name":"RailroadOptionalExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"RailroadOneOrMoreExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"RailroadZeroOrMoreExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"RailroadTerminalExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"RailroadNonTerminalExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"RailroadSpecialExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"text","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]}],"imports":[],"types":[]}`)),"RailroadGrammarGrammar"),Qkt=E(()=>gwe??(gwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"RailroadPegGrammar","rules":[{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"PEG_ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[A-Z_a-z][\\\\w-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"PEG_STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t \\\\r\\\\n]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/#[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"ParserRule","entry":true,"name":"RailroadPeg","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"railroad-peg-beta"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"},{"$type":"Assignment","feature":"rules","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegRule","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Keyword","value":"<-"},{"$type":"Assignment","feature":"definition","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":";"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegOrderedChoice","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"/"},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegSequence","returnType":{"$ref":"#/interfaces@3"},"definition":{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"cardinality":"+"},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegPrefix","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"&"}},{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"!"}}],"cardinality":"?"},{"$type":"Assignment","feature":"suffix","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegSuffix","returnType":{"$ref":"#/interfaces@5"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"primary","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"?"}},{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"*"}},{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"+"}}],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegPrimary","returnType":{"$ref":"#/interfaces@6"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegLiteral","returnType":{"$ref":"#/interfaces@7"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegIdentifier","returnType":{"$ref":"#/interfaces@8"},"definition":{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegGroup","returnType":{"$ref":"#/interfaces@9"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegAny","returnType":{"$ref":"#/interfaces@10"},"definition":{"$type":"Assignment","feature":"dot","operator":"=","terminal":{"$type":"Keyword","value":"."}},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"RailroadPeg","attributes":[{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"rules","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@1"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegRule","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"definition","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegOrderedChoice","attributes":[{"$type":"TypeAttribute","name":"alternatives","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@3"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegSequence","attributes":[{"$type":"TypeAttribute","name":"elements","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@4"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegPrefix","attributes":[{"$type":"TypeAttribute","name":"operator","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"suffix","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@5"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegSuffix","attributes":[{"$type":"TypeAttribute","name":"primary","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@6"}},"isOptional":false},{"$type":"TypeAttribute","name":"operator","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]},{"$type":"Interface","name":"PegPrimary","attributes":[],"superTypes":[]},{"$type":"Interface","name":"PegLiteral","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"PegIdentifier","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"PegGroup","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"PegAny","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"dot","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]}],"imports":[],"types":[]}`)),"RailroadPegGrammarGrammar"),Jkt=E(()=>ywe??(ywe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"TreemapGrammar","rules":[{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","entry":true,"name":"Treemap","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]},{"$type":"Assignment","feature":"TreemapRows","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"TREEMAP_KEYWORD","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"treemap-beta"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"treemap"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"CLASS_DEF","definition":{"$type":"RegexToken","regex":"/classDef\\\\s+([a-zA-Z_][a-zA-Z0-9_]+)(?:\\\\s+([^;\\\\r\\\\n]*))?(?:;)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STYLE_SEPARATOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":":::"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"SEPARATOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":":"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"COMMA","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":","},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INDENTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]{1,}/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WS","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ML_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\%\\\\%[^\\\\n]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"NL","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false},{"$type":"ParserRule","name":"TreemapRow","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"indent","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"item","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"ClassDef","dataType":"string","definition":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Item","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Section","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]},{"$type":"Assignment","feature":"classSelector","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}}],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Leaf","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[],"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[],"cardinality":"?"},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]},{"$type":"Assignment","feature":"classSelector","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}}],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"ID2","definition":{"$type":"RegexToken","regex":"/[a-zA-Z_][a-zA-Z0-9_]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER2","definition":{"$type":"RegexToken","regex":"/[0-9_\\\\.\\\\,]+/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"MyNumber","dataType":"number","definition":{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"STRING2","definition":{"$type":"RegexToken","regex":"/\\"[^\\"]*\\"|'[^']*'/","parenthesized":false},"fragment":false,"hidden":false}],"interfaces":[{"$type":"Interface","name":"Item","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"classSelector","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]},{"$type":"Interface","name":"Section","superTypes":[{"$ref":"#/interfaces@0"}],"attributes":[]},{"$type":"Interface","name":"Leaf","superTypes":[{"$ref":"#/interfaces@0"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"number"},"isOptional":false}]},{"$type":"Interface","name":"ClassDefStatement","attributes":[{"$type":"TypeAttribute","name":"className","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"styleText","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"Treemap","attributes":[{"$type":"TypeAttribute","name":"TreemapRows","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/rules@15"}}},"isOptional":false},{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]}],"imports":[],"types":[],"$comment":"/**\\n * Treemap grammar for Langium\\n * Converted from mindmap grammar\\n *\\n * The ML_COMMENT and NL hidden terminals handle whitespace, comments, and newlines\\n * before the treemap keyword, allowing for empty lines and comments before the\\n * treemap declaration.\\n */"}`)),"TreemapGrammarGrammar"),eEt=E(()=>vwe??(vwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"TreeViewGrammar","rules":[{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","entry":true,"name":"TreeView","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"treeView-beta"},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[],"cardinality":"?"},{"$type":"Assignment","feature":"nodes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"CLASS_ANNOTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+:::[ \\\\t]*[A-Za-z_][\\\\w-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ICON_ANNOTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+icon\\\\([\\\\w-]*(?::[\\\\w-]+)?\\\\)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"DESC_ANNOTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+##[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INDENTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]{1,}/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"QUOTED_NAME","definition":{"$type":"RegexToken","regex":"/\\"[^\\"]*\\"|'[^']*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WS","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ML_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\%\\\\%[^\\\\n]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"NL","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","name":"BARE_NAME","definition":{"$type":"RegexToken","regex":"/(?!:::|icon\\\\(|##)[^ \\\\t\\\\n\\\\r\\"'](?:(?![ \\\\t]+:::[ \\\\t]*[A-Za-z_]|[ \\\\t]+icon\\\\(|[ \\\\t]+##)[^\\\\n\\\\r])*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"TreeNode","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"indent","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}}]},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"classAnnotation","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"iconAnnotation","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"descAnnotation","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"TreeView","attributes":[{"$type":"TypeAttribute","name":"nodes","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/rules@14"}}},"isOptional":false},{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]}],"imports":[],"types":[],"$comment":"/**\\n * TreeView grammar for Langium\\n *\\n * Supports both quoted labels (\\"my file\\") and bare labels (index.js).\\n * Annotations (:::class, icon(), ## description) are parsed directly into\\n * AST fields by the grammar. Value conversion for stripping quotes, extracting\\n * class names, icon names, and description text happens in valueConverter.ts.\\n *\\n * The ML_COMMENT and NL hidden terminals handle whitespace, comments, and newlines\\n * before the treeView keyword, allowing for empty lines and comments before the\\n * treeView declaration.\\n */"}`)),"TreeViewGrammarGrammar"),tEt=E(()=>xwe??(xwe=Ha(`{"$type":"Grammar","isDeclared":true,"name":"WardleyGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Wardley","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@52"},"arguments":[],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@25"},"arguments":[]},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@52"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@42"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Statement","definition":{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"size","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Assignment","feature":"anchors","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"components","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"links","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"evolves","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}},{"$type":"Assignment","feature":"pipelines","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}},{"$type":"Assignment","feature":"notes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}},{"$type":"Assignment","feature":"annotations","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Assignment","feature":"annotation","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Assignment","feature":"accelerators","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},{"$type":"Assignment","feature":"deaccelerators","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"Size","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@26"},"arguments":[]},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"width","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"height","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Evolution","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@27"},"arguments":[]},{"$type":"Assignment","feature":"stages","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]},{"$type":"Assignment","feature":"stages","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}}],"cardinality":"+"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EvolutionStage","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"@"},{"$type":"Assignment","feature":"boundary","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}}],"cardinality":"?"},{"$type":"Group","elements":[{"$type":"Keyword","value":"/"},{"$type":"Assignment","feature":"secondName","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}}],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Anchor","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@28"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"visibility","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Component","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@29"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"visibility","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]},"cardinality":"?"},{"$type":"Assignment","feature":"decorator","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"inertia","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@31"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"inertia","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@31"},"arguments":[]}},{"$type":"Keyword","value":")"}]}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Label","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@30"},"arguments":[]},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"negX","operator":"?=","terminal":{"$type":"Keyword","value":"-"},"cardinality":"?"},{"$type":"Assignment","feature":"offsetX","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"negY","operator":"?=","terminal":{"$type":"Keyword","value":"-"},"cardinality":"?"},{"$type":"Assignment","feature":"offsetY","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":"]"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Decorator","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"strategy","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Link","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"from","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Assignment","feature":"fromPort","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},"cardinality":"?"},{"$type":"Assignment","feature":"arrow","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}]},"cardinality":"?"},{"$type":"Assignment","feature":"to","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Assignment","feature":"toPort","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},"cardinality":"?"},{"$type":"Assignment","feature":"linkLabel","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Evolve","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@32"},"arguments":[]},{"$type":"Assignment","feature":"component","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Assignment","feature":"target","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Pipeline","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@33"},"arguments":[]},{"$type":"Assignment","feature":"parent","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"{"},{"$type":"RuleCall","rule":{"$ref":"#/rules@52"},"arguments":[],"cardinality":"+"},{"$type":"Assignment","feature":"components","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]},"cardinality":"+"},{"$type":"Keyword","value":"}"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PipelineComponent","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@29"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Note","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@34"},"arguments":[]},{"$type":"Assignment","feature":"text","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"visibility","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Annotations","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@35"},"arguments":[]},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"x","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"y","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Annotation","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@36"},"arguments":[]},{"$type":"Assignment","feature":"number","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"x","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"y","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"Assignment","feature":"text","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"CoordinateValue","dataType":"number","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Accelerator","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@37"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"x","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"y","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Deaccelerator","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@38"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"x","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"y","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"WARDLEY_NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ARROW","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"->"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"LINK_PORT","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"+<>"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"+>"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"+<"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"LINK_ARROW","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"-->"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"-.->"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":">"},"parenthesized":false}],"parenthesized":false},{"$type":"RegexToken","regex":"/\\\\+'[^']*'<>/","parenthesized":false}],"parenthesized":false},{"$type":"RegexToken","regex":"/\\\\+'[^']*'/","parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"LINK_LABEL","definition":{"$type":"RegexToken","regex":"/;[^\\\\n\\\\r]+/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRATEGY","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"build"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"buy"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"outsource"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"market"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_WARDLEY","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"wardley-beta"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_SIZE","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"size"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_EVOLUTION","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"evolution"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_ANCHOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"anchor"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_COMPONENT","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"component"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_LABEL","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"label"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_INERTIA","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"inertia"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_EVOLVE","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"evolve"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_PIPELINE","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"pipeline"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_NOTE","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"note"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_ANNOTATIONS","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"annotations"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_ANNOTATION","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"annotation"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_ACCELERATOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"accelerator"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_DEACCELERATOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"deaccelerator"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NAME_WITH_SPACES","definition":{"$type":"RegexToken","regex":"/(?!title\\\\s|accTitle|accDescr)[A-Za-z](?:[A-Za-z0-9_()&]|-(?!>))*(?:[ \\\\t]+[A-Za-z(](?:[A-Za-z0-9_()&]|-(?!>))*)*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WS","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+/","parenthesized":false},"fragment":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@52"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@44"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@45"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@46"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@47"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@48"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"WardleyGrammarGrammar"),rEt={languageId:"architecture",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},nEt={languageId:"cynefin",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},iEt={languageId:"eventmodeling",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},aEt={languageId:"gitGraph",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},sEt={languageId:"info",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},oEt={languageId:"packet",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},lEt={languageId:"pie",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},cEt={languageId:"radar",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},uEt={languageId:"railroadAbnf",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},hEt={languageId:"railroadEbnf",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},dEt={languageId:"railroad",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},fEt={languageId:"railroadPeg",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},pEt={languageId:"treemap",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},mEt={languageId:"treeView",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},gEt={languageId:"wardley",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},In={AstReflection:E(()=>new VDe,"AstReflection")},HH={Grammar:E(()=>zkt(),"Grammar"),LanguageMetaData:E(()=>rEt,"LanguageMetaData"),parser:{}},UH={Grammar:E(()=>Vkt(),"Grammar"),LanguageMetaData:E(()=>nEt,"LanguageMetaData"),parser:{}},YH={Grammar:E(()=>Wkt(),"Grammar"),LanguageMetaData:E(()=>iEt,"LanguageMetaData"),parser:{}},jH={Grammar:E(()=>qkt(),"Grammar"),LanguageMetaData:E(()=>aEt,"LanguageMetaData"),parser:{}},XH={Grammar:E(()=>Hkt(),"Grammar"),LanguageMetaData:E(()=>sEt,"LanguageMetaData"),parser:{}},KH={Grammar:E(()=>Ukt(),"Grammar"),LanguageMetaData:E(()=>oEt,"LanguageMetaData"),parser:{}},ZH={Grammar:E(()=>Ykt(),"Grammar"),LanguageMetaData:E(()=>lEt,"LanguageMetaData"),parser:{}},QH={Grammar:E(()=>jkt(),"Grammar"),LanguageMetaData:E(()=>cEt,"LanguageMetaData"),parser:{}},JH={Grammar:E(()=>Xkt(),"Grammar"),LanguageMetaData:E(()=>uEt,"LanguageMetaData"),parser:{}},eU={Grammar:E(()=>Kkt(),"Grammar"),LanguageMetaData:E(()=>hEt,"LanguageMetaData"),parser:{}},tU={Grammar:E(()=>Zkt(),"Grammar"),LanguageMetaData:E(()=>dEt,"LanguageMetaData"),parser:{}},rU={Grammar:E(()=>Qkt(),"Grammar"),LanguageMetaData:E(()=>fEt,"LanguageMetaData"),parser:{}},nU={Grammar:E(()=>Jkt(),"Grammar"),LanguageMetaData:E(()=>pEt,"LanguageMetaData"),parser:{}},iU={Grammar:E(()=>eEt(),"Grammar"),LanguageMetaData:E(()=>mEt,"LanguageMetaData"),parser:{}},aU={Grammar:E(()=>tEt(),"Grammar"),LanguageMetaData:E(()=>gEt,"LanguageMetaData"),parser:{}},yEt=/accDescr(?:[\t ]*:([^\n\r]*)|\s*{([^}]*)})/,vEt=/accTitle[\t ]*:([^\n\r]*)/,xEt=/title([\t ][^\n\r]*|)/,bEt={ACC_DESCR:yEt,ACC_TITLE:vEt,TITLE:xEt},oa=class extends SH{static{s(this,"AbstractMermaidValueConverter")}static{E(this,"AbstractMermaidValueConverter")}runConverter(e,t,r){let n=this.runCommonConverter(e,t,r);return n===void 0&&(n=this.runCustomConverter(e,t,r)),n===void 0?super.runConverter(e,t,r):n}runCommonConverter(e,t,r){let n=bEt[e.name];if(n===void 0)return;let i=n.exec(t);if(i!==null){if(i[1]!==void 0)return i[1].trim().replace(/[\t ]{2,}/gm," ");if(i[2]!==void 0)return i[2].replace(/^\s*/gm,"").replace(/\s+$/gm,"").replace(/[\t ]{2,}/gm," ").replace(/[\n\r]{2,}/gm,` +`)}}},al=class extends oa{static{s(this,"CommonValueConverter")}static{E(this,"CommonValueConverter")}runCustomConverter(e,t,r){}},Bn=class extends e_{static{s(this,"AbstractMermaidTokenBuilder")}static{E(this,"AbstractMermaidTokenBuilder")}constructor(e){super(),this.keywords=new Set(e)}buildKeywordTokens(e,t,r){let n=super.buildKeywordTokens(e,t,r);return n.forEach(i=>{this.keywords.has(i.name)&&i.PATTERN!==void 0&&(i.PATTERN=new RegExp(i.PATTERN.toString()+"(?:(?=%%)|(?!\\S))"))}),n}},TEt=class extends Bn{static{s(this,"CommonTokenBuilder")}static{E(this,"CommonTokenBuilder")}};});function h_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),QH,u_);return t.ServiceRegistry.register(r),{shared:t,Radar:r}}var CEt,u_,sU=F(()=>{"use strict";Sn();CEt=class extends Bn{static{s(this,"RadarTokenBuilder")}static{E(this,"RadarTokenBuilder")}constructor(){super(["radar-beta"])}},u_={parser:{TokenBuilder:E(()=>new CEt,"TokenBuilder"),ValueConverter:E(()=>new al,"ValueConverter")}};s(h_,"createRadarServices");E(h_,"createRadarServices")});function Fv(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),tU,d_);return t.ServiceRegistry.register(r),{shared:t,Railroad:r}}var SEt,WDe,wEt,d_,oU=F(()=>{"use strict";Sn();SEt=class extends Bn{static{s(this,"RailroadTokenBuilder")}static{E(this,"RailroadTokenBuilder")}constructor(){super(["railroad-beta"])}},WDe=E(e=>{let t=e.slice(1,-1),r="";for(let n=0;nnew SEt,"TokenBuilder"),ValueConverter:E(()=>new wEt,"ValueConverter")}};s(Fv,"createRailroadServices");E(Fv,"createRailroadServices")});function Gv(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),eU,f_);return t.ServiceRegistry.register(r),{shared:t,RailroadEbnf:r}}var kEt,qDe,EEt,f_,lU=F(()=>{"use strict";Sn();kEt=class extends Bn{static{s(this,"RailroadEbnfTokenBuilder")}static{E(this,"RailroadEbnfTokenBuilder")}constructor(){super(["railroad-ebnf-beta"])}},qDe=E(e=>{let t=e.slice(1,-1),r="";for(let n=0;nnew kEt,"TokenBuilder"),ValueConverter:E(()=>new EEt,"ValueConverter")}};s(Gv,"createRailroadEbnfServices");E(Gv,"createRailroadEbnfServices")});function zv(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),JH,p_);return t.ServiceRegistry.register(r),{shared:t,RailroadAbnf:r}}var AEt,REt,p_,cU=F(()=>{"use strict";Sn();AEt=class extends Bn{static{s(this,"RailroadAbnfTokenBuilder")}static{E(this,"RailroadAbnfTokenBuilder")}constructor(){super(["railroad-abnf-beta"])}},REt=class extends oa{static{s(this,"RailroadAbnfValueConverter")}static{E(this,"RailroadAbnfValueConverter")}runConverter(e,t,r){let n=super.runConverter(e,t,r);if(e.name==="TITLE"&&typeof n=="string"){let i=n.trim();if(i.startsWith('"')&&i.endsWith('"')||i.startsWith("'")&&i.endsWith("'"))return i.slice(1,-1)}return n}runCustomConverter(e,t,r){if(e.name==="ABNF_STRING")return t.slice(1,-1)}},p_={parser:{TokenBuilder:E(()=>new AEt,"TokenBuilder"),ValueConverter:E(()=>new REt,"ValueConverter")}};s(zv,"createRailroadAbnfServices");E(zv,"createRailroadAbnfServices")});function Vv(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),rU,m_);return t.ServiceRegistry.register(r),{shared:t,RailroadPeg:r}}var _Et,HDe,LEt,m_,uU=F(()=>{"use strict";Sn();_Et=class extends Bn{static{s(this,"RailroadPegTokenBuilder")}static{E(this,"RailroadPegTokenBuilder")}constructor(){super(["railroad-peg-beta"])}},HDe=E(e=>{let t=e.slice(1,-1),r="";for(let n=0;nnew _Et,"TokenBuilder"),ValueConverter:E(()=>new LEt,"ValueConverter")}};s(Vv,"createRailroadPegServices");E(Vv,"createRailroadPegServices")});function UDe(e){let t=e.validation.TreemapValidator,r=e.validation.ValidationRegistry;if(r){let n={Treemap:t.checkSingleRoot.bind(t)};r.register(n,t)}}function y_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),nU,g_);return t.ServiceRegistry.register(r),UDe(r),{shared:t,Treemap:r}}var DEt,IEt,MEt,NEt,g_,hU=F(()=>{"use strict";Sn();DEt=class extends Bn{static{s(this,"TreemapTokenBuilder")}static{E(this,"TreemapTokenBuilder")}constructor(){super(["treemap"])}},IEt=/classDef\s+([A-Z_a-z]\w+)(?:\s+([^\n\r;]*))?;?/,MEt=class extends oa{static{s(this,"TreemapValueConverter")}static{E(this,"TreemapValueConverter")}runCustomConverter(e,t,r){if(e.name==="NUMBER2")return parseFloat(t.replace(/,/g,""));if(e.name==="SEPARATOR")return t.substring(1,t.length-1);if(e.name==="STRING2")return t.substring(1,t.length-1);if(e.name==="INDENTATION")return t.length;if(e.name==="ClassDef"){if(typeof t!="string")return t;let n=IEt.exec(t);if(n)return{$type:"ClassDefStatement",className:n[1],styleText:n[2]||void 0}}}};s(UDe,"registerValidationChecks");E(UDe,"registerValidationChecks");NEt=class{static{s(this,"TreemapValidator")}static{E(this,"TreemapValidator")}checkSingleRoot(e,t){let r;for(let n of e.TreemapRows)n.item&&(r===void 0&&n.indent===void 0?r=0:n.indent===void 0?t("error","Multiple root nodes are not allowed in a treemap.",{node:n,property:"item"}):r!==void 0&&r>=parseInt(n.indent,10)&&t("error","Multiple root nodes are not allowed in a treemap.",{node:n,property:"item"}))}},g_={parser:{TokenBuilder:E(()=>new DEt,"TokenBuilder"),ValueConverter:E(()=>new MEt,"ValueConverter")},validation:{TreemapValidator:E(()=>new NEt,"TreemapValidator")}};s(y_,"createTreemapServices");E(y_,"createTreemapServices")});function x_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),aU,v_);return t.ServiceRegistry.register(r),{shared:t,Wardley:r}}var PEt,v_,dU=F(()=>{"use strict";Sn();PEt=class extends oa{static{s(this,"WardleyValueConverter")}static{E(this,"WardleyValueConverter")}runCustomConverter(e,t,r){switch(e.name.toUpperCase()){case"LINK_LABEL":return t.substring(1).trim();default:return}}},v_={parser:{ValueConverter:E(()=>new PEt,"ValueConverter")}};s(x_,"createWardleyServices");E(x_,"createWardleyServices")});function T_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),UH,b_);return t.ServiceRegistry.register(r),{shared:t,Cynefin:r}}var OEt,b_,fU=F(()=>{"use strict";Sn();OEt=class extends Bn{static{s(this,"CynefinTokenBuilder")}static{E(this,"CynefinTokenBuilder")}constructor(){super(["cynefin-beta"])}},b_={parser:{TokenBuilder:E(()=>new OEt,"TokenBuilder"),ValueConverter:E(()=>new al,"ValueConverter")}};s(T_,"createCynefinServices");E(T_,"createCynefinServices")});function S_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),jH,C_);return t.ServiceRegistry.register(r),{shared:t,GitGraph:r}}var BEt,C_,pU=F(()=>{"use strict";Sn();BEt=class extends Bn{static{s(this,"GitGraphTokenBuilder")}static{E(this,"GitGraphTokenBuilder")}constructor(){super(["gitGraph"])}},C_={parser:{TokenBuilder:E(()=>new BEt,"TokenBuilder"),ValueConverter:E(()=>new al,"ValueConverter")}};s(S_,"createGitGraphServices");E(S_,"createGitGraphServices")});function k_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),XH,w_);return t.ServiceRegistry.register(r),{shared:t,Info:r}}var $Et,w_,mU=F(()=>{"use strict";Sn();$Et=class extends Bn{static{s(this,"InfoTokenBuilder")}static{E(this,"InfoTokenBuilder")}constructor(){super(["info","showInfo"])}},w_={parser:{TokenBuilder:E(()=>new $Et,"TokenBuilder"),ValueConverter:E(()=>new al,"ValueConverter")}};s(k_,"createInfoServices");E(k_,"createInfoServices")});function A_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),KH,E_);return t.ServiceRegistry.register(r),{shared:t,Packet:r}}var FEt,E_,gU=F(()=>{"use strict";Sn();FEt=class extends Bn{static{s(this,"PacketTokenBuilder")}static{E(this,"PacketTokenBuilder")}constructor(){super(["packet"])}},E_={parser:{TokenBuilder:E(()=>new FEt,"TokenBuilder"),ValueConverter:E(()=>new al,"ValueConverter")}};s(A_,"createPacketServices");E(A_,"createPacketServices")});function __(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),ZH,R_);return t.ServiceRegistry.register(r),{shared:t,Pie:r}}var GEt,zEt,R_,yU=F(()=>{"use strict";Sn();GEt=class extends Bn{static{s(this,"PieTokenBuilder")}static{E(this,"PieTokenBuilder")}constructor(){super(["pie","showData"])}},zEt=class extends oa{static{s(this,"PieValueConverter")}static{E(this,"PieValueConverter")}runCustomConverter(e,t,r){if(e.name==="PIE_SECTION_LABEL")return t.replace(/"/g,"").trim()}},R_={parser:{TokenBuilder:E(()=>new GEt,"TokenBuilder"),ValueConverter:E(()=>new zEt,"ValueConverter")}};s(__,"createPieServices");E(__,"createPieServices")});function D_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),iU,L_);return t.ServiceRegistry.register(r),{shared:t,TreeView:r}}var VEt,WEt,L_,vU=F(()=>{"use strict";Sn();VEt=class extends oa{static{s(this,"TreeViewValueConverter")}static{E(this,"TreeViewValueConverter")}runCustomConverter(e,t,r){if(e.name==="INDENTATION")return t?.length||0;if(e.name==="QUOTED_NAME")return t.substring(1,t.length-1);if(e.name==="BARE_NAME")return t.replace(/[\t ]+$/,"");if(e.name==="CLASS_ANNOTATION")return t.trim().substring(3).trim();if(e.name==="ICON_ANNOTATION"){let n=t.trim();return n.substring(5,n.length-1)}if(e.name==="DESC_ANNOTATION")return t.trim().substring(2).trim()}},WEt=class extends Bn{static{s(this,"TreeViewTokenBuilder")}static{E(this,"TreeViewTokenBuilder")}constructor(){super(["treeView-beta"])}},L_={parser:{TokenBuilder:E(()=>new WEt,"TokenBuilder"),ValueConverter:E(()=>new VEt,"ValueConverter")}};s(D_,"createTreeViewServices");E(D_,"createTreeViewServices")});function M_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),HH,I_);return t.ServiceRegistry.register(r),{shared:t,Architecture:r}}var qEt,HEt,I_,xU=F(()=>{"use strict";Sn();qEt=class extends Bn{static{s(this,"ArchitectureTokenBuilder")}static{E(this,"ArchitectureTokenBuilder")}constructor(){super(["architecture"])}},HEt=class extends oa{static{s(this,"ArchitectureValueConverter")}static{E(this,"ArchitectureValueConverter")}runCustomConverter(e,t,r){if(e.name==="ARCH_ICON")return t.replace(/[()]/g,"").trim();if(e.name==="ARCH_TEXT_ICON")return t.replace(/["()]/g,"");if(e.name==="ARCH_TITLE"){let n=t.replace(/^\[|]$/g,"").trim();return(n.startsWith('"')&&n.endsWith('"')||n.startsWith("'")&&n.endsWith("'"))&&(n=n.slice(1,-1),n=n.replace(/\\"/g,'"').replace(/\\'/g,"'")),n.trim()}}},I_={parser:{TokenBuilder:E(()=>new qEt,"TokenBuilder"),ValueConverter:E(()=>new HEt,"ValueConverter")}};s(M_,"createArchitectureServices");E(M_,"createArchitectureServices")});function ZDe(e){let t=e.validation.EventModelingValidator,r=e.validation.ValidationRegistry;if(r){let n={EmTimeFrame:t.checkSourceFrameTypes.bind(t),EmResetFrame:t.checkSourceFrameTypes.bind(t)};r.register(n,t)}}function P_(e=Cn){let t=$r(vn(e),In),r=$r(yn({shared:t}),YH,N_);return t.ServiceRegistry.register(r),ZDe(r),{shared:t,EventModel:r}}var UEt,YDe,jDe,bU,XDe,KDe,YEt,N_,TU=F(()=>{"use strict";Sn();UEt=class extends Bn{static{s(this,"EventModelingTokenBuilder")}static{E(this,"EventModelingTokenBuilder")}constructor(){super(["eventmodeling"])}},YDe=new Set(["cmd","command"]),jDe=new Set(["evt","event"]),bU=new Set(["rmo","readmodel"]),XDe=new Set(["pcr","processor"]),KDe=new Set(["ui"]);s(ZDe,"registerValidationChecks");E(ZDe,"registerValidationChecks");YEt=class{static{s(this,"EventModelingValidator")}static{E(this,"EventModelingValidator")}checkSourceFrameTypes(e,t){e.sourceFrames.length!==0&&(YDe.has(e.modelEntityType)?this.validateSources(e,new Set([...KDe,...XDe]),"command","ui or processor",t):jDe.has(e.modelEntityType)?this.validateSources(e,YDe,"event","command",t):bU.has(e.modelEntityType)?this.validateSources(e,jDe,"read model","event",t):XDe.has(e.modelEntityType)?this.validateSources(e,bU,"processor","read model",t):KDe.has(e.modelEntityType)&&this.validateSources(e,bU,"ui","read model",t))}validateSources(e,t,r,n,i){for(let a of e.sourceFrames){let o=a.ref;o!==void 0&&!t.has(o.modelEntityType)&&i("error",`A ${r} can only receive input from a ${n}, not from '${o.modelEntityType}'.`,{node:e,property:"sourceFrames"})}}},N_={parser:{TokenBuilder:E(()=>new UEt,"TokenBuilder"),ValueConverter:E(()=>new al,"ValueConverter")},validation:{EventModelingValidator:E(()=>new YEt,"EventModelingValidator")}};s(P_,"createEventModelingServices");E(P_,"createEventModelingServices")});var QDe={};sr(QDe,{InfoModule:()=>w_,createInfoServices:()=>k_});var JDe=F(()=>{"use strict";mU();Sn()});var e7e={};sr(e7e,{PacketModule:()=>E_,createPacketServices:()=>A_});var t7e=F(()=>{"use strict";gU();Sn()});var r7e={};sr(r7e,{PieModule:()=>R_,createPieServices:()=>__});var n7e=F(()=>{"use strict";yU();Sn()});var i7e={};sr(i7e,{TreeViewModule:()=>L_,createTreeViewServices:()=>D_});var a7e=F(()=>{"use strict";vU();Sn()});var s7e={};sr(s7e,{ArchitectureModule:()=>I_,createArchitectureServices:()=>M_});var o7e=F(()=>{"use strict";xU();Sn()});var l7e={};sr(l7e,{GitGraphModule:()=>C_,createGitGraphServices:()=>S_});var c7e=F(()=>{"use strict";pU();Sn()});var u7e={};sr(u7e,{EventModelingModule:()=>N_,createEventModelingServices:()=>P_});var h7e=F(()=>{"use strict";TU();Sn()});var d7e={};sr(d7e,{RadarModule:()=>u_,createRadarServices:()=>h_});var f7e=F(()=>{"use strict";sU();Sn()});var p7e={};sr(p7e,{RailroadModule:()=>d_,createRailroadServices:()=>Fv});var m7e=F(()=>{"use strict";oU();Sn()});var g7e={};sr(g7e,{RailroadEbnfModule:()=>f_,createRailroadEbnfServices:()=>Gv});var y7e=F(()=>{"use strict";lU();Sn()});var v7e={};sr(v7e,{RailroadAbnfModule:()=>p_,createRailroadAbnfServices:()=>zv});var x7e=F(()=>{"use strict";cU();Sn()});var b7e={};sr(b7e,{RailroadPegModule:()=>m_,createRailroadPegServices:()=>Vv});var T7e=F(()=>{"use strict";uU();Sn()});var C7e={};sr(C7e,{TreemapModule:()=>g_,createTreemapServices:()=>y_});var S7e=F(()=>{"use strict";hU();Sn()});var w7e={};sr(w7e,{WardleyModule:()=>v_,createWardleyServices:()=>x_});var k7e=F(()=>{"use strict";dU();Sn()});var E7e={};sr(E7e,{CynefinModule:()=>b_,createCynefinServices:()=>T_});var A7e=F(()=>{"use strict";fU();Sn()});async function Li(e,t){let r=jEt[e];if(!r)throw new Error(`Unknown diagram type: ${e}`);os[e]||await r();let i=os[e].parse(t);if(i.lexerErrors.length>0||i.parserErrors.length>0)throw new od(i);return i.value}var os,jEt,od,ls=F(()=>{"use strict";sU();oU();lU();cU();uU();hU();dU();fU();pU();mU();gU();yU();vU();xU();TU();Sn();os={},jEt={info:E(async()=>{let{createInfoServices:e}=await Promise.resolve().then(()=>(JDe(),QDe)),t=e().Info.parser.LangiumParser;os.info=t},"info"),packet:E(async()=>{let{createPacketServices:e}=await Promise.resolve().then(()=>(t7e(),e7e)),t=e().Packet.parser.LangiumParser;os.packet=t},"packet"),pie:E(async()=>{let{createPieServices:e}=await Promise.resolve().then(()=>(n7e(),r7e)),t=e().Pie.parser.LangiumParser;os.pie=t},"pie"),treeView:E(async()=>{let{createTreeViewServices:e}=await Promise.resolve().then(()=>(a7e(),i7e)),t=e().TreeView.parser.LangiumParser;os.treeView=t},"treeView"),architecture:E(async()=>{let{createArchitectureServices:e}=await Promise.resolve().then(()=>(o7e(),s7e)),t=e().Architecture.parser.LangiumParser;os.architecture=t},"architecture"),gitGraph:E(async()=>{let{createGitGraphServices:e}=await Promise.resolve().then(()=>(c7e(),l7e)),t=e().GitGraph.parser.LangiumParser;os.gitGraph=t},"gitGraph"),eventmodeling:E(async()=>{let{createEventModelingServices:e}=await Promise.resolve().then(()=>(h7e(),u7e)),t=e().EventModel.parser.LangiumParser;os.eventmodeling=t},"eventmodeling"),radar:E(async()=>{let{createRadarServices:e}=await Promise.resolve().then(()=>(f7e(),d7e)),t=e().Radar.parser.LangiumParser;os.radar=t},"radar"),railroad:E(async()=>{let{createRailroadServices:e}=await Promise.resolve().then(()=>(m7e(),p7e)),t=e().Railroad.parser.LangiumParser;os.railroad=t},"railroad"),railroadEbnf:E(async()=>{let{createRailroadEbnfServices:e}=await Promise.resolve().then(()=>(y7e(),g7e)),t=e().RailroadEbnf.parser.LangiumParser;os.railroadEbnf=t},"railroadEbnf"),railroadAbnf:E(async()=>{let{createRailroadAbnfServices:e}=await Promise.resolve().then(()=>(x7e(),v7e)),t=e().RailroadAbnf.parser.LangiumParser;os.railroadAbnf=t},"railroadAbnf"),railroadPeg:E(async()=>{let{createRailroadPegServices:e}=await Promise.resolve().then(()=>(T7e(),b7e)),t=e().RailroadPeg.parser.LangiumParser;os.railroadPeg=t},"railroadPeg"),treemap:E(async()=>{let{createTreemapServices:e}=await Promise.resolve().then(()=>(S7e(),C7e)),t=e().Treemap.parser.LangiumParser;os.treemap=t},"treemap"),wardley:E(async()=>{let{createWardleyServices:e}=await Promise.resolve().then(()=>(k7e(),w7e)),t=e().Wardley.parser.LangiumParser;os.wardley=t},"wardley"),cynefin:E(async()=>{let{createCynefinServices:e}=await Promise.resolve().then(()=>(A7e(),E7e)),t=e().Cynefin.parser.LangiumParser;os.cynefin=t},"cynefin")};s(Li,"parse");E(Li,"parse");od=class extends Error{static{s(this,"MermaidParseError")}constructor(e){let t=e.lexerErrors.map(n=>{let i=n.line!==void 0&&!isNaN(n.line)?n.line:"?",a=n.column!==void 0&&!isNaN(n.column)?n.column:"?";return`Lexer error on line ${i}, column ${a}: ${n.message}`}).join(` +`),r=e.parserErrors.map(n=>{let i=n.token.startLine!==void 0&&!isNaN(n.token.startLine)?n.token.startLine:"?",a=n.token.startColumn!==void 0&&!isNaN(n.token.startColumn)?n.token.startColumn:"?";return`Parse error on line ${i}, column ${a}: ${n.message}`}).join(` +`);super(`Parsing failed: ${t} ${r}`),this.result=e}static{E(this,"MermaidParseError")}}});function Hn(e,t){e.accDescr&&t.setAccDescription?.(e.accDescr),e.accTitle&&t.setAccTitle?.(e.accTitle),e.title&&t.setDiagramTitle?.(e.title)}var to=F(()=>{"use strict";s(Hn,"populateCommonDb")});var Mn,O_=F(()=>{"use strict";Mn={NORMAL:0,REVERSE:1,HIGHLIGHT:2,MERGE:3,CHERRY_PICK:4}});var wp,B_=F(()=>{"use strict";wp=class{constructor(t){this.init=t;this.records=this.init()}static{s(this,"ImperativeState")}reset(){this.records=this.init()}}});function CU(){return jN({length:7})}function KEt(e,t){let r=Object.create(null);return e.reduce((n,i)=>{let a=t(i);return r[a]||(r[a]=!0,n.push(i)),n},[])}function R7e(e,t,r){let n=e.indexOf(t);n===-1?e.push(r):e.splice(n,1,r)}function L7e(e){let t=e.reduce((i,a)=>i.seq>a.seq?i:a,e[0]),r="";e.forEach(function(i){i===t?r+=" *":r+=" |"});let n=[r,t.id,t.seq];for(let i in qt.records.branches)qt.records.branches.get(i)===t.id&&n.push(i);if(se.debug(n.join(" ")),t.parents&&t.parents.length==2&&t.parents[0]&&t.parents[1]){let i=qt.records.commits.get(t.parents[0]);R7e(e,t,i),t.parents[1]&&e.push(qt.records.commits.get(t.parents[1]))}else{if(t.parents.length==0)return;if(t.parents[0]){let i=qt.records.commits.get(t.parents[0]);R7e(e,t,i)}}e=KEt(e,i=>i.id),L7e(e)}var XEt,x0,qt,ZEt,QEt,JEt,e4t,t4t,r4t,n4t,_7e,i4t,a4t,s4t,o4t,l4t,D7e,c4t,u4t,h4t,$_,SU=F(()=>{"use strict";St();er();br();Yr();$n();O_();B_();Qi();XEt=mr.gitGraph,x0=s(()=>Xr({...XEt,...Lt().gitGraph}),"getConfig"),qt=new wp(()=>{let e=x0(),t=e.mainBranchName,r=e.mainBranchOrder;return{mainBranchName:t,commits:new Map,head:null,branchConfig:new Map([[t,{name:t,order:r}]]),branches:new Map([[t,null]]),currBranch:t,direction:"LR",seq:0,options:{}}});s(CU,"getID");s(KEt,"uniqBy");ZEt=s(function(e){qt.records.direction=e},"setDirection"),QEt=s(function(e){se.debug("options str",e),e=e?.trim(),e=e||"{}";try{qt.records.options=JSON.parse(e)}catch(t){se.error("error while parsing gitGraph options",t.message)}},"setOptions"),JEt=s(function(){return qt.records.options},"getOptions"),e4t=s(function(e){let t=e.msg,r=e.id,n=e.type,i=e.tags;se.info("commit",t,r,n,i),se.debug("Entering commit:",t,r,n,i);let a=x0();r=Tt.sanitizeText(r,a),t=Tt.sanitizeText(t,a),i=i?.map(l=>Tt.sanitizeText(l,a));let o={id:r||qt.records.seq+"-"+CU(),message:t,seq:qt.records.seq++,type:n??Mn.NORMAL,tags:i??[],parents:qt.records.head==null?[]:[qt.records.head.id],branch:qt.records.currBranch};qt.records.head=o,se.info("main branch",a.mainBranchName),qt.records.commits.has(o.id)&&se.warn(`Commit ID ${o.id} already exists`),qt.records.commits.set(o.id,o),qt.records.branches.set(qt.records.currBranch,o.id),se.debug("in pushCommit "+o.id)},"commit"),t4t=s(function(e){let t=e.name,r=e.order;if(t=Tt.sanitizeText(t,x0()),qt.records.branches.has(t))throw new Error(`Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout ${t}")`);qt.records.branches.set(t,qt.records.head!=null?qt.records.head.id:null),qt.records.branchConfig.set(t,{name:t,order:r}),_7e(t),se.debug("in createBranch")},"branch"),r4t=s(e=>{let t=e.branch,r=e.id,n=e.type,i=e.tags,a=x0();t=Tt.sanitizeText(t,a),r&&(r=Tt.sanitizeText(r,a));let o=qt.records.branches.get(qt.records.currBranch),l=qt.records.branches.get(t),u=o?qt.records.commits.get(o):void 0,h=l?qt.records.commits.get(l):void 0;if(u&&h&&u.branch===t)throw new Error(`Cannot merge branch '${t}' into itself.`);if(qt.records.currBranch===t){let p=new Error('Incorrect usage of "merge". Cannot merge a branch to itself');throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:["branch abc"]},p}if(u===void 0||!u){let p=new Error(`Incorrect usage of "merge". Current branch (${qt.records.currBranch})has no commits`);throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:["commit"]},p}if(!qt.records.branches.has(t)){let p=new Error('Incorrect usage of "merge". Branch to be merged ('+t+") does not exist");throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:[`branch ${t}`]},p}if(h===void 0||!h){let p=new Error('Incorrect usage of "merge". Branch to be merged ('+t+") has no commits");throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:['"commit"']},p}if(u===h){let p=new Error('Incorrect usage of "merge". Both branches have same head');throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:["branch abc"]},p}if(r&&qt.records.commits.has(r)){let p=new Error('Incorrect usage of "merge". Commit with id:'+r+" already exists, use different custom id");throw p.hash={text:`merge ${t} ${r} ${n} ${i?.join(" ")}`,token:`merge ${t} ${r} ${n} ${i?.join(" ")}`,expected:[`merge ${t} ${r}_UNIQUE ${n} ${i?.join(" ")}`]},p}let d=l||"",f={id:r||`${qt.records.seq}-${CU()}`,message:`merged branch ${t} into ${qt.records.currBranch}`,seq:qt.records.seq++,parents:qt.records.head==null?[]:[qt.records.head.id,d],branch:qt.records.currBranch,type:Mn.MERGE,customType:n,customId:!!r,tags:i??[]};qt.records.head=f,qt.records.commits.set(f.id,f),qt.records.branches.set(qt.records.currBranch,f.id),se.debug(qt.records.branches),se.debug("in mergeBranch")},"merge"),n4t=s(function(e){let t=e.id,r=e.targetId,n=e.tags,i=e.parent;se.debug("Entering cherryPick:",t,r,n);let a=x0();if(t=Tt.sanitizeText(t,a),r=Tt.sanitizeText(r,a),n=n?.map(u=>Tt.sanitizeText(u,a)),i=Tt.sanitizeText(i,a),!t||!qt.records.commits.has(t)){let u=new Error('Incorrect usage of "cherryPick". Source commit id should exist and provided');throw u.hash={text:`cherryPick ${t} ${r}`,token:`cherryPick ${t} ${r}`,expected:["cherry-pick abc"]},u}let o=qt.records.commits.get(t);if(o===void 0||!o)throw new Error('Incorrect usage of "cherryPick". Source commit id should exist and provided');if(i&&!(Array.isArray(o.parents)&&o.parents.includes(i)))throw new Error("Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.");let l=o.branch;if(o.type===Mn.MERGE&&!i)throw new Error("Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.");if(!r||!qt.records.commits.has(r)){if(l===qt.records.currBranch){let f=new Error('Incorrect usage of "cherryPick". Source commit is already on current branch');throw f.hash={text:`cherryPick ${t} ${r}`,token:`cherryPick ${t} ${r}`,expected:["cherry-pick abc"]},f}let u=qt.records.branches.get(qt.records.currBranch);if(u===void 0||!u){let f=new Error(`Incorrect usage of "cherry-pick". Current branch (${qt.records.currBranch})has no commits`);throw f.hash={text:`cherryPick ${t} ${r}`,token:`cherryPick ${t} ${r}`,expected:["cherry-pick abc"]},f}let h=qt.records.commits.get(u);if(h===void 0||!h){let f=new Error(`Incorrect usage of "cherry-pick". Current branch (${qt.records.currBranch})has no commits`);throw f.hash={text:`cherryPick ${t} ${r}`,token:`cherryPick ${t} ${r}`,expected:["cherry-pick abc"]},f}let d={id:qt.records.seq+"-"+CU(),message:`cherry-picked ${o?.message} into ${qt.records.currBranch}`,seq:qt.records.seq++,parents:qt.records.head==null?[]:[qt.records.head.id,o.id],branch:qt.records.currBranch,type:Mn.CHERRY_PICK,tags:n?n.filter(Boolean):[`cherry-pick:${o.id}${o.type===Mn.MERGE?`|parent:${i}`:""}`]};qt.records.head=d,qt.records.commits.set(d.id,d),qt.records.branches.set(qt.records.currBranch,d.id),se.debug(qt.records.branches),se.debug("in cherryPick")}},"cherryPick"),_7e=s(function(e){if(e=Tt.sanitizeText(e,x0()),qt.records.branches.has(e)){qt.records.currBranch=e;let t=qt.records.branches.get(qt.records.currBranch);t===void 0||!t?qt.records.head=null:qt.records.head=qt.records.commits.get(t)??null}else{let t=new Error(`Trying to checkout branch which is not yet created. (Help try using "branch ${e}")`);throw t.hash={text:`checkout ${e}`,token:`checkout ${e}`,expected:[`branch ${e}`]},t}},"checkout");s(R7e,"upsert");s(L7e,"prettyPrintCommitHistory");i4t=s(function(){se.debug(qt.records.commits);let e=D7e()[0];L7e([e])},"prettyPrint"),a4t=s(function(){qt.reset(),Tr()},"clear"),s4t=s(function(){return[...qt.records.branchConfig.values()].map((t,r)=>t.order!==null&&t.order!==void 0?t:{...t,order:parseFloat(`0.${r}`)}).sort((t,r)=>(t.order??0)-(r.order??0)).map(({name:t})=>({name:t}))},"getBranchesAsObjArray"),o4t=s(function(){return qt.records.branches},"getBranches"),l4t=s(function(){return qt.records.commits},"getCommits"),D7e=s(function(){let e=[...qt.records.commits.values()];return e.forEach(function(t){se.debug(t.id)}),e.sort((t,r)=>t.seq-r.seq),e},"getCommitsArray"),c4t=s(function(){return qt.records.currBranch},"getCurrentBranch"),u4t=s(function(){return qt.records.direction},"getDirection"),h4t=s(function(){return qt.records.head},"getHead"),$_={commitType:Mn,getConfig:x0,setDirection:ZEt,setOptions:QEt,getOptions:JEt,commit:e4t,branch:t4t,merge:r4t,cherryPick:n4t,checkout:_7e,prettyPrint:i4t,clear:a4t,getBranchesAsObjArray:s4t,getBranches:o4t,getCommits:l4t,getCommitsArray:D7e,getCurrentBranch:c4t,getDirection:u4t,getHead:h4t,setAccTitle:Lr,getAccTitle:Ir,getAccDescription:Nr,setAccDescription:Mr,setDiagramTitle:Gr,getDiagramTitle:Pr}});var d4t,f4t,p4t,m4t,g4t,y4t,v4t,I7e,M7e=F(()=>{"use strict";ls();St();to();SU();O_();d4t=s((e,t)=>{Hn(e,t),e.dir&&t.setDirection(e.dir);for(let r of e.statements)f4t(r,t)},"populate"),f4t=s((e,t)=>{let n={Commit:s(i=>t.commit(p4t(i)),"Commit"),Branch:s(i=>t.branch(m4t(i)),"Branch"),Merge:s(i=>t.merge(g4t(i)),"Merge"),Checkout:s(i=>t.checkout(y4t(i)),"Checkout"),CherryPicking:s(i=>t.cherryPick(v4t(i)),"CherryPicking")}[e.$type];n?n(e):se.error(`Unknown statement type: ${e.$type}`)},"parseStatement"),p4t=s(e=>({id:e.id,msg:e.message??"",type:e.type!==void 0?Mn[e.type]:Mn.NORMAL,tags:e.tags??void 0}),"parseCommit"),m4t=s(e=>({name:e.name,order:e.order??0}),"parseBranch"),g4t=s(e=>({branch:e.branch,id:e.id??"",type:e.type!==void 0?Mn[e.type]:void 0,tags:e.tags??void 0}),"parseMerge"),y4t=s(e=>e.branch,"parseCheckout"),v4t=s(e=>({id:e.id,targetId:"",tags:e.tags?.length===0?void 0:e.tags,parent:e.parent}),"parseCherryPicking"),I7e={parse:s(async e=>{let t=await Li("gitGraph",e);se.debug(t),d4t(t,$_)},"parse")}});var kp,Ep,Hu,ld,b0,z_,wU,kU,x4t,T0,_o,Lo,F_,LS,G_,cd,sn,b4t,P7e,O7e,T4t,C4t,S4t,w4t,k4t,E4t,A4t,R4t,_4t,L4t,D4t,I4t,N7e,M4t,DS,N4t,P4t,O4t,B4t,$4t,B7e,$7e=F(()=>{"use strict";Wr();Jt();St();er();O_();kp=10,Ep=40,Hu=4,ld=2,b0=8,z_=new Set(["redux","redux-dark","redux-color","redux-dark-color"]),wU=12,kU=new Set(["redux-color","redux-dark-color"]),x4t=new Set(["dark","redux-dark","redux-dark-color","neo-dark"]),T0=s((e,t,r=!1)=>r&&e>0?(e-1)%(t-1)+1:e%t,"calcColorIndex"),_o=new Map,Lo=new Map,F_=30,LS=new Map,G_=[],cd=0,sn="LR",b4t=s(()=>{_o.clear(),Lo.clear(),LS.clear(),cd=0,G_=[],sn="LR"},"clear"),P7e=s(e=>{let t=document.createElementNS("http://www.w3.org/2000/svg","text");return(typeof e=="string"?e.split(/\\n|\n|/gi):e).forEach(n=>{let i=document.createElementNS("http://www.w3.org/2000/svg","tspan");i.setAttributeNS("http://www.w3.org/XML/1998/namespace","xml:space","preserve"),i.setAttribute("dy","1em"),i.setAttribute("x","0"),i.setAttribute("class","row"),i.textContent=n.trim(),t.appendChild(i)}),t},"drawText"),O7e=s(e=>{let t,r,n;return sn==="BT"?(r=s((i,a)=>i<=a,"comparisonFunc"),n=1/0):(r=s((i,a)=>i>=a,"comparisonFunc"),n=0),e.forEach(i=>{let a=sn==="TB"||sn=="BT"?Lo.get(i)?.y:Lo.get(i)?.x;a!==void 0&&r(a,n)&&(t=i,n=a)}),t},"findClosestParent"),T4t=s(e=>{let t="",r=1/0;return e.forEach(n=>{let i=Lo.get(n).y;i<=r&&(t=n,r=i)}),t||void 0},"findClosestParentBT"),C4t=s((e,t,r)=>{let n=r,i=r,a=[];e.forEach(o=>{let l=t.get(o);if(!l)throw new Error(`Commit not found for key ${o}`);l.parents.length?(n=w4t(l),i=Math.max(n,i)):a.push(l),k4t(l,n)}),n=i,a.forEach(o=>{E4t(o,n,r)}),e.forEach(o=>{let l=t.get(o);if(l?.parents.length){let u=T4t(l.parents);n=Lo.get(u).y-Ep,n<=i&&(i=n);let h=_o.get(l.branch).pos,d=n-kp;Lo.set(l.id,{x:h,y:d})}})},"setParallelBTPos"),S4t=s(e=>{let t=O7e(e.parents.filter(n=>n!==null));if(!t)throw new Error(`Closest parent not found for commit ${e.id}`);let r=Lo.get(t)?.y;if(r===void 0)throw new Error(`Closest parent position not found for commit ${e.id}`);return r},"findClosestParentPos"),w4t=s(e=>S4t(e)+Ep,"calculateCommitPosition"),k4t=s((e,t)=>{let r=_o.get(e.branch);if(!r)throw new Error(`Branch not found for commit ${e.id}`);let n=r.pos,i=t+kp;return Lo.set(e.id,{x:n,y:i}),{x:n,y:i}},"setCommitPosition"),E4t=s((e,t,r)=>{let n=_o.get(e.branch);if(!n)throw new Error(`Branch not found for commit ${e.id}`);let i=t+r,a=n.pos;Lo.set(e.id,{x:a,y:i})},"setRootPosition"),A4t=s((e,t,r,n,i,a)=>{let{theme:o}=Pe(),l=z_.has(o??""),u=kU.has(o??""),h=x4t.has(o??"");if(a===Mn.HIGHLIGHT)e.append("rect").attr("x",r.x-10+(l?3:0)).attr("y",r.y-10+(l?3:0)).attr("width",l?14:20).attr("height",l?14:20).attr("class",`commit ${t.id} commit-highlight${T0(i,b0,u)} ${n}-outer`),e.append("rect").attr("x",r.x-6+(l?2:0)).attr("y",r.y-6+(l?2:0)).attr("width",l?8:12).attr("height",l?8:12).attr("class",`commit ${t.id} commit${T0(i,b0,u)} ${n}-inner`);else if(a===Mn.CHERRY_PICK)e.append("circle").attr("cx",r.x).attr("cy",r.y).attr("r",l?7:10).attr("class",`commit ${t.id} ${n}`),e.append("circle").attr("cx",r.x-3).attr("cy",r.y+2).attr("r",l?2.5:2.75).attr("fill",h?"#000000":"#fff").attr("class",`commit ${t.id} ${n}`),e.append("circle").attr("cx",r.x+3).attr("cy",r.y+2).attr("r",l?2.5:2.75).attr("fill",h?"#000000":"#fff").attr("class",`commit ${t.id} ${n}`),e.append("line").attr("x1",r.x+3).attr("y1",r.y+1).attr("x2",r.x).attr("y2",r.y-5).attr("stroke",h?"#000000":"#fff").attr("class",`commit ${t.id} ${n}`),e.append("line").attr("x1",r.x-3).attr("y1",r.y+1).attr("x2",r.x).attr("y2",r.y-5).attr("stroke",h?"#000000":"#fff").attr("class",`commit ${t.id} ${n}`);else{let d=e.append("circle");if(d.attr("cx",r.x),d.attr("cy",r.y),d.attr("r",l?7:10),d.attr("class",`commit ${t.id} commit${T0(i,b0,u)}`),a===Mn.MERGE){let f=e.append("circle");f.attr("cx",r.x),f.attr("cy",r.y),f.attr("r",l?5:6),f.attr("class",`commit ${n} ${t.id} commit${T0(i,b0,u)}`)}if(a===Mn.REVERSE){let f=e.append("path"),p=l?4:5;f.attr("d",`M ${r.x-p},${r.y-p}L${r.x+p},${r.y+p}M${r.x-p},${r.y+p}L${r.x+p},${r.y-p}`).attr("class",`commit ${n} ${t.id} commit${T0(i,b0,u)}`)}}},"drawCommitBullet"),R4t=s((e,t,r,n,i)=>{if(t.type!==Mn.CHERRY_PICK&&(t.customId&&t.type===Mn.MERGE||t.type!==Mn.MERGE)&&i.showCommitLabel){let a=e.append("g"),o=a.insert("rect").attr("class","commit-label-bkg"),l=a.append("text").attr("x",n).attr("y",r.y+25).attr("class","commit-label").text(t.id),u=l.node()?.getBBox();if(u&&(o.attr("x",r.posWithOffset-u.width/2-ld).attr("y",r.y+13.5).attr("width",u.width+2*ld).attr("height",u.height+2*ld),sn==="TB"||sn==="BT"?(o.attr("x",r.x-(u.width+4*Hu+5)).attr("y",r.y-12),l.attr("x",r.x-(u.width+4*Hu)).attr("y",r.y+u.height-12)):l.attr("x",r.posWithOffset-u.width/2),i.rotateCommitLabel))if(sn==="TB"||sn==="BT")l.attr("transform","rotate(-45, "+r.x+", "+r.y+")"),o.attr("transform","rotate(-45, "+r.x+", "+r.y+")");else{let h=-7.5-(u.width+10)/25*9.5,d=10+u.width/25*8.5;a.attr("transform","translate("+h+", "+d+") rotate(-45, "+n+", "+r.y+")")}}},"drawCommitLabel"),_4t=s((e,t,r,n)=>{if(t.tags.length>0){let i=0,a=0,o=0,l=[];for(let u of t.tags.reverse()){let h=e.insert("polygon"),d=e.append("circle"),f=e.append("text").attr("y",r.y-16-i).attr("class","tag-label").text(u),p=f.node()?.getBBox();if(!p)throw new Error("Tag bbox not found");a=Math.max(a,p.width),o=Math.max(o,p.height),f.attr("x",r.posWithOffset-p.width/2),l.push({tag:f,hole:d,rect:h,yOffset:i}),i+=20}for(let{tag:u,hole:h,rect:d,yOffset:f}of l){let p=o/2,m=r.y-19.2-f;if(d.attr("class","tag-label-bkg").attr("points",` + ${n-a/2-Hu/2},${m+ld} + ${n-a/2-Hu/2},${m-ld} + ${r.posWithOffset-a/2-Hu},${m-p-ld} + ${r.posWithOffset+a/2+Hu},${m-p-ld} + ${r.posWithOffset+a/2+Hu},${m+p+ld} + ${r.posWithOffset-a/2-Hu},${m+p+ld}`),h.attr("cy",m).attr("cx",n-a/2+Hu/2).attr("r",1.5).attr("class","tag-hole"),sn==="TB"||sn==="BT"){let g=n+f;d.attr("class","tag-label-bkg").attr("points",` + ${r.x},${g+2} + ${r.x},${g-2} + ${r.x+kp},${g-p-2} + ${r.x+kp+a+4},${g-p-2} + ${r.x+kp+a+4},${g+p+2} + ${r.x+kp},${g+p+2}`).attr("transform","translate(12,12) rotate(45, "+r.x+","+n+")"),h.attr("cx",r.x+Hu/2).attr("cy",g).attr("transform","translate(12,12) rotate(45, "+r.x+","+n+")"),u.attr("x",r.x+5).attr("y",g+3).attr("transform","translate(14,14) rotate(45, "+r.x+","+n+")")}}}},"drawCommitTags"),L4t=s(e=>{switch(e.customType??e.type){case Mn.NORMAL:return"commit-normal";case Mn.REVERSE:return"commit-reverse";case Mn.HIGHLIGHT:return"commit-highlight";case Mn.MERGE:return"commit-merge";case Mn.CHERRY_PICK:return"commit-cherry-pick";default:return"commit-normal"}},"getCommitClassType"),D4t=s((e,t,r,n)=>{let i={x:0,y:0};if(e.parents.length>0){let a=O7e(e.parents);if(a){let o=n.get(a)??i;return t==="TB"?o.y+Ep:t==="BT"?(n.get(e.id)??i).y-Ep:o.x+Ep}}else return t==="TB"?F_:t==="BT"?(n.get(e.id)??i).y-Ep:0;return 0},"calculatePosition"),I4t=s((e,t,r)=>{let n=sn==="BT"&&r?t:t+kp,i=_o.get(e.branch)?.pos,a=sn==="TB"||sn==="BT"?_o.get(e.branch)?.pos:n;if(a===void 0||i===void 0)throw new Error(`Position were undefined for commit ${e.id}`);let o=z_.has(Pe().theme??""),l=sn==="TB"||sn==="BT"?n:i+(o?wU/2+1:-2);return{x:a,y:l,posWithOffset:n}},"getCommitPosition"),N7e=s((e,t,r,n)=>{let i=e.append("g").attr("class","commit-bullets"),a=e.append("g").attr("class","commit-labels"),o=sn==="TB"||sn==="BT"?F_:0,l=[...t.keys()],u=n.parallelCommits??!1,h=s((f,p)=>{let m=t.get(f)?.seq,g=t.get(p)?.seq;return m!==void 0&&g!==void 0?m-g:0},"sortKeys"),d=l.sort(h);sn==="BT"&&(u&&C4t(d,t,o),d=d.reverse()),d.forEach(f=>{let p=t.get(f);if(!p)throw new Error(`Commit not found for key ${f}`);u&&(o=D4t(p,sn,o,Lo));let m=I4t(p,o,u);if(r){let g=L4t(p),y=p.customType??p.type,v=_o.get(p.branch)?.index??0;A4t(i,p,m,g,v,y),R4t(a,p,m,o,n),_4t(a,p,m,o)}sn==="TB"||sn==="BT"?Lo.set(p.id,{x:m.x,y:m.posWithOffset}):Lo.set(p.id,{x:m.posWithOffset,y:m.y}),o=sn==="BT"&&u?o+Ep:o+Ep+kp,o>cd&&(cd=o)})},"drawCommits"),M4t=s((e,t,r,n,i)=>{let o=(sn==="TB"||sn==="BT"?r.xh.branch===o,"isOnBranchToGetCurve"),u=s(h=>h.seq>e.seq&&h.sequ(h)&&l(h))},"shouldRerouteArrow"),DS=s((e,t,r=0)=>{let n=e+Math.abs(e-t)/2;if(r>5)return n;if(G_.every(o=>Math.abs(o-n)>=10))return G_.push(n),n;let a=Math.abs(e-t);return DS(e,t-a/5,r+1)},"findLane"),N4t=s((e,t,r,n)=>{let{theme:i}=Pe(),a=kU.has(i??""),o=Lo.get(t.id),l=Lo.get(r.id);if(o===void 0||l===void 0)throw new Error(`Commit positions not found for commits ${t.id} and ${r.id}`);let u=M4t(t,r,o,l,n),h="",d="",f=0,p=0,m=_o.get(r.branch)?.index;r.type===Mn.MERGE&&t.id!==r.parents[0]&&(m=_o.get(t.branch)?.index);let g;if(u){h="A 10 10, 0, 0, 0,",d="A 10 10, 0, 0, 1,",f=10,p=10;let y=o.yl.x&&(h="A 20 20, 0, 0, 0,",d="A 20 20, 0, 0, 1,",f=20,p=20,r.type===Mn.MERGE&&t.id!==r.parents[0]?g=`M ${o.x} ${o.y} L ${o.x} ${l.y-f} ${d} ${o.x-p} ${l.y} L ${l.x} ${l.y}`:g=`M ${o.x} ${o.y} L ${l.x+f} ${o.y} ${h} ${l.x} ${o.y+p} L ${l.x} ${l.y}`),o.x===l.x&&(g=`M ${o.x} ${o.y} L ${l.x} ${l.y}`)):sn==="BT"?(o.xl.x&&(h="A 20 20, 0, 0, 0,",d="A 20 20, 0, 0, 1,",f=20,p=20,r.type===Mn.MERGE&&t.id!==r.parents[0]?g=`M ${o.x} ${o.y} L ${o.x} ${l.y+f} ${h} ${o.x-p} ${l.y} L ${l.x} ${l.y}`:g=`M ${o.x} ${o.y} L ${l.x+f} ${o.y} ${d} ${l.x} ${o.y-p} L ${l.x} ${l.y}`),o.x===l.x&&(g=`M ${o.x} ${o.y} L ${l.x} ${l.y}`)):(o.yl.y&&(r.type===Mn.MERGE&&t.id!==r.parents[0]?g=`M ${o.x} ${o.y} L ${l.x-f} ${o.y} ${h} ${l.x} ${o.y-p} L ${l.x} ${l.y}`:g=`M ${o.x} ${o.y} L ${o.x} ${l.y+f} ${d} ${o.x+p} ${l.y} L ${l.x} ${l.y}`),o.y===l.y&&(g=`M ${o.x} ${o.y} L ${l.x} ${l.y}`));if(g===void 0)throw new Error("Line definition not found");e.append("path").attr("d",g).attr("class","arrow arrow"+T0(m,b0,a))},"drawArrow"),P4t=s((e,t)=>{let r=e.append("g").attr("class","commit-arrows");[...t.keys()].forEach(n=>{let i=t.get(n);i.parents&&i.parents.length>0&&i.parents.forEach(a=>{N4t(r,t.get(a),i,t)})})},"drawArrows"),O4t=s((e,t,r,n)=>{let{look:i,theme:a,themeVariables:o}=Pe(),{dropShadow:l,THEME_COLOR_LIMIT:u}=o,h=z_.has(a??""),d=kU.has(a??""),f=e.append("g");t.forEach((p,m)=>{let g=T0(m,h?u:b0,d),y=_o.get(p.name)?.pos;if(y===void 0)throw new Error(`Position not found for branch ${p.name}`);let v=sn==="TB"||sn==="BT"?y:h?y+wU/2+1:y-2,x=f.append("line");x.attr("x1",0),x.attr("y1",v),x.attr("x2",cd),x.attr("y2",v),x.attr("class","branch branch"+g),sn==="TB"?(x.attr("y1",F_),x.attr("x1",y),x.attr("y2",cd),x.attr("x2",y)):sn==="BT"&&(x.attr("y1",cd),x.attr("x1",y),x.attr("y2",F_),x.attr("x2",y)),G_.push(v);let b=p.name,T=P7e(b),w=f.insert("rect"),S=f.insert("g").attr("class","branchLabel").insert("g").attr("class","label branch-label"+g);S.node().appendChild(T);let k=T.getBBox(),R=h?0:4,M=h?16:0,P=h?wU:0;i==="neo"&&w.attr("data-look","neo"),w.attr("class","branchLabelBkg label"+g).attr("style",i==="neo"?`filter:${h?`url(#${n}-drop-shadow)`:l}`:"").attr("rx",R).attr("ry",R).attr("x",-k.width-4-(r.rotateCommitLabel===!0?30:0)).attr("y",-k.height/2+10).attr("width",k.width+18+M).attr("height",k.height+4+P),S.attr("transform","translate("+(-k.width-14-(r.rotateCommitLabel===!0?30:0)+M/2)+", "+(v-k.height/2-2)+")"),sn==="TB"?(w.attr("x",y-k.width/2-10).attr("y",0),S.attr("transform","translate("+(y-k.width/2-5)+", 0)"),h&&(w.attr("transform",`translate(${-M/2-3}, ${-P-10})`),S.attr("transform","translate("+(y-k.width/2-5)+", "+(-P*2+7)+")"))):sn==="BT"?(w.attr("x",y-k.width/2-10).attr("y",cd),S.attr("transform","translate("+(y-k.width/2-5)+", "+cd+")"),h&&(w.attr("transform",`translate(${-M/2-3}, ${P+10})`),S.attr("transform","translate("+(y-k.width/2-5)+", "+(cd+P*2+4)+")"))):w.attr("transform","translate(-19, "+(v-12-P/2)+")")})},"drawBranches"),B4t=s(function(e,t,r,n,i){return _o.set(e,{pos:t,index:r}),t+=50+(i?40:0)+(sn==="TB"||sn==="BT"?n.width/2:0),t},"setBranchPosition"),$4t=s(function(e,t,r,n){b4t(),se.debug("in gitgraph renderer",e+` +`,"id:",t,r);let i=n.db;if(!i.getConfig){se.error("getConfig method is not available on db");return}let a=i.getConfig(),o=a.rotateCommitLabel??!1;LS=i.getCommits();let l=i.getBranchesAsObjArray();sn=i.getDirection();let u=ut(`[id="${t}"]`),{look:h,theme:d,themeVariables:f}=Pe(),{useGradient:p,gradientStart:m,gradientStop:g,filterColor:y}=f;if(p){let x=u.append("defs").append("linearGradient").attr("id",t+"-gradient").attr("gradientUnits","objectBoundingBox").attr("x1","0%").attr("y1","0%").attr("x2","100%").attr("y2","0%");x.append("stop").attr("offset","0%").attr("stop-color",m).attr("stop-opacity",1),x.append("stop").attr("offset","100%").attr("stop-color",g).attr("stop-opacity",1)}h==="neo"&&z_.has(d??"")&&u.append("defs").append("filter").attr("id",t+"-drop-shadow").attr("height","130%").attr("width","130%").append("feDropShadow").attr("dx","4").attr("dy","4").attr("stdDeviation",0).attr("flood-opacity","0.06").attr("flood-color",y);let v=0;l.forEach((x,b)=>{let T=P7e(x.name),w=u.append("g"),C=w.insert("g").attr("class","branchLabel"),S=C.insert("g").attr("class","label branch-label");S.node()?.appendChild(T);let k=T.getBBox();v=B4t(x.name,v,b,k,o),S.remove(),C.remove(),w.remove()}),N7e(u,LS,!1,a),a.showBranches&&O4t(u,l,a,t),P4t(u,LS),N7e(u,LS,!0,a),or.insertTitle(u,"gitTitleText",a.titleTopMargin??0,i.getDiagramTitle()),sb(void 0,u,a.diagramPadding,a.useMaxWidth)},"draw"),B7e={draw:$4t}});var F7e,G7e,F4t,G4t,z4t,V4t,W4t,q4t,H4t,U4t,z7e,V7e=F(()=>{"use strict";br();F7e=8,G7e=new Set(["redux","redux-dark","redux-color","redux-dark-color"]),F4t=new Set(["redux-color","redux-dark-color"]),G4t=new Set(["neo","neo-dark"]),z4t=new Set(["dark","redux-dark","redux-dark-color","neo-dark"]),V4t=new Set(["redux","redux-dark","redux-color","redux-dark-color","neo","neo-dark"]),W4t=s(e=>{let{svgId:t}=e,r="";if(e.useGradient&&t)for(let n=0;n{let t=Lt(),{theme:r,themeVariables:n}=t,{borderColorArray:i}=n,a=G7e.has(r);if(G4t.has(r)){let o="";for(let l=0;l`${Array.from({length:e.THEME_COLOR_LIMIT},(t,r)=>r).map(t=>{let r=t%F7e;return` + .branch-label${t} { fill: ${e["gitBranchLabel"+r]}; } + .commit${t} { stroke: ${e["git"+r]}; fill: ${e["git"+r]}; } + .commit-highlight${t} { stroke: ${e["gitInv"+r]}; fill: ${e["gitInv"+r]}; } + .label${t} { fill: ${e["git"+r]}; } + .arrow${t} { stroke: ${e["git"+r]}; } + `}).join(` +`)}`,"normalTheme"),U4t=s(e=>{let t=Lt(),{theme:r}=t,n=V4t.has(r);return` + .commit-id, + .commit-msg, + .branch-label { + fill: lightgrey; + color: lightgrey; + font-family: 'trebuchet ms', verdana, arial, sans-serif; + font-family: var(--mermaid-font-family); + } + + ${n?q4t(e):H4t(e)} + + .branch { + stroke-width: ${e.strokeWidth}; + stroke: ${e.commitLineColor??e.lineColor}; + stroke-dasharray: ${n?"4 2":"2"}; + } + .commit-label { font-size: ${e.commitLabelFontSize}; fill: ${n?e.nodeBorder:e.commitLabelColor}; ${n?`font-weight:${e.noteFontWeight};`:""}} + .commit-label-bkg { font-size: ${e.commitLabelFontSize}; fill: ${n?"transparent":e.commitLabelBackground}; opacity: ${n?"":.5}; } + .tag-label { font-size: ${e.tagLabelFontSize}; fill: ${e.tagLabelColor};} + .tag-label-bkg { fill: ${n?e.mainBkg:e.tagLabelBackground}; stroke: ${n?e.nodeBorder:e.tagLabelBorder}; ${n?`filter:${e.dropShadow}`:""} } + .tag-hole { fill: ${e.textColor}; } + + .commit-merge { + stroke: ${n?e.mainBkg:e.primaryColor}; + fill: ${n?e.mainBkg:e.primaryColor}; + } + .commit-reverse { + stroke: ${n?e.mainBkg:e.primaryColor}; + fill: ${n?e.mainBkg:e.primaryColor}; + stroke-width: ${n?e.strokeWidth:3}; + } + .commit-highlight-outer { + } + .commit-highlight-inner { + stroke: ${n?e.mainBkg:e.primaryColor}; + fill: ${n?e.mainBkg:e.primaryColor}; + } + + .arrow { + /* Intentional: neo themes keep the bold 8px arrow (like classic themes); only redux-geometry themes use the thinner options.strokeWidth. */ + stroke-width: ${G7e.has(r)?e.strokeWidth:8}; + stroke-linecap: round; + fill: none + } + .gitTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; + } +`},"getStyles"),z7e=U4t});var W7e={};sr(W7e,{diagram:()=>Y4t});var Y4t,q7e=F(()=>{"use strict";M7e();SU();$7e();V7e();Y4t={parser:I7e,db:$_,renderer:B7e,styles:z7e}});var EU,Y7e,j7e=F(()=>{"use strict";EU=(function(){var e=s(function(A,I,D,L){for(D=D||{},L=A.length;L--;D[A[L]]=I);return D},"o"),t=[6,8,10,12,13,14,15,16,17,18,20,21,22,23,24,25,26,27,28,29,30,31,33,35,36,38,40],r=[1,26],n=[1,27],i=[1,28],a=[1,29],o=[1,30],l=[1,31],u=[1,32],h=[1,33],d=[1,34],f=[1,9],p=[1,10],m=[1,11],g=[1,12],y=[1,13],v=[1,14],x=[1,15],b=[1,16],T=[1,19],w=[1,20],C=[1,21],S=[1,22],k=[1,23],R=[1,25],M=[1,35],P={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,gantt:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NL:10,weekday:11,weekday_monday:12,weekday_tuesday:13,weekday_wednesday:14,weekday_thursday:15,weekday_friday:16,weekday_saturday:17,weekday_sunday:18,weekend:19,weekend_friday:20,weekend_saturday:21,dateFormat:22,inclusiveEndDates:23,topAxis:24,axisFormat:25,tickInterval:26,excludes:27,includes:28,todayMarker:29,title:30,acc_title:31,acc_title_value:32,acc_descr:33,acc_descr_value:34,acc_descr_multiline_value:35,section:36,clickStatement:37,taskTxt:38,taskData:39,click:40,callbackname:41,callbackargs:42,href:43,clickStatementDebug:44,$accept:0,$end:1},terminals_:{2:"error",4:"gantt",6:"EOF",8:"SPACE",10:"NL",12:"weekday_monday",13:"weekday_tuesday",14:"weekday_wednesday",15:"weekday_thursday",16:"weekday_friday",17:"weekday_saturday",18:"weekday_sunday",20:"weekend_friday",21:"weekend_saturday",22:"dateFormat",23:"inclusiveEndDates",24:"topAxis",25:"axisFormat",26:"tickInterval",27:"excludes",28:"includes",29:"todayMarker",30:"title",31:"acc_title",32:"acc_title_value",33:"acc_descr",34:"acc_descr_value",35:"acc_descr_multiline_value",36:"section",38:"taskTxt",39:"taskData",40:"click",41:"callbackname",42:"callbackargs",43:"href"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[11,1],[11,1],[11,1],[11,1],[11,1],[11,1],[11,1],[19,1],[19,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,1],[9,2],[37,2],[37,3],[37,3],[37,4],[37,3],[37,4],[37,2],[44,2],[44,3],[44,3],[44,4],[44,3],[44,4],[44,2]],performAction:s(function(I,D,L,B,O,$,W){var V=$.length-1;switch(O){case 1:return $[V-1];case 2:this.$=[];break;case 3:$[V-1].push($[V]),this.$=$[V-1];break;case 4:case 5:this.$=$[V];break;case 6:case 7:this.$=[];break;case 8:B.setWeekday("monday");break;case 9:B.setWeekday("tuesday");break;case 10:B.setWeekday("wednesday");break;case 11:B.setWeekday("thursday");break;case 12:B.setWeekday("friday");break;case 13:B.setWeekday("saturday");break;case 14:B.setWeekday("sunday");break;case 15:B.setWeekend("friday");break;case 16:B.setWeekend("saturday");break;case 17:B.setDateFormat($[V].substr(11)),this.$=$[V].substr(11);break;case 18:B.enableInclusiveEndDates(),this.$=$[V].substr(18);break;case 19:B.TopAxis(),this.$=$[V].substr(8);break;case 20:B.setAxisFormat($[V].substr(11)),this.$=$[V].substr(11);break;case 21:B.setTickInterval($[V].substr(13)),this.$=$[V].substr(13);break;case 22:B.setExcludes($[V].substr(9)),this.$=$[V].substr(9);break;case 23:B.setIncludes($[V].substr(9)),this.$=$[V].substr(9);break;case 24:B.setTodayMarker($[V].substr(12)),this.$=$[V].substr(12);break;case 27:B.setDiagramTitle($[V].substr(6)),this.$=$[V].substr(6);break;case 28:this.$=$[V].trim(),B.setAccTitle(this.$);break;case 29:case 30:this.$=$[V].trim(),B.setAccDescription(this.$);break;case 31:B.addSection($[V].substr(8)),this.$=$[V].substr(8);break;case 33:B.addTask($[V-1],$[V]),this.$="task";break;case 34:this.$=$[V-1],B.setClickEvent($[V-1],$[V],null);break;case 35:this.$=$[V-2],B.setClickEvent($[V-2],$[V-1],$[V]);break;case 36:this.$=$[V-2],B.setClickEvent($[V-2],$[V-1],null),B.setLink($[V-2],$[V]);break;case 37:this.$=$[V-3],B.setClickEvent($[V-3],$[V-2],$[V-1]),B.setLink($[V-3],$[V]);break;case 38:this.$=$[V-2],B.setClickEvent($[V-2],$[V],null),B.setLink($[V-2],$[V-1]);break;case 39:this.$=$[V-3],B.setClickEvent($[V-3],$[V-1],$[V]),B.setLink($[V-3],$[V-2]);break;case 40:this.$=$[V-1],B.setLink($[V-1],$[V]);break;case 41:case 47:this.$=$[V-1]+" "+$[V];break;case 42:case 43:case 45:this.$=$[V-2]+" "+$[V-1]+" "+$[V];break;case 44:case 46:this.$=$[V-3]+" "+$[V-2]+" "+$[V-1]+" "+$[V];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},e(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:17,12:r,13:n,14:i,15:a,16:o,17:l,18:u,19:18,20:h,21:d,22:f,23:p,24:m,25:g,26:y,27:v,28:x,29:b,30:T,31:w,33:C,35:S,36:k,37:24,38:R,40:M},e(t,[2,7],{1:[2,1]}),e(t,[2,3]),{9:36,11:17,12:r,13:n,14:i,15:a,16:o,17:l,18:u,19:18,20:h,21:d,22:f,23:p,24:m,25:g,26:y,27:v,28:x,29:b,30:T,31:w,33:C,35:S,36:k,37:24,38:R,40:M},e(t,[2,5]),e(t,[2,6]),e(t,[2,17]),e(t,[2,18]),e(t,[2,19]),e(t,[2,20]),e(t,[2,21]),e(t,[2,22]),e(t,[2,23]),e(t,[2,24]),e(t,[2,25]),e(t,[2,26]),e(t,[2,27]),{32:[1,37]},{34:[1,38]},e(t,[2,30]),e(t,[2,31]),e(t,[2,32]),{39:[1,39]},e(t,[2,8]),e(t,[2,9]),e(t,[2,10]),e(t,[2,11]),e(t,[2,12]),e(t,[2,13]),e(t,[2,14]),e(t,[2,15]),e(t,[2,16]),{41:[1,40],43:[1,41]},e(t,[2,4]),e(t,[2,28]),e(t,[2,29]),e(t,[2,33]),e(t,[2,34],{42:[1,42],43:[1,43]}),e(t,[2,40],{41:[1,44]}),e(t,[2,35],{43:[1,45]}),e(t,[2,36]),e(t,[2,38],{42:[1,46]}),e(t,[2,37]),e(t,[2,39])],defaultActions:{},parseError:s(function(I,D){if(D.recoverable)this.trace(I);else{var L=new Error(I);throw L.hash=D,L}},"parseError"),parse:s(function(I){var D=this,L=[0],B=[],O=[null],$=[],W=this.table,V="",z=0,H=0,K=0,J=2,ne=1,Z=$.slice.call(arguments,1),pe=Object.create(this.lexer),te={yy:{}};for(var fe in this.yy)Object.prototype.hasOwnProperty.call(this.yy,fe)&&(te.yy[fe]=this.yy[fe]);pe.setInput(I,te.yy),te.yy.lexer=pe,te.yy.parser=this,typeof pe.yylloc>"u"&&(pe.yylloc={});var ce=pe.yylloc;$.push(ce);var de=pe.options&&pe.options.ranges;typeof te.yy.parseError=="function"?this.parseError=te.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Ee(oe){L.length=L.length-2*oe,O.length=O.length-oe,$.length=$.length-oe}s(Ee,"popStack");function we(){var oe;return oe=B.pop()||pe.lex()||ne,typeof oe!="number"&&(oe instanceof Array&&(B=oe,oe=B.pop()),oe=D.symbols_[oe]||oe),oe}s(we,"lex");for(var Fe,Ye,He,ye,Be,Ie,Ne={},re,ue,le,he;;){if(He=L[L.length-1],this.defaultActions[He]?ye=this.defaultActions[He]:((Fe===null||typeof Fe>"u")&&(Fe=we()),ye=W[He]&&W[He][Fe]),typeof ye>"u"||!ye.length||!ye[0]){var Te="";he=[];for(re in W[He])this.terminals_[re]&&re>J&&he.push("'"+this.terminals_[re]+"'");pe.showPosition?Te="Parse error on line "+(z+1)+`: +`+pe.showPosition()+` +Expecting `+he.join(", ")+", got '"+(this.terminals_[Fe]||Fe)+"'":Te="Parse error on line "+(z+1)+": Unexpected "+(Fe==ne?"end of input":"'"+(this.terminals_[Fe]||Fe)+"'"),this.parseError(Te,{text:pe.match,token:this.terminals_[Fe]||Fe,line:pe.yylineno,loc:ce,expected:he})}if(ye[0]instanceof Array&&ye.length>1)throw new Error("Parse Error: multiple actions possible at state: "+He+", token: "+Fe);switch(ye[0]){case 1:L.push(Fe),O.push(pe.yytext),$.push(pe.yylloc),L.push(ye[1]),Fe=null,Ye?(Fe=Ye,Ye=null):(H=pe.yyleng,V=pe.yytext,z=pe.yylineno,ce=pe.yylloc,K>0&&K--);break;case 2:if(ue=this.productions_[ye[1]][1],Ne.$=O[O.length-ue],Ne._$={first_line:$[$.length-(ue||1)].first_line,last_line:$[$.length-1].last_line,first_column:$[$.length-(ue||1)].first_column,last_column:$[$.length-1].last_column},de&&(Ne._$.range=[$[$.length-(ue||1)].range[0],$[$.length-1].range[1]]),Ie=this.performAction.apply(Ne,[V,H,z,te.yy,ye[1],O,$].concat(Z)),typeof Ie<"u")return Ie;ue&&(L=L.slice(0,-1*ue*2),O=O.slice(0,-1*ue),$=$.slice(0,-1*ue)),L.push(this.productions_[ye[1]][0]),O.push(Ne.$),$.push(Ne._$),le=W[L[L.length-2]][L[L.length-1]],L.push(le);break;case 3:return!0}}return!0},"parse")},N=(function(){var A={EOF:1,parseError:s(function(D,L){if(this.yy.parser)this.yy.parser.parseError(D,L);else throw new Error(D)},"parseError"),setInput:s(function(I,D){return this.yy=D||this.yy||{},this._input=I,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var I=this._input[0];this.yytext+=I,this.yyleng++,this.offset++,this.match+=I,this.matched+=I;var D=I.match(/(?:\r\n?|\n).*/g);return D?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),I},"input"),unput:s(function(I){var D=I.length,L=I.split(/(?:\r\n?|\n)/g);this._input=I+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-D),this.offset-=D;var B=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),L.length-1&&(this.yylineno-=L.length-1);var O=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:L?(L.length===B.length?this.yylloc.first_column:0)+B[B.length-L.length].length-L[0].length:this.yylloc.first_column-D},this.options.ranges&&(this.yylloc.range=[O[0],O[0]+this.yyleng-D]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(I){this.unput(this.match.slice(I))},"less"),pastInput:s(function(){var I=this.matched.substr(0,this.matched.length-this.match.length);return(I.length>20?"...":"")+I.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var I=this.match;return I.length<20&&(I+=this._input.substr(0,20-I.length)),(I.substr(0,20)+(I.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var I=this.pastInput(),D=new Array(I.length+1).join("-");return I+this.upcomingInput()+` +`+D+"^"},"showPosition"),test_match:s(function(I,D){var L,B,O;if(this.options.backtrack_lexer&&(O={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(O.yylloc.range=this.yylloc.range.slice(0))),B=I[0].match(/(?:\r\n?|\n).*/g),B&&(this.yylineno+=B.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:B?B[B.length-1].length-B[B.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+I[0].length},this.yytext+=I[0],this.match+=I[0],this.matches=I,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(I[0].length),this.matched+=I[0],L=this.performAction.call(this,this.yy,this,D,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),L)return L;if(this._backtrack){for(var $ in O)this[$]=O[$];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var I,D,L,B;this._more||(this.yytext="",this.match="");for(var O=this._currentRules(),$=0;$D[0].length)){if(D=L,B=$,this.options.backtrack_lexer){if(I=this.test_match(L,O[$]),I!==!1)return I;if(this._backtrack){D=!1;continue}else return!1}else if(!this.options.flex)break}return D?(I=this.test_match(D,O[B]),I!==!1?I:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var D=this.next();return D||this.lex()},"lex"),begin:s(function(D){this.conditionStack.push(D)},"begin"),popState:s(function(){var D=this.conditionStack.length-1;return D>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(D){return D=this.conditionStack.length-1-Math.abs(D||0),D>=0?this.conditionStack[D]:"INITIAL"},"topState"),pushState:s(function(D){this.begin(D)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(D,L,B,O){var $=O;switch(B){case 0:return this.begin("open_directive"),"open_directive";break;case 1:return this.begin("acc_title"),31;break;case 2:return this.popState(),"acc_title_value";break;case 3:return this.begin("acc_descr"),33;break;case 4:return this.popState(),"acc_descr_value";break;case 5:this.begin("acc_descr_multiline");break;case 6:this.popState();break;case 7:return"acc_descr_multiline_value";case 8:break;case 9:break;case 10:break;case 11:return 10;case 12:break;case 13:break;case 14:this.begin("href");break;case 15:this.popState();break;case 16:return 43;case 17:this.begin("callbackname");break;case 18:this.popState();break;case 19:this.popState(),this.begin("callbackargs");break;case 20:return 41;case 21:this.popState();break;case 22:return 42;case 23:this.begin("click");break;case 24:this.popState();break;case 25:return 40;case 26:return 4;case 27:return 22;case 28:return 23;case 29:return 24;case 30:return 25;case 31:return 26;case 32:return 28;case 33:return 27;case 34:return 29;case 35:return 12;case 36:return 13;case 37:return 14;case 38:return 15;case 39:return 16;case 40:return 17;case 41:return 18;case 42:return 20;case 43:return 21;case 44:return"date";case 45:return 30;case 46:return"accDescription";case 47:return 36;case 48:return 38;case 49:return 39;case 50:return":";case 51:return 6;case 52:return"INVALID"}},"anonymous"),rules:[/^(?:%%\{)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:%%(?!\{)*[^\n]*)/i,/^(?:[^\}]%%*[^\n]*)/i,/^(?:%%*[^\n]*[\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:%[^\n]*)/i,/^(?:href[\s]+["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:call[\s]+)/i,/^(?:\([\s]*\))/i,/^(?:\()/i,/^(?:[^(]*)/i,/^(?:\))/i,/^(?:[^)]*)/i,/^(?:click[\s]+)/i,/^(?:[\s\n])/i,/^(?:[^\s\n]*)/i,/^(?:gantt\b)/i,/^(?:dateFormat\s[^#\n;]+)/i,/^(?:inclusiveEndDates\b)/i,/^(?:topAxis\b)/i,/^(?:axisFormat\s[^#\n;]+)/i,/^(?:tickInterval\s[^#\n;]+)/i,/^(?:includes\s[^#\n;]+)/i,/^(?:excludes\s[^#\n;]+)/i,/^(?:todayMarker\s[^\n;]+)/i,/^(?:weekday\s+monday\b)/i,/^(?:weekday\s+tuesday\b)/i,/^(?:weekday\s+wednesday\b)/i,/^(?:weekday\s+thursday\b)/i,/^(?:weekday\s+friday\b)/i,/^(?:weekday\s+saturday\b)/i,/^(?:weekday\s+sunday\b)/i,/^(?:weekend\s+friday\b)/i,/^(?:weekend\s+saturday\b)/i,/^(?:\d\d\d\d-\d\d-\d\d\b)/i,/^(?:title\s[^\n]+)/i,/^(?:accDescription\s[^#\n;]+)/i,/^(?:section\s[^\n]+)/i,/^(?:[^:\n]+)/i,/^(?::[^#\n;]+)/i,/^(?::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{acc_descr_multiline:{rules:[6,7],inclusive:!1},acc_descr:{rules:[4],inclusive:!1},acc_title:{rules:[2],inclusive:!1},callbackargs:{rules:[21,22],inclusive:!1},callbackname:{rules:[18,19,20],inclusive:!1},href:{rules:[15,16],inclusive:!1},click:{rules:[24,25],inclusive:!1},INITIAL:{rules:[0,1,3,5,8,9,10,11,12,13,14,17,23,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52],inclusive:!0}}};return A})();P.lexer=N;function _(){this.yy={}}return s(_,"Parser"),_.prototype=P,P.Parser=_,new _})();EU.parser=EU;Y7e=EU});var X7e=As((AU,RU)=>{"use strict";(function(e,t){typeof AU=="object"&&typeof RU<"u"?RU.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self).dayjs_plugin_isoWeek=t()})(AU,(function(){"use strict";var e="day";return function(t,r,n){var i=s(function(l){return l.add(4-l.isoWeekday(),e)},"a"),a=r.prototype;a.isoWeekYear=function(){return i(this).year()},a.isoWeek=function(l){if(!this.$utils().u(l))return this.add(7*(l-this.isoWeek()),e);var u,h,d,f,p=i(this),m=(u=this.isoWeekYear(),h=this.$u,d=(h?n.utc:n)().year(u).startOf("year"),f=4-d.isoWeekday(),d.isoWeekday()>4&&(f+=7),d.add(f,e));return p.diff(m,"week")+1},a.isoWeekday=function(l){return this.$utils().u(l)?this.day()||7:this.day(this.day()%7?l:l-7)};var o=a.startOf;a.startOf=function(l,u){var h=this.$utils(),d=!!h.u(u)||u;return h.p(l)==="isoweek"?d?this.date(this.date()-(this.isoWeekday()-1)).startOf("day"):this.date(this.date()-1-(this.isoWeekday()-1)+7).endOf("day"):o.bind(this)(l,u)}}}))});var K7e=As((_U,LU)=>{"use strict";(function(e,t){typeof _U=="object"&&typeof LU<"u"?LU.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self).dayjs_plugin_customParseFormat=t()})(_U,(function(){"use strict";var e={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},t=/(\[[^[]*\])|([-_:/.,()\s]+)|(A|a|Q|YYYY|YY?|ww?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g,r=/\d/,n=/\d\d/,i=/\d\d?/,a=/\d*[^-_:/,()\s\d]+/,o={},l=s(function(g){return(g=+g)+(g>68?1900:2e3)},"a"),u=s(function(g){return function(y){this[g]=+y}},"f"),h=[/[+-]\d\d:?(\d\d)?|Z/,function(g){(this.zone||(this.zone={})).offset=(function(y){if(!y||y==="Z")return 0;var v=y.match(/([+-]|\d\d)/g),x=60*v[1]+(+v[2]||0);return x===0?0:v[0]==="+"?-x:x})(g)}],d=s(function(g){var y=o[g];return y&&(y.indexOf?y:y.s.concat(y.f))},"u"),f=s(function(g,y){var v,x=o.meridiem;if(x){for(var b=1;b<=24;b+=1)if(g.indexOf(x(b,0,y))>-1){v=b>12;break}}else v=g===(y?"pm":"PM");return v},"d"),p={A:[a,function(g){this.afternoon=f(g,!1)}],a:[a,function(g){this.afternoon=f(g,!0)}],Q:[r,function(g){this.month=3*(g-1)+1}],S:[r,function(g){this.milliseconds=100*+g}],SS:[n,function(g){this.milliseconds=10*+g}],SSS:[/\d{3}/,function(g){this.milliseconds=+g}],s:[i,u("seconds")],ss:[i,u("seconds")],m:[i,u("minutes")],mm:[i,u("minutes")],H:[i,u("hours")],h:[i,u("hours")],HH:[i,u("hours")],hh:[i,u("hours")],D:[i,u("day")],DD:[n,u("day")],Do:[a,function(g){var y=o.ordinal,v=g.match(/\d+/);if(this.day=v[0],y)for(var x=1;x<=31;x+=1)y(x).replace(/\[|\]/g,"")===g&&(this.day=x)}],w:[i,u("week")],ww:[n,u("week")],M:[i,u("month")],MM:[n,u("month")],MMM:[a,function(g){var y=d("months"),v=(d("monthsShort")||y.map((function(x){return x.slice(0,3)}))).indexOf(g)+1;if(v<1)throw new Error;this.month=v%12||v}],MMMM:[a,function(g){var y=d("months").indexOf(g)+1;if(y<1)throw new Error;this.month=y%12||y}],Y:[/[+-]?\d+/,u("year")],YY:[n,function(g){this.year=l(g)}],YYYY:[/\d{4}/,u("year")],Z:h,ZZ:h};function m(g){var y,v;y=g,v=o&&o.formats;for(var x=(g=y.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,(function(R,M,P){var N=P&&P.toUpperCase();return M||v[P]||e[P]||v[N].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,(function(_,A,I){return A||I.slice(1)}))}))).match(t),b=x.length,T=0;T-1)return new Date((L==="X"?1e3:1)*D);var $=m(L)(D),W=$.year,V=$.month,z=$.day,H=$.hours,K=$.minutes,J=$.seconds,ne=$.milliseconds,Z=$.zone,pe=$.week,te=new Date,fe=z||(W||V?1:te.getDate()),ce=W||te.getFullYear(),de=0;W&&!V||(de=V>0?V-1:te.getMonth());var Ee,we=H||0,Fe=K||0,Ye=J||0,He=ne||0;return Z?new Date(Date.UTC(ce,de,fe,we,Fe,Ye,He+60*Z.offset*1e3)):B?new Date(Date.UTC(ce,de,fe,we,Fe,Ye,He)):(Ee=new Date(ce,de,fe,we,Fe,Ye,He),pe&&(Ee=O(Ee).week(pe).toDate()),Ee)}catch{return new Date("")}})(w,k,C,v),this.init(),N&&N!==!0&&(this.$L=this.locale(N).$L),P&&w!=this.format(k)&&(this.$d=new Date("")),o={}}else if(k instanceof Array)for(var _=k.length,A=1;A<=_;A+=1){S[1]=k[A-1];var I=v.apply(this,S);if(I.isValid()){this.$d=I.$d,this.$L=I.$L,this.init();break}A===_&&(this.$d=new Date(""))}else b.call(this,T)}}}))});var Z7e=As((DU,IU)=>{"use strict";(function(e,t){typeof DU=="object"&&typeof IU<"u"?IU.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self).dayjs_plugin_advancedFormat=t()})(DU,(function(){"use strict";return function(e,t){var r=t.prototype,n=r.format;r.format=function(i){var a=this,o=this.$locale();if(!this.isValid())return n.bind(this)(i);var l=this.$utils(),u=(i||"YYYY-MM-DDTHH:mm:ssZ").replace(/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g,(function(h){switch(h){case"Q":return Math.ceil((a.$M+1)/3);case"Do":return o.ordinal(a.$D);case"gggg":return a.weekYear();case"GGGG":return a.isoWeekYear();case"wo":return o.ordinal(a.week(),"W");case"w":case"ww":return l.s(a.week(),h==="w"?1:2,"0");case"W":case"WW":return l.s(a.isoWeek(),h==="W"?1:2,"0");case"k":case"kk":return l.s(String(a.$H===0?24:a.$H),h==="k"?1:2,"0");case"X":return Math.floor(a.$d.getTime()/1e3);case"x":return a.$d.getTime();case"z":return"["+a.offsetName()+"]";case"zzz":return"["+a.offsetName("long")+"]";default:return h}}));return n.bind(this)(u)}}}))});function pIe(e,t,r){let n=!0;for(;n;)n=!1,r.forEach(function(i){let a="^\\s*"+i+"\\s*$",o=new RegExp(a);e[0].match(o)&&(t[i]=!0,e.shift(1),n=!0)})}var eIe,sl,tIe,rIe,nIe,Q7e,Uu,OU,BU,$U,Hv,Uv,FU,GU,q_,Yv,zU,iIe,VU,Wv,IS,WU,qU,H_,MU,Z4t,Q4t,J4t,e3t,t3t,r3t,n3t,i3t,a3t,s3t,o3t,l3t,c3t,u3t,h3t,d3t,aIe,f3t,p3t,m3t,g3t,y3t,v3t,x3t,b3t,sIe,T3t,C3t,S3t,oIe,w3t,NU,lIe,cIe,V_,qv,k3t,E3t,PU,W_,Ta,uIe,A3t,C0,R3t,J7e,_3t,hIe,L3t,dIe,D3t,I3t,fIe,mIe=F(()=>{"use strict";eIe=Rs(ub(),1),sl=Rs(mw(),1),tIe=Rs(X7e(),1),rIe=Rs(K7e(),1),nIe=Rs(Z7e(),1);St();Jt();er();$n();sl.default.extend(tIe.default);sl.default.extend(rIe.default);sl.default.extend(nIe.default);Q7e={friday:5,saturday:6},Uu="",OU="",$U="",Hv=[],Uv=[],FU=new Map,GU=[],q_=[],Yv="",zU="",iIe=["active","done","crit","milestone","vert"],VU=[],Wv="",IS=!1,WU=!1,qU="sunday",H_="saturday",MU=0,Z4t=s(function(){GU=[],q_=[],Yv="",VU=[],V_=0,PU=void 0,W_=void 0,Ta=[],Uu="",OU="",zU="",BU=void 0,$U="",Hv=[],Uv=[],IS=!1,WU=!1,MU=0,FU=new Map,Wv="",Tr(),qU="sunday",H_="saturday"},"clear"),Q4t=s(function(e){Wv=e},"setDiagramId"),J4t=s(function(e){OU=e},"setAxisFormat"),e3t=s(function(){return OU},"getAxisFormat"),t3t=s(function(e){BU=e},"setTickInterval"),r3t=s(function(){return BU},"getTickInterval"),n3t=s(function(e){$U=e},"setTodayMarker"),i3t=s(function(){return $U},"getTodayMarker"),a3t=s(function(e){Uu=e},"setDateFormat"),s3t=s(function(){IS=!0},"enableInclusiveEndDates"),o3t=s(function(){return IS},"endDatesAreInclusive"),l3t=s(function(){WU=!0},"enableTopAxis"),c3t=s(function(){return WU},"topAxisEnabled"),u3t=s(function(e){zU=e},"setDisplayMode"),h3t=s(function(){return zU},"getDisplayMode"),d3t=s(function(){return Uu},"getDateFormat"),aIe=s((e,t)=>{let r=t.toLowerCase().split(/[\s,]+/).filter(n=>n!=="");return[...new Set([...e,...r])]},"mergeTokens"),f3t=s(function(e){Hv=aIe(Hv,e)},"setIncludes"),p3t=s(function(){return Hv},"getIncludes"),m3t=s(function(e){Uv=aIe(Uv,e)},"setExcludes"),g3t=s(function(){return Uv},"getExcludes"),y3t=s(function(){return FU},"getLinks"),v3t=s(function(e){Yv=e,GU.push(e)},"addSection"),x3t=s(function(){return GU},"getSections"),b3t=s(function(){let e=J7e(),t=10,r=0;for(;!e&&rl))throw new Error("Failed to find a valid date that was not excluded by `excludes` after 10,000 iterations.");e=e.add(1,"d")}return[t,o]},"fixTaskDates"),NU=s(function(e,t,r){if(r=r.trim(),s(l=>{let u=l.trim();return u==="x"||u==="X"},"isTimestampFormat")(t)&&/^\d+$/.test(r))return new Date(Number(r));let a=/^after\s+(?[\d\w- ]+)/.exec(r);if(a!==null){let l=null;for(let h of a.groups.ids.split(" ")){let d=C0(h);d!==void 0&&(!l||d.endTime>l.endTime)&&(l=d)}if(l)return l.endTime;let u=new Date;return u.setHours(0,0,0,0),u}let o=(0,sl.default)(r,t.trim(),!0);if(o.isValid())return o.toDate();{se.debug("Invalid date:"+r),se.debug("With date format:"+t.trim());let l=new Date(r);if(l===void 0||isNaN(l.getTime())||l.getFullYear()<-1e4||l.getFullYear()>1e4)throw new Error("Invalid date:"+r);return l}},"getStartDate"),lIe=s(function(e){let t=/^(\d+(?:\.\d+)?)([Mdhmswy]|ms)$/.exec(e.trim());return t!==null?[Number.parseFloat(t[1]),t[2]]:[NaN,"ms"]},"parseDuration"),cIe=s(function(e,t,r,n=!1){r=r.trim();let a=/^until\s+(?[\d\w- ]+)/.exec(r);if(a!==null){let d=null;for(let p of a.groups.ids.split(" ")){let m=C0(p);m!==void 0&&(!d||m.startTime{window.open(r,"_self")}),FU.set(n,r))}),hIe(e,"clickable")},"setLink"),hIe=s(function(e,t){e.split(",").forEach(function(r){let n=C0(r);n!==void 0&&n.classes.push(t)})},"setClass"),L3t=s(function(e,t,r){if(Pe().securityLevel!=="loose"||t===void 0)return;let n=[];if(typeof r=="string"){n=r.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);for(let a=0;a{or.runFunc(t,...n)})},"setClickFun"),dIe=s(function(e,t){VU.push(function(){let r=Wv?`${Wv}-${e}`:e,n=document.querySelector(`[id="${r}"]`);n!==null&&n.addEventListener("click",function(){t()})},function(){let r=Wv?`${Wv}-${e}`:e,n=document.querySelector(`[id="${r}-text"]`);n!==null&&n.addEventListener("click",function(){t()})})},"pushFun"),D3t=s(function(e,t,r){e.split(",").forEach(function(n){L3t(n,t,r)}),hIe(e,"clickable")},"setClickEvent"),I3t=s(function(e){VU.forEach(function(t){t(e)})},"bindFunctions"),fIe={getConfig:s(()=>Pe().gantt,"getConfig"),clear:Z4t,setDateFormat:a3t,getDateFormat:d3t,enableInclusiveEndDates:s3t,endDatesAreInclusive:o3t,enableTopAxis:l3t,topAxisEnabled:c3t,setAxisFormat:J4t,getAxisFormat:e3t,setTickInterval:t3t,getTickInterval:r3t,setTodayMarker:n3t,getTodayMarker:i3t,setAccTitle:Lr,getAccTitle:Ir,setDiagramTitle:Gr,getDiagramTitle:Pr,setDiagramId:Q4t,setDisplayMode:u3t,getDisplayMode:h3t,setAccDescription:Mr,getAccDescription:Nr,addSection:v3t,getSections:x3t,getTasks:b3t,addTask:A3t,findTaskById:C0,addTaskOrg:R3t,setIncludes:f3t,getIncludes:p3t,setExcludes:m3t,getExcludes:g3t,setClickEvent:D3t,setLink:_3t,getLinks:y3t,bindFunctions:I3t,parseDuration:lIe,isInvalidDate:sIe,setWeekday:T3t,getWeekday:C3t,setWeekend:S3t};s(pIe,"getTaskTags")});var gIe=As((HU,UU)=>{"use strict";(function(e,t){typeof HU=="object"&&typeof UU<"u"?UU.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self).dayjs_plugin_duration=t()})(HU,(function(){"use strict";var e,t,r=1e3,n=6e4,i=36e5,a=864e5,o=31536e6,l=2628e6,u=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/,h=/\[([^\]]+)]|YYYY|YY|Y|M{1,2}|D{1,2}|H{1,2}|m{1,2}|s{1,2}|SSS/g,d={years:o,months:l,days:a,hours:i,minutes:n,seconds:r,milliseconds:1,weeks:6048e5},f=s(function(w){return w instanceof b},"c"),p=s(function(w,C,S){return new b(w,S,C.$l)},"f"),m=s(function(w){return t.p(w)+"s"},"m"),g=s(function(w){return w<0},"l"),y=s(function(w){return g(w)?Math.ceil(w):Math.floor(w)},"$"),v=s(function(w){return Math.abs(w)},"y"),x=s(function(w,C){return w?g(w)?{negative:!0,format:""+v(w)+C}:{negative:!1,format:""+w+C}:{negative:!1,format:""}},"v"),b=(function(){function w(S,k,R){var M=this;if(this.$d={},this.$l=R,S===void 0&&(this.$ms=0,this.parseFromMilliseconds()),k)return p(S*d[m(k)],this);if(typeof S=="number")return this.$ms=S,this.parseFromMilliseconds(),this;if(typeof S=="object")return Object.keys(S).forEach((function(_){M.$d[m(_)]=S[_]})),this.calMilliseconds(),this;if(typeof S=="string"){var P=S.match(u);if(P){var N=P.slice(2).map((function(_){return _!=null?Number(_):0}));return this.$d.years=N[0],this.$d.months=N[1],this.$d.weeks=N[2],this.$d.days=N[3],this.$d.hours=N[4],this.$d.minutes=N[5],this.$d.seconds=N[6],this.calMilliseconds(),this}}return this}s(w,"l");var C=w.prototype;return C.calMilliseconds=function(){var S=this;this.$ms=Object.keys(this.$d).reduce((function(k,R){return k+(S.$d[R]||0)*d[R]}),0)},C.parseFromMilliseconds=function(){var S=this.$ms;this.$d.years=y(S/o),S%=o,this.$d.months=y(S/l),S%=l,this.$d.days=y(S/a),S%=a,this.$d.hours=y(S/i),S%=i,this.$d.minutes=y(S/n),S%=n,this.$d.seconds=y(S/r),S%=r,this.$d.milliseconds=S},C.toISOString=function(){var S=x(this.$d.years,"Y"),k=x(this.$d.months,"M"),R=+this.$d.days||0;this.$d.weeks&&(R+=7*this.$d.weeks);var M=x(R,"D"),P=x(this.$d.hours,"H"),N=x(this.$d.minutes,"M"),_=this.$d.seconds||0;this.$d.milliseconds&&(_+=this.$d.milliseconds/1e3,_=Math.round(1e3*_)/1e3);var A=x(_,"S"),I=S.negative||k.negative||M.negative||P.negative||N.negative||A.negative,D=P.format||N.format||A.format?"T":"",L=(I?"-":"")+"P"+S.format+k.format+M.format+D+P.format+N.format+A.format;return L==="P"||L==="-P"?"P0D":L},C.toJSON=function(){return this.toISOString()},C.format=function(S){var k=S||"YYYY-MM-DDTHH:mm:ss",R={Y:this.$d.years,YY:t.s(this.$d.years,2,"0"),YYYY:t.s(this.$d.years,4,"0"),M:this.$d.months,MM:t.s(this.$d.months,2,"0"),D:this.$d.days,DD:t.s(this.$d.days,2,"0"),H:this.$d.hours,HH:t.s(this.$d.hours,2,"0"),m:this.$d.minutes,mm:t.s(this.$d.minutes,2,"0"),s:this.$d.seconds,ss:t.s(this.$d.seconds,2,"0"),SSS:t.s(this.$d.milliseconds,3,"0")};return k.replace(h,(function(M,P){return P||String(R[M])}))},C.as=function(S){return this.$ms/d[m(S)]},C.get=function(S){var k=this.$ms,R=m(S);return R==="milliseconds"?k%=1e3:k=R==="weeks"?y(k/d[R]):this.$d[R],k||0},C.add=function(S,k,R){var M;return M=k?S*d[m(k)]:f(S)?S.$ms:p(S,this).$ms,p(this.$ms+M*(R?-1:1),this)},C.subtract=function(S,k){return this.add(S,k,!0)},C.locale=function(S){var k=this.clone();return k.$l=S,k},C.clone=function(){return p(this.$ms,this)},C.humanize=function(S){return e().add(this.$ms,"ms").locale(this.$l).fromNow(!S)},C.valueOf=function(){return this.asMilliseconds()},C.milliseconds=function(){return this.get("milliseconds")},C.asMilliseconds=function(){return this.as("milliseconds")},C.seconds=function(){return this.get("seconds")},C.asSeconds=function(){return this.as("seconds")},C.minutes=function(){return this.get("minutes")},C.asMinutes=function(){return this.as("minutes")},C.hours=function(){return this.get("hours")},C.asHours=function(){return this.as("hours")},C.days=function(){return this.get("days")},C.asDays=function(){return this.as("days")},C.weeks=function(){return this.get("weeks")},C.asWeeks=function(){return this.as("weeks")},C.months=function(){return this.get("months")},C.asMonths=function(){return this.as("months")},C.years=function(){return this.get("years")},C.asYears=function(){return this.as("years")},w})(),T=s(function(w,C,S){return w.add(C.years()*S,"y").add(C.months()*S,"M").add(C.days()*S,"d").add(C.hours()*S,"h").add(C.minutes()*S,"m").add(C.seconds()*S,"s").add(C.milliseconds()*S,"ms")},"p");return function(w,C,S){e=S,t=S().$utils(),S.duration=function(M,P){var N=S.locale();return p(M,{$l:N},P)},S.isDuration=f;var k=C.prototype.add,R=C.prototype.subtract;C.prototype.add=function(M,P){return f(M)?T(this,M,1):k.bind(this)(M,P)},C.prototype.subtract=function(M,P){return f(M)?T(this,M,-1):R.bind(this)(M,P)}}}))});var jv,vIe,M3t,yIe,N3t,ud,YU,P3t,xIe,bIe=F(()=>{"use strict";jv=Rs(mw(),1),vIe=Rs(gIe(),1);St();Wr();Yr();Jt();Vn();jv.default.extend(vIe.default);M3t=s(function(){se.debug("Something is calling, setConf, remove the call")},"setConf"),yIe={monday:Hd,tuesday:uE,wednesday:hE,thursday:Kc,friday:dE,saturday:fE,sunday:ec},N3t=s((e,t)=>{let r=[...e].map(()=>-1/0),n=[...e].sort((a,o)=>a.startTime-o.startTime||a.order-o.order),i=0;for(let a of n)for(let o=0;o=r[o]){r[o]=a.endTime,a.order=o+t,o>i&&(i=o);break}return i},"getMaxIntersections"),YU=1e4,P3t=s(function(e,t,r,n){let i=Pe().gantt;n.db.setDiagramId(t);let a=Pe().securityLevel,o;a==="sandbox"&&(o=ut("#i"+t));let l=a==="sandbox"?ut(o.nodes()[0].contentDocument.body):ut("body"),u=a==="sandbox"?o.nodes()[0].contentDocument:document,h=u.getElementById(t);ud=h.parentElement.offsetWidth,ud===void 0&&(ud=1200),i.useWidth!==void 0&&(ud=i.useWidth);let d=n.db.getTasks(),f=d.filter(P=>!P.vert),p=[];for(let P of f)p.push(P.type);p=M(p);let m={},g=2*i.topPadding;if(n.db.getDisplayMode()==="compact"||i.displayMode==="compact"){let P={};for(let _ of f)P[_.section]===void 0?P[_.section]=[_]:P[_.section].push(_);let N=0;for(let _ of Object.keys(P)){let A=N3t(P[_],N)+1;N+=A,g+=A*(i.barHeight+i.barGap),m[_]=A}}else{g+=f.length*(i.barHeight+i.barGap);for(let P of p)m[P]=f.filter(N=>N.type===P).length}h.setAttribute("viewBox","0 0 "+ud+" "+g);let y=l.select(`[id="${t}"]`),v=gE().domain([Tk(d,function(P){return P.startTime}),bk(d,function(P){return P.endTime})]).rangeRound([0,ud-i.leftPadding-i.rightPadding]);function x(P,N){let _=P.startTime,A=N.startTime,I=0;return _>A?I=1:_H.vert===K.vert?0:H.vert?1:-1);let B=P.filter(H=>!H.vert),$=[...new Set(B.map(H=>H.order))].map(H=>B.find(K=>K.order===H));y.append("g").selectAll("rect").data($).enter().append("rect").attr("x",0).attr("y",function(H,K){return K=H.order,K*N+_-2}).attr("width",function(){return L-i.rightPadding/2}).attr("height",N).attr("class",function(H){for(let[K,J]of p.entries())if(H.type===J)return"section section"+K%i.numberSectionStyles;return"section section0"}).enter();let W=y.append("g").selectAll("rect").data(P).enter(),V=n.db.getLinks();if(W.append("rect").attr("id",function(H){return t+"-"+H.id}).attr("rx",3).attr("ry",3).attr("x",function(H){return H.milestone?v(H.startTime)+A+.5*(v(H.endTime)-v(H.startTime))-.5*I:v(H.startTime)+A}).attr("y",function(H,K){return K=H.order,H.vert?i.gridLineStartPadding:K*N+_}).attr("width",function(H){return H.milestone?I:H.vert?.08*I:v(H.renderEndTime||H.endTime)-v(H.startTime)}).attr("height",function(H){return H.vert?B.length*(i.barHeight+i.barGap)+i.barHeight*2:I}).attr("transform-origin",function(H,K){return K=H.order,(v(H.startTime)+A+.5*(v(H.endTime)-v(H.startTime))).toString()+"px "+(K*N+_+.5*I).toString()+"px"}).attr("class",function(H){let K="task",J="";H.classes.length>0&&(J=H.classes.join(" "));let ne=0;for(let[pe,te]of p.entries())H.type===te&&(ne=pe%i.numberSectionStyles);let Z="";return H.active?H.crit?Z+=" activeCrit":Z=" active":H.done?H.crit?Z=" doneCrit":Z=" done":H.crit&&(Z+=" crit"),Z.length===0&&(Z=" task"),H.milestone&&(Z=" milestone "+Z),H.vert&&(Z=" vert "+Z),Z+=ne,Z+=" "+J,K+Z}),W.append("text").attr("id",function(H){return t+"-"+H.id+"-text"}).text(function(H){return H.task}).attr("font-size",i.fontSize).attr("x",function(H){let K=v(H.startTime),J=v(H.renderEndTime||H.endTime);if(H.milestone&&(K+=.5*(v(H.endTime)-v(H.startTime))-.5*I,J=K+I),H.vert)return v(H.startTime)+A;let ne=this.getBBox().width;return ne>J-K?J+ne+1.5*i.leftPadding>L?K+A-5:J+A+5:(J-K)/2+K+A}).attr("y",function(H,K){return H.vert?i.gridLineStartPadding+B.length*(i.barHeight+i.barGap)+60:(K=H.order,K*N+i.barHeight/2+(i.fontSize/2-2)+_)}).attr("text-height",I).attr("class",function(H){let K=v(H.startTime),J=v(H.endTime);H.milestone&&(J=K+I);let ne=this.getBBox().width,Z="";H.classes.length>0&&(Z=H.classes.join(" "));let pe=0;for(let[fe,ce]of p.entries())H.type===ce&&(pe=fe%i.numberSectionStyles);let te="";return H.active&&(H.crit?te="activeCritText"+pe:te="activeText"+pe),H.done?H.crit?te=te+" doneCritText"+pe:te=te+" doneText"+pe:H.crit&&(te=te+" critText"+pe),H.milestone&&(te+=" milestoneText"),H.vert&&(te+=" vertText"),ne>J-K?J+ne+1.5*i.leftPadding>L?Z+" taskTextOutsideLeft taskTextOutside"+pe+" "+te:Z+" taskTextOutsideRight taskTextOutside"+pe+" "+te+" width-"+ne:Z+" taskText taskText"+pe+" "+te+" width-"+ne}),Pe().securityLevel==="sandbox"){let H;H=ut("#i"+t);let K=H.nodes()[0].contentDocument;W.filter(function(J){return V.has(J.id)}).each(function(J){var ne=K.querySelector("#"+CSS.escape(t+"-"+J.id)),Z=K.querySelector("#"+CSS.escape(t+"-"+J.id+"-text"));let pe=ne.parentNode;var te=K.createElement("a");te.setAttribute("xlink:href",V.get(J.id)),te.setAttribute("target","_top"),pe.appendChild(te),te.appendChild(ne),te.appendChild(Z)})}}s(T,"drawRects");function w(P,N,_,A,I,D,L,B){if(L.length===0&&B.length===0)return;let O,$;for(let{startTime:J,endTime:ne}of D)(O===void 0||J$)&&($=ne);if(!O||!$)return;if((0,jv.default)($).diff((0,jv.default)(O),"year")>5){se.warn("The difference between the min and max time is more than 5 years. This will cause performance issues. Skipping drawing exclude days.");return}let W=n.db.getDateFormat(),V=[],z=null,H=(0,jv.default)(O);for(;H.valueOf()<=$;)n.db.isInvalidDate(H,W,L,B)?z?z.end=H:z={start:H,end:H}:z&&(V.push(z),z=null),H=H.add(1,"d");y.append("g").selectAll("rect").data(V).enter().append("rect").attr("id",J=>t+"-exclude-"+J.start.format("YYYY-MM-DD")).attr("x",J=>v(J.start.startOf("day"))+_).attr("y",i.gridLineStartPadding).attr("width",J=>v(J.end.endOf("day"))-v(J.start.startOf("day"))).attr("height",I-N-i.gridLineStartPadding).attr("transform-origin",function(J,ne){return(v(J.start)+_+.5*(v(J.end)-v(J.start))).toString()+"px "+(ne*P+.5*I).toString()+"px"}).attr("class","exclude-range")}s(w,"drawExcludeDays");function C(P,N,_,A){if(_<=0||P>N)return 1/0;let I=N-P,D=jv.default.duration({[A??"day"]:_}).asMilliseconds();return D<=0?1/0:Math.ceil(I/D)}s(C,"getEstimatedTickCount");function S(P,N,_,A){let I=n.db.getDateFormat(),D=n.db.getAxisFormat(),L;D?L=D:I==="D"?L="%d":L=i.axisFormat??"%Y-%m-%d";let B=vI(v).tickSize(-A+N+i.gridLineStartPadding).tickFormat(pm(L)),$=/^([1-9]\d*)(millisecond|second|minute|hour|day|week|month)$/.exec(n.db.getTickInterval()||i.tickInterval);if($!==null){let W=parseInt($[1],10);if(isNaN(W)||W<=0)se.warn(`Invalid tick interval value: "${$[1]}". Skipping custom tick interval.`);else{let V=$[2],z=n.db.getWeekday()||i.weekday,H=v.domain(),K=H[0],J=H[1],ne=C(K,J,W,V);if(ne>YU)se.warn(`The tick interval "${W}${V}" would generate ${ne} ticks, which exceeds the maximum allowed (${YU}). This may indicate an invalid date or time range. Skipping custom tick interval.`);else switch(V){case"millisecond":B.ticks(jc.every(W));break;case"second":B.ticks(zo.every(W));break;case"minute":B.ticks(gh.every(W));break;case"hour":B.ticks(yh.every(W));break;case"day":B.ticks(gl.every(W));break;case"week":B.ticks(yIe[z].every(W));break;case"month":B.ticks(vh.every(W));break}}}if(y.append("g").attr("class","grid").attr("transform","translate("+P+", "+(A-50)+")").call(B).selectAll("text").style("text-anchor","middle").attr("fill","#000").attr("stroke","none").attr("font-size",10).attr("dy","1em"),n.db.topAxisEnabled()||i.topAxis){let W=yI(v).tickSize(-A+N+i.gridLineStartPadding).tickFormat(pm(L));if($!==null){let V=parseInt($[1],10);if(isNaN(V)||V<=0)se.warn(`Invalid tick interval value: "${$[1]}". Skipping custom tick interval.`);else{let z=$[2],H=n.db.getWeekday()||i.weekday,K=v.domain(),J=K[0],ne=K[1];if(C(J,ne,V,z)<=YU)switch(z){case"millisecond":W.ticks(jc.every(V));break;case"second":W.ticks(zo.every(V));break;case"minute":W.ticks(gh.every(V));break;case"hour":W.ticks(yh.every(V));break;case"day":W.ticks(gl.every(V));break;case"week":W.ticks(yIe[H].every(V));break;case"month":W.ticks(vh.every(V));break}}}y.append("g").attr("class","grid").attr("transform","translate("+P+", "+N+")").call(W).selectAll("text").style("text-anchor","middle").attr("fill","#000").attr("stroke","none").attr("font-size",10)}}s(S,"makeGrid");function k(P,N){let _=0,A=Object.keys(m).map(I=>[I,m[I]]);y.append("g").selectAll("text").data(A).enter().append(function(I){let D=I[0].split(Tt.lineBreakRegex),L=-(D.length-1)/2,B=u.createElementNS("http://www.w3.org/2000/svg","text");B.setAttribute("dy",L+"em");for(let[O,$]of D.entries()){let W=u.createElementNS("http://www.w3.org/2000/svg","tspan");W.setAttribute("alignment-baseline","central"),W.setAttribute("x","10"),O>0&&W.setAttribute("dy","1em"),W.textContent=$,B.appendChild(W)}return B}).attr("x",10).attr("y",function(I,D){if(D>0)for(let L=0;L{"use strict";O3t=s(e=>` + .mermaid-main-font { + font-family: ${e.fontFamily}; + } + + .exclude-range { + fill: ${e.excludeBkgColor}; + } + + .section { + stroke: none; + opacity: 0.2; + } + + .section0 { + fill: ${e.sectionBkgColor}; + } + + .section2 { + fill: ${e.sectionBkgColor2}; + } + + .section1, + .section3 { + fill: ${e.altSectionBkgColor}; + opacity: 0.2; + } + + .sectionTitle0 { + fill: ${e.titleColor}; + } + + .sectionTitle1 { + fill: ${e.titleColor}; + } + + .sectionTitle2 { + fill: ${e.titleColor}; + } + + .sectionTitle3 { + fill: ${e.titleColor}; + } + + .sectionTitle { + text-anchor: start; + font-family: ${e.fontFamily}; + } + + + /* Grid and axis */ + + .grid .tick { + stroke: ${e.gridColor}; + opacity: 0.8; + shape-rendering: crispEdges; + } + + .grid .tick text { + font-family: ${e.fontFamily}; + fill: ${e.textColor}; + } + + .grid path { + stroke-width: 0; + } + + + /* Today line */ + + .today { + fill: none; + stroke: ${e.todayLineColor}; + stroke-width: 2px; + } + + + /* Task styling */ + + /* Default task */ + + .task { + stroke-width: 2; + } + + .taskText { + text-anchor: middle; + font-family: ${e.fontFamily}; + } + + .taskTextOutsideRight { + fill: ${e.taskTextDarkColor}; + text-anchor: start; + font-family: ${e.fontFamily}; + } + + .taskTextOutsideLeft { + fill: ${e.taskTextDarkColor}; + text-anchor: end; + } + + + /* Special case clickable */ + + .task.clickable { + cursor: pointer; + } + + .taskText.clickable { + cursor: pointer; + fill: ${e.taskTextClickableColor} !important; + font-weight: bold; + } + + .taskTextOutsideLeft.clickable { + cursor: pointer; + fill: ${e.taskTextClickableColor} !important; + font-weight: bold; + } + + .taskTextOutsideRight.clickable { + cursor: pointer; + fill: ${e.taskTextClickableColor} !important; + font-weight: bold; + } + + + /* Specific task settings for the sections*/ + + .taskText0, + .taskText1, + .taskText2, + .taskText3 { + fill: ${e.taskTextColor}; + } + + .task0, + .task1, + .task2, + .task3 { + fill: ${e.taskBkgColor}; + stroke: ${e.taskBorderColor}; + } + + .taskTextOutside0, + .taskTextOutside2 + { + fill: ${e.taskTextOutsideColor}; + } + + .taskTextOutside1, + .taskTextOutside3 { + fill: ${e.taskTextOutsideColor}; + } + + + /* Active task */ + + .active0, + .active1, + .active2, + .active3 { + fill: ${e.activeTaskBkgColor}; + stroke: ${e.activeTaskBorderColor}; + } + + .activeText0, + .activeText1, + .activeText2, + .activeText3 { + fill: ${e.taskTextDarkColor} !important; + } + + + /* Completed task */ + + .done0, + .done1, + .done2, + .done3 { + stroke: ${e.doneTaskBorderColor}; + fill: ${e.doneTaskBkgColor}; + stroke-width: 2; + } + + .doneText0, + .doneText1, + .doneText2, + .doneText3 { + fill: ${e.taskTextDarkColor} !important; + } + + /* Done task text displayed outside the bar sits against the diagram background, + not against the done-task bar, so it must use the outside/contrast color. */ + .doneText0.taskTextOutsideLeft, + .doneText0.taskTextOutsideRight, + .doneText1.taskTextOutsideLeft, + .doneText1.taskTextOutsideRight, + .doneText2.taskTextOutsideLeft, + .doneText2.taskTextOutsideRight, + .doneText3.taskTextOutsideLeft, + .doneText3.taskTextOutsideRight { + fill: ${e.taskTextOutsideColor} !important; + } + + + /* Tasks on the critical line */ + + .crit0, + .crit1, + .crit2, + .crit3 { + stroke: ${e.critBorderColor}; + fill: ${e.critBkgColor}; + stroke-width: 2; + } + + .activeCrit0, + .activeCrit1, + .activeCrit2, + .activeCrit3 { + stroke: ${e.critBorderColor}; + fill: ${e.activeTaskBkgColor}; + stroke-width: 2; + } + + .doneCrit0, + .doneCrit1, + .doneCrit2, + .doneCrit3 { + stroke: ${e.critBorderColor}; + fill: ${e.doneTaskBkgColor}; + stroke-width: 2; + cursor: pointer; + shape-rendering: crispEdges; + } + + .milestone { + transform: rotate(45deg) scale(0.8,0.8); + } + + .milestoneText { + font-style: italic; + } + .doneCritText0, + .doneCritText1, + .doneCritText2, + .doneCritText3 { + fill: ${e.taskTextDarkColor} !important; + } + + /* Done-crit task text outside the bar \u2014 same reasoning as doneText above. */ + .doneCritText0.taskTextOutsideLeft, + .doneCritText0.taskTextOutsideRight, + .doneCritText1.taskTextOutsideLeft, + .doneCritText1.taskTextOutsideRight, + .doneCritText2.taskTextOutsideLeft, + .doneCritText2.taskTextOutsideRight, + .doneCritText3.taskTextOutsideLeft, + .doneCritText3.taskTextOutsideRight { + fill: ${e.taskTextOutsideColor} !important; + } + + .vert { + stroke: ${e.vertLineColor}; + } + + .vertText { + font-size: 15px; + text-anchor: middle; + fill: ${e.vertLineColor} !important; + } + + .activeCritText0, + .activeCritText1, + .activeCritText2, + .activeCritText3 { + fill: ${e.taskTextDarkColor} !important; + } + + .titleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.titleColor||e.textColor}; + font-family: ${e.fontFamily}; + } +`,"getStyles"),TIe=O3t});var SIe={};sr(SIe,{diagram:()=>B3t});var B3t,wIe=F(()=>{"use strict";j7e();mIe();bIe();CIe();B3t={parser:Y7e,db:fIe,renderer:xIe,styles:TIe}});var AIe,RIe=F(()=>{"use strict";ls();St();AIe={parse:s(async e=>{let t=await Li("info",e);se.debug(t)},"parse")}});var z3t,V3t,_Ie,LIe=F(()=>{"use strict";z3t={version:"11.17.2"},V3t=s(()=>z3t.version,"getVersion"),_Ie={getVersion:V3t}});var wn,cs=F(()=>{"use strict";Wr();Jt();wn=s(e=>{let{securityLevel:t}=Pe(),r=ut("body");if(t==="sandbox"){let a=ut(`#i${e}`).node()?.contentDocument??document;r=ut(a.body)}return r.select(`#${e}`)},"selectSvgElement")});var W3t,DIe,IIe=F(()=>{"use strict";St();cs();Vn();W3t=s((e,t,r)=>{se.debug(`rendering info diagram +`+e);let n=wn(t);jr(n,100,400,!0),n.append("g").append("text").attr("x",100).attr("y",40).attr("class","version").attr("font-size",32).style("text-anchor","middle").text(`v${r}`)},"draw"),DIe={draw:W3t}});var MIe={};sr(MIe,{diagram:()=>q3t});var q3t,NIe=F(()=>{"use strict";RIe();LIe();IIe();q3t={parser:AIe,db:_Ie,renderer:DIe}});var BIe,jU,U_,XU,Y3t,j3t,X3t,K3t,Z3t,Q3t,J3t,Y_,KU=F(()=>{"use strict";St();$n();Qi();BIe=mr.pie,jU={sections:new Map,showData:!1,config:BIe},U_=jU.sections,XU=jU.showData,Y3t=structuredClone(BIe),j3t=s(()=>structuredClone(Y3t),"getConfig"),X3t=s(()=>{U_=new Map,XU=jU.showData,Tr()},"clear"),K3t=s(({label:e,value:t})=>{if(t<0)throw new Error(`"${e}" has invalid value: ${t}. Negative values are not allowed in pie charts. All slice values must be >= 0.`);U_.has(e)||(U_.set(e,t),se.debug(`added new section: ${e}, with value: ${t}`))},"addSection"),Z3t=s(()=>U_,"getSections"),Q3t=s(e=>{XU=e},"setShowData"),J3t=s(()=>XU,"getShowData"),Y_={getConfig:j3t,clear:X3t,setDiagramTitle:Gr,getDiagramTitle:Pr,setAccTitle:Lr,getAccTitle:Ir,setAccDescription:Mr,getAccDescription:Nr,addSection:K3t,getSections:Z3t,setShowData:Q3t,getShowData:J3t}});var e5t,$Ie,FIe=F(()=>{"use strict";ls();St();to();KU();e5t=s((e,t)=>{Hn(e,t),t.setShowData(e.showData),e.sections.map(t.addSection)},"populateDb"),$Ie={parse:s(async e=>{let t=await Li("pie",e);se.debug(t),e5t(t,Y_)},"parse")}});var t5t,GIe,zIe=F(()=>{"use strict";t5t=s(e=>` + .pieCircle{ + stroke: ${e.pieStrokeColor}; + stroke-width : ${e.pieStrokeWidth}; + opacity : ${e.pieOpacity}; + } + .pieCircle.highlighted{ + scale: 1.05; + opacity: 1; + } + .pieCircle.highlightedOnHover:hover{ + transition-duration: 250ms; + scale: 1.05; + opacity: 1; + } + .pieOuterCircle{ + stroke: ${e.pieOuterStrokeColor}; + stroke-width: ${e.pieOuterStrokeWidth}; + fill: none; + } + .pieTitleText { + text-anchor: middle; + font-size: ${e.pieTitleTextSize}; + fill: ${e.pieTitleTextColor}; + font-family: ${e.fontFamily}; + } + .slice { + font-family: ${e.fontFamily}; + fill: ${e.pieSectionTextColor}; + font-size:${e.pieSectionTextSize}; + // fill: white; + } + .legend text { + fill: ${e.pieLegendTextColor}; + font-family: ${e.fontFamily}; + font-size: ${e.pieLegendTextSize}; + } +`,"getStyles"),GIe=t5t});var r5t,n5t,VIe,WIe=F(()=>{"use strict";Wr();Jt();St();cs();Vn();er();r5t=s(e=>{let t=[...e.values()].reduce((i,a)=>i+a,0),r=[...e.entries()].map(([i,a])=>({label:i,value:a})).filter(i=>i.value/t*100>=1);return CE().value(i=>i.value).sort(null)(r)},"createPieArcs"),n5t=s((e,t,r,n)=>{se.debug(`rendering pie chart +`+e);let i=n.db,a=Pe(),o=Xr(i.getConfig(),a.pie),l=40,u=18,h=4,d=450,f=d,p=wn(t),m=p.append("g");m.attr("transform","translate("+f/2+","+d/2+")");let{themeVariables:g}=a,[y]=Bs(g.pieOuterStrokeWidth);y??=2;let v=o.legendPosition,x=o.textPosition,b=o.donutHole>0&&o.donutHole<=.9?o.donutHole:0,T=Math.min(f,d)/2-l,w=nc().innerRadius(b*T).outerRadius(T),C=nc().innerRadius(T*x).outerRadius(T*x),S=m.append("g");S.append("circle").attr("cx",0).attr("cy",0).attr("r",T+y/2).attr("class","pieOuterCircle");let k=i.getSections(),R=r5t(k),M=[g.pie1,g.pie2,g.pie3,g.pie4,g.pie5,g.pie6,g.pie7,g.pie8,g.pie9,g.pie10,g.pie11,g.pie12],P=0;k.forEach(Z=>{P+=Z});let N=R.filter(Z=>(Z.data.value/P*100).toFixed(0)!=="0"),_=Go(M).domain([...k.keys()]);S.selectAll("mySlices").data(N).enter().append("path").attr("d",w).attr("fill",Z=>_(Z.data.label)).attr("class",Z=>{let pe="pieCircle";return o.highlightSlice==="hover"?pe+=" highlightedOnHover":o.highlightSlice===Z.data.label&&(pe+=" highlighted"),pe}),S.selectAll("mySlices").data(N).enter().append("text").text(Z=>(Z.data.value/P*100).toFixed(0)+"%").attr("transform",Z=>"translate("+C.centroid(Z)+")").style("text-anchor","middle").attr("class","slice");let A=m.append("text").text(i.getDiagramTitle()).attr("x",0).attr("y",-(d-50)/2).attr("class","pieTitleText"),I=[...k.entries()].map(([Z,pe])=>({label:Z,value:pe})),D=m.selectAll(".legend").data(I).enter().append("g").attr("class","legend");D.append("rect").attr("width",u).attr("height",u).style("fill",Z=>_(Z.label)).style("stroke",Z=>_(Z.label)),D.append("text").attr("x",u+h).attr("y",u-h).text(Z=>i.getShowData()?`${Z.label} [${Z.value}]`:Z.label);let L=Math.max(...D.selectAll("text").nodes().map(Z=>Z?.getBoundingClientRect().width??0)),B=d,O=f+l,$=u+h,W=I.length*$;switch(v){case"center":D.attr("transform",(Z,pe)=>{let te=$*I.length/2,fe=-L/2-(u+h),ce=pe*$-te;return"translate("+fe+","+ce+")"});break;case"top":B+=W,D.attr("transform",(Z,pe)=>{let te=T,fe=-L/2-(u+h),ce=pe*$-te;return`translate(${fe}, ${ce})`}),S.attr("transform",()=>`translate(0, ${W+$})`);break;case"bottom":B+=W,D.attr("transform",(Z,pe)=>{let te=-T-$,fe=-L/2-(u+h),ce=pe*$-te;return"translate("+fe+","+ce+")"});break;case"left":O+=u+h+L,D.attr("transform",(Z,pe)=>{let te=$*I.length/2,fe=-T-(u+h),ce=pe*$-te;return"translate("+fe+","+ce+")"}),S.attr("transform",()=>`translate(${L+u+h}, 0)`);break;case"right":default:O+=u+h+L,D.attr("transform",(Z,pe)=>{let te=$*I.length/2,fe=12*u,ce=pe*$-te;return"translate("+fe+","+ce+")"});break}let V=A.node()?.getBoundingClientRect().width??0,z=f/2-V/2,H=f/2+V/2,K=Math.min(0,z),ne=Math.max(O,H)-K;p.attr("viewBox",`${K} 0 ${ne} ${B}`),jr(p,B,ne,o.useMaxWidth)},"draw"),VIe={draw:n5t}});var qIe={};sr(qIe,{diagram:()=>i5t});var i5t,HIe=F(()=>{"use strict";FIe();KU();zIe();WIe();i5t={parser:$Ie,db:Y_,renderer:VIe,styles:GIe}});var ZU,YIe,jIe=F(()=>{"use strict";ZU=(function(){var e=s(function(j,U,Y,Ae){for(Y=Y||{},Ae=j.length;Ae--;Y[j[Ae]]=U);return Y},"o"),t=[1,3],r=[1,4],n=[1,5],i=[1,6],a=[1,7],o=[1,4,5,10,12,13,14,15,18,25,35,37,39,41,42,48,50,51,52,53,54,55,56,57,60,61,63,64,65,66,67],l=[1,4,5,10,12,13,14,15,18,25,28,35,37,39,41,42,48,50,51,52,53,54,55,56,57,60,61,63,64,65,66,67],u=[55,56,57],h=[2,36],d=[1,37],f=[1,36],p=[1,38],m=[1,35],g=[1,43],y=[1,41],v=[1,45],x=[1,14],b=[1,23],T=[1,18],w=[1,19],C=[1,20],S=[1,21],k=[1,22],R=[1,24],M=[1,25],P=[1,26],N=[1,27],_=[1,28],A=[1,29],I=[1,32],D=[1,33],L=[1,34],B=[1,39],O=[1,40],$=[1,42],W=[1,44],V=[1,63],z=[1,62],H=[4,5,8,10,12,13,14,15,18,44,47,49,55,56,57,63,64,65,66,67],K=[1,66],J=[1,67],ne=[1,68],Z=[1,69],pe=[1,70],te=[1,71],fe=[1,72],ce=[1,73],de=[1,74],Ee=[1,75],we=[1,76],Fe=[1,77],Ye=[4,5,6,7,8,9,10,11,12,13,14,15,18],He=[1,91],ye=[1,92],Be=[1,93],Ie=[1,100],Ne=[1,94],re=[1,97],ue=[1,95],le=[1,96],he=[1,98],Te=[1,99],oe=[1,103],G=[10,55,56,57],X=[4,5,6,8,10,11,13,17,18,19,20,55,56,57],ee={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,idStringToken:3,ALPHA:4,NUM:5,NODE_STRING:6,DOWN:7,MINUS:8,DEFAULT:9,COMMA:10,COLON:11,AMP:12,BRKT:13,MULT:14,UNICODE_TEXT:15,styleComponent:16,UNIT:17,SPACE:18,STYLE:19,PCT:20,idString:21,style:22,stylesOpt:23,classDefStatement:24,CLASSDEF:25,start:26,eol:27,QUADRANT:28,document:29,line:30,statement:31,axisDetails:32,quadrantDetails:33,points:34,title:35,title_value:36,acc_title:37,acc_title_value:38,acc_descr:39,acc_descr_value:40,acc_descr_multiline_value:41,section:42,text:43,point_start:44,point_x:45,point_y:46,class_name:47,"X-AXIS":48,"AXIS-TEXT-DELIMITER":49,"Y-AXIS":50,QUADRANT_1:51,QUADRANT_2:52,QUADRANT_3:53,QUADRANT_4:54,NEWLINE:55,SEMI:56,EOF:57,alphaNumToken:58,textNoTagsToken:59,STR:60,MD_STR:61,alphaNum:62,PUNCTUATION:63,PLUS:64,EQUALS:65,DOT:66,UNDERSCORE:67,$accept:0,$end:1},terminals_:{2:"error",4:"ALPHA",5:"NUM",6:"NODE_STRING",7:"DOWN",8:"MINUS",9:"DEFAULT",10:"COMMA",11:"COLON",12:"AMP",13:"BRKT",14:"MULT",15:"UNICODE_TEXT",17:"UNIT",18:"SPACE",19:"STYLE",20:"PCT",25:"CLASSDEF",28:"QUADRANT",35:"title",36:"title_value",37:"acc_title",38:"acc_title_value",39:"acc_descr",40:"acc_descr_value",41:"acc_descr_multiline_value",42:"section",44:"point_start",45:"point_x",46:"point_y",47:"class_name",48:"X-AXIS",49:"AXIS-TEXT-DELIMITER",50:"Y-AXIS",51:"QUADRANT_1",52:"QUADRANT_2",53:"QUADRANT_3",54:"QUADRANT_4",55:"NEWLINE",56:"SEMI",57:"EOF",60:"STR",61:"MD_STR",63:"PUNCTUATION",64:"PLUS",65:"EQUALS",66:"DOT",67:"UNDERSCORE"},productions_:[0,[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[21,1],[21,2],[22,1],[22,2],[23,1],[23,3],[24,5],[26,2],[26,2],[26,2],[29,0],[29,2],[30,2],[31,0],[31,1],[31,2],[31,1],[31,1],[31,1],[31,2],[31,2],[31,2],[31,1],[31,1],[34,4],[34,5],[34,5],[34,6],[32,4],[32,3],[32,2],[32,4],[32,3],[32,2],[33,2],[33,2],[33,2],[33,2],[27,1],[27,1],[27,1],[43,1],[43,2],[43,1],[43,1],[62,1],[62,2],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[59,1],[59,1],[59,1]],performAction:s(function(U,Y,Ae,me,Se,Ce,Me){var _e=Ce.length-1;switch(Se){case 23:this.$=Ce[_e];break;case 24:this.$=Ce[_e-1]+""+Ce[_e];break;case 26:this.$=Ce[_e-1]+Ce[_e];break;case 27:this.$=[Ce[_e].trim()];break;case 28:Ce[_e-2].push(Ce[_e].trim()),this.$=Ce[_e-2];break;case 29:this.$=Ce[_e-4],me.addClass(Ce[_e-2],Ce[_e]);break;case 37:this.$=[];break;case 42:this.$=Ce[_e].trim(),me.setDiagramTitle(this.$);break;case 43:this.$=Ce[_e].trim(),me.setAccTitle(this.$);break;case 44:case 45:this.$=Ce[_e].trim(),me.setAccDescription(this.$);break;case 46:me.addSection(Ce[_e].substr(8)),this.$=Ce[_e].substr(8);break;case 47:me.addPoint(Ce[_e-3],"",Ce[_e-1],Ce[_e],[]);break;case 48:me.addPoint(Ce[_e-4],Ce[_e-3],Ce[_e-1],Ce[_e],[]);break;case 49:me.addPoint(Ce[_e-4],"",Ce[_e-2],Ce[_e-1],Ce[_e]);break;case 50:me.addPoint(Ce[_e-5],Ce[_e-4],Ce[_e-2],Ce[_e-1],Ce[_e]);break;case 51:me.setXAxisLeftText(Ce[_e-2]),me.setXAxisRightText(Ce[_e]);break;case 52:Ce[_e-1].text+=" \u27F6 ",me.setXAxisLeftText(Ce[_e-1]);break;case 53:me.setXAxisLeftText(Ce[_e]);break;case 54:me.setYAxisBottomText(Ce[_e-2]),me.setYAxisTopText(Ce[_e]);break;case 55:Ce[_e-1].text+=" \u27F6 ",me.setYAxisBottomText(Ce[_e-1]);break;case 56:me.setYAxisBottomText(Ce[_e]);break;case 57:me.setQuadrant1Text(Ce[_e]);break;case 58:me.setQuadrant2Text(Ce[_e]);break;case 59:me.setQuadrant3Text(Ce[_e]);break;case 60:me.setQuadrant4Text(Ce[_e]);break;case 64:this.$={text:Ce[_e],type:"text"};break;case 65:this.$={text:Ce[_e-1].text+""+Ce[_e],type:Ce[_e-1].type};break;case 66:this.$={text:Ce[_e],type:"text"};break;case 67:this.$={text:Ce[_e],type:"markdown"};break;case 68:this.$=Ce[_e];break;case 69:this.$=Ce[_e-1]+""+Ce[_e];break}},"anonymous"),table:[{18:t,26:1,27:2,28:r,55:n,56:i,57:a},{1:[3]},{18:t,26:8,27:2,28:r,55:n,56:i,57:a},{18:t,26:9,27:2,28:r,55:n,56:i,57:a},e(o,[2,33],{29:10}),e(l,[2,61]),e(l,[2,62]),e(l,[2,63]),{1:[2,30]},{1:[2,31]},e(u,h,{30:11,31:12,24:13,32:15,33:16,34:17,43:30,58:31,1:[2,32],4:d,5:f,10:p,12:m,13:g,14:y,15:v,18:x,25:b,35:T,37:w,39:C,41:S,42:k,48:R,50:M,51:P,52:N,53:_,54:A,60:I,61:D,63:L,64:B,65:O,66:$,67:W}),e(o,[2,34]),{27:46,55:n,56:i,57:a},e(u,[2,37]),e(u,h,{24:13,32:15,33:16,34:17,43:30,58:31,31:47,4:d,5:f,10:p,12:m,13:g,14:y,15:v,18:x,25:b,35:T,37:w,39:C,41:S,42:k,48:R,50:M,51:P,52:N,53:_,54:A,60:I,61:D,63:L,64:B,65:O,66:$,67:W}),e(u,[2,39]),e(u,[2,40]),e(u,[2,41]),{36:[1,48]},{38:[1,49]},{40:[1,50]},e(u,[2,45]),e(u,[2,46]),{18:[1,51]},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:52,58:31,60:I,61:D,63:L,64:B,65:O,66:$,67:W},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:53,58:31,60:I,61:D,63:L,64:B,65:O,66:$,67:W},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:54,58:31,60:I,61:D,63:L,64:B,65:O,66:$,67:W},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:55,58:31,60:I,61:D,63:L,64:B,65:O,66:$,67:W},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:56,58:31,60:I,61:D,63:L,64:B,65:O,66:$,67:W},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:57,58:31,60:I,61:D,63:L,64:B,65:O,66:$,67:W},{4:d,5:f,8:V,10:p,12:m,13:g,14:y,15:v,18:z,44:[1,58],47:[1,59],58:61,59:60,63:L,64:B,65:O,66:$,67:W},e(H,[2,64]),e(H,[2,66]),e(H,[2,67]),e(H,[2,70]),e(H,[2,71]),e(H,[2,72]),e(H,[2,73]),e(H,[2,74]),e(H,[2,75]),e(H,[2,76]),e(H,[2,77]),e(H,[2,78]),e(H,[2,79]),e(H,[2,80]),e(H,[2,81]),e(o,[2,35]),e(u,[2,38]),e(u,[2,42]),e(u,[2,43]),e(u,[2,44]),{3:65,4:K,5:J,6:ne,7:Z,8:pe,9:te,10:fe,11:ce,12:de,13:Ee,14:we,15:Fe,21:64},e(u,[2,53],{59:60,58:61,4:d,5:f,8:V,10:p,12:m,13:g,14:y,15:v,18:z,49:[1,78],63:L,64:B,65:O,66:$,67:W}),e(u,[2,56],{59:60,58:61,4:d,5:f,8:V,10:p,12:m,13:g,14:y,15:v,18:z,49:[1,79],63:L,64:B,65:O,66:$,67:W}),e(u,[2,57],{59:60,58:61,4:d,5:f,8:V,10:p,12:m,13:g,14:y,15:v,18:z,63:L,64:B,65:O,66:$,67:W}),e(u,[2,58],{59:60,58:61,4:d,5:f,8:V,10:p,12:m,13:g,14:y,15:v,18:z,63:L,64:B,65:O,66:$,67:W}),e(u,[2,59],{59:60,58:61,4:d,5:f,8:V,10:p,12:m,13:g,14:y,15:v,18:z,63:L,64:B,65:O,66:$,67:W}),e(u,[2,60],{59:60,58:61,4:d,5:f,8:V,10:p,12:m,13:g,14:y,15:v,18:z,63:L,64:B,65:O,66:$,67:W}),{45:[1,80]},{44:[1,81]},e(H,[2,65]),e(H,[2,82]),e(H,[2,83]),e(H,[2,84]),{3:83,4:K,5:J,6:ne,7:Z,8:pe,9:te,10:fe,11:ce,12:de,13:Ee,14:we,15:Fe,18:[1,82]},e(Ye,[2,23]),e(Ye,[2,1]),e(Ye,[2,2]),e(Ye,[2,3]),e(Ye,[2,4]),e(Ye,[2,5]),e(Ye,[2,6]),e(Ye,[2,7]),e(Ye,[2,8]),e(Ye,[2,9]),e(Ye,[2,10]),e(Ye,[2,11]),e(Ye,[2,12]),e(u,[2,52],{58:31,43:84,4:d,5:f,10:p,12:m,13:g,14:y,15:v,60:I,61:D,63:L,64:B,65:O,66:$,67:W}),e(u,[2,55],{58:31,43:85,4:d,5:f,10:p,12:m,13:g,14:y,15:v,60:I,61:D,63:L,64:B,65:O,66:$,67:W}),{46:[1,86]},{45:[1,87]},{4:He,5:ye,6:Be,8:Ie,11:Ne,13:re,16:90,17:ue,18:le,19:he,20:Te,22:89,23:88},e(Ye,[2,24]),e(u,[2,51],{59:60,58:61,4:d,5:f,8:V,10:p,12:m,13:g,14:y,15:v,18:z,63:L,64:B,65:O,66:$,67:W}),e(u,[2,54],{59:60,58:61,4:d,5:f,8:V,10:p,12:m,13:g,14:y,15:v,18:z,63:L,64:B,65:O,66:$,67:W}),e(u,[2,47],{22:89,16:90,23:101,4:He,5:ye,6:Be,8:Ie,11:Ne,13:re,17:ue,18:le,19:he,20:Te}),{46:[1,102]},e(u,[2,29],{10:oe}),e(G,[2,27],{16:104,4:He,5:ye,6:Be,8:Ie,11:Ne,13:re,17:ue,18:le,19:he,20:Te}),e(X,[2,25]),e(X,[2,13]),e(X,[2,14]),e(X,[2,15]),e(X,[2,16]),e(X,[2,17]),e(X,[2,18]),e(X,[2,19]),e(X,[2,20]),e(X,[2,21]),e(X,[2,22]),e(u,[2,49],{10:oe}),e(u,[2,48],{22:89,16:90,23:105,4:He,5:ye,6:Be,8:Ie,11:Ne,13:re,17:ue,18:le,19:he,20:Te}),{4:He,5:ye,6:Be,8:Ie,11:Ne,13:re,16:90,17:ue,18:le,19:he,20:Te,22:106},e(X,[2,26]),e(u,[2,50],{10:oe}),e(G,[2,28],{16:104,4:He,5:ye,6:Be,8:Ie,11:Ne,13:re,17:ue,18:le,19:he,20:Te})],defaultActions:{8:[2,30],9:[2,31]},parseError:s(function(U,Y){if(Y.recoverable)this.trace(U);else{var Ae=new Error(U);throw Ae.hash=Y,Ae}},"parseError"),parse:s(function(U){var Y=this,Ae=[0],me=[],Se=[null],Ce=[],Me=this.table,_e="",ze=0,Le=0,qe=0,ve=2,lt=1,Oe=Ce.slice.call(arguments,1),mt=Object.create(this.lexer),At={yy:{}};for(var Gt in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Gt)&&(At.yy[Gt]=this.yy[Gt]);mt.setInput(U,At.yy),At.yy.lexer=mt,At.yy.parser=this,typeof mt.yylloc>"u"&&(mt.yylloc={});var Ct=mt.yylloc;Ce.push(Ct);var Rt=mt.options&&mt.options.ranges;typeof At.yy.parseError=="function"?this.parseError=At.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function gt(Wt){Ae.length=Ae.length-2*Wt,Se.length=Se.length-Wt,Ce.length=Ce.length-Wt}s(gt,"popStack");function be(){var Wt;return Wt=me.pop()||mt.lex()||lt,typeof Wt!="number"&&(Wt instanceof Array&&(me=Wt,Wt=me.pop()),Wt=Y.symbols_[Wt]||Wt),Wt}s(be,"lex");for(var ct,xt,Ke,De,tt,Ze,dt={},Ut,$t,Yt,Sr;;){if(Ke=Ae[Ae.length-1],this.defaultActions[Ke]?De=this.defaultActions[Ke]:((ct===null||typeof ct>"u")&&(ct=be()),De=Me[Ke]&&Me[Ke][ct]),typeof De>"u"||!De.length||!De[0]){var lr="";Sr=[];for(Ut in Me[Ke])this.terminals_[Ut]&&Ut>ve&&Sr.push("'"+this.terminals_[Ut]+"'");mt.showPosition?lr="Parse error on line "+(ze+1)+`: +`+mt.showPosition()+` +Expecting `+Sr.join(", ")+", got '"+(this.terminals_[ct]||ct)+"'":lr="Parse error on line "+(ze+1)+": Unexpected "+(ct==lt?"end of input":"'"+(this.terminals_[ct]||ct)+"'"),this.parseError(lr,{text:mt.match,token:this.terminals_[ct]||ct,line:mt.yylineno,loc:Ct,expected:Sr})}if(De[0]instanceof Array&&De.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Ke+", token: "+ct);switch(De[0]){case 1:Ae.push(ct),Se.push(mt.yytext),Ce.push(mt.yylloc),Ae.push(De[1]),ct=null,xt?(ct=xt,xt=null):(Le=mt.yyleng,_e=mt.yytext,ze=mt.yylineno,Ct=mt.yylloc,qe>0&&qe--);break;case 2:if($t=this.productions_[De[1]][1],dt.$=Se[Se.length-$t],dt._$={first_line:Ce[Ce.length-($t||1)].first_line,last_line:Ce[Ce.length-1].last_line,first_column:Ce[Ce.length-($t||1)].first_column,last_column:Ce[Ce.length-1].last_column},Rt&&(dt._$.range=[Ce[Ce.length-($t||1)].range[0],Ce[Ce.length-1].range[1]]),Ze=this.performAction.apply(dt,[_e,Le,ze,At.yy,De[1],Se,Ce].concat(Oe)),typeof Ze<"u")return Ze;$t&&(Ae=Ae.slice(0,-1*$t*2),Se=Se.slice(0,-1*$t),Ce=Ce.slice(0,-1*$t)),Ae.push(this.productions_[De[1]][0]),Se.push(dt.$),Ce.push(dt._$),Yt=Me[Ae[Ae.length-2]][Ae[Ae.length-1]],Ae.push(Yt);break;case 3:return!0}}return!0},"parse")},q=(function(){var j={EOF:1,parseError:s(function(Y,Ae){if(this.yy.parser)this.yy.parser.parseError(Y,Ae);else throw new Error(Y)},"parseError"),setInput:s(function(U,Y){return this.yy=Y||this.yy||{},this._input=U,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var U=this._input[0];this.yytext+=U,this.yyleng++,this.offset++,this.match+=U,this.matched+=U;var Y=U.match(/(?:\r\n?|\n).*/g);return Y?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),U},"input"),unput:s(function(U){var Y=U.length,Ae=U.split(/(?:\r\n?|\n)/g);this._input=U+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-Y),this.offset-=Y;var me=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),Ae.length-1&&(this.yylineno-=Ae.length-1);var Se=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:Ae?(Ae.length===me.length?this.yylloc.first_column:0)+me[me.length-Ae.length].length-Ae[0].length:this.yylloc.first_column-Y},this.options.ranges&&(this.yylloc.range=[Se[0],Se[0]+this.yyleng-Y]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(U){this.unput(this.match.slice(U))},"less"),pastInput:s(function(){var U=this.matched.substr(0,this.matched.length-this.match.length);return(U.length>20?"...":"")+U.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var U=this.match;return U.length<20&&(U+=this._input.substr(0,20-U.length)),(U.substr(0,20)+(U.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var U=this.pastInput(),Y=new Array(U.length+1).join("-");return U+this.upcomingInput()+` +`+Y+"^"},"showPosition"),test_match:s(function(U,Y){var Ae,me,Se;if(this.options.backtrack_lexer&&(Se={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(Se.yylloc.range=this.yylloc.range.slice(0))),me=U[0].match(/(?:\r\n?|\n).*/g),me&&(this.yylineno+=me.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:me?me[me.length-1].length-me[me.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+U[0].length},this.yytext+=U[0],this.match+=U[0],this.matches=U,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(U[0].length),this.matched+=U[0],Ae=this.performAction.call(this,this.yy,this,Y,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),Ae)return Ae;if(this._backtrack){for(var Ce in Se)this[Ce]=Se[Ce];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var U,Y,Ae,me;this._more||(this.yytext="",this.match="");for(var Se=this._currentRules(),Ce=0;CeY[0].length)){if(Y=Ae,me=Ce,this.options.backtrack_lexer){if(U=this.test_match(Ae,Se[Ce]),U!==!1)return U;if(this._backtrack){Y=!1;continue}else return!1}else if(!this.options.flex)break}return Y?(U=this.test_match(Y,Se[me]),U!==!1?U:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var Y=this.next();return Y||this.lex()},"lex"),begin:s(function(Y){this.conditionStack.push(Y)},"begin"),popState:s(function(){var Y=this.conditionStack.length-1;return Y>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(Y){return Y=this.conditionStack.length-1-Math.abs(Y||0),Y>=0?this.conditionStack[Y]:"INITIAL"},"topState"),pushState:s(function(Y){this.begin(Y)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(Y,Ae,me,Se){var Ce=Se;switch(me){case 0:break;case 1:break;case 2:return 55;case 3:break;case 4:return this.begin("title"),35;break;case 5:return this.popState(),"title_value";break;case 6:return this.begin("acc_title"),37;break;case 7:return this.popState(),"acc_title_value";break;case 8:return this.begin("acc_descr"),39;break;case 9:return this.popState(),"acc_descr_value";break;case 10:this.begin("acc_descr_multiline");break;case 11:this.popState();break;case 12:return"acc_descr_multiline_value";case 13:return 48;case 14:return 50;case 15:return 49;case 16:return 51;case 17:return 52;case 18:return 53;case 19:return 54;case 20:return 25;case 21:this.begin("md_string");break;case 22:return"MD_STR";case 23:this.popState();break;case 24:this.begin("string");break;case 25:this.popState();break;case 26:return"STR";case 27:this.begin("class_name");break;case 28:return this.popState(),47;break;case 29:return this.begin("point_start"),44;break;case 30:return this.begin("point_x"),45;break;case 31:this.popState();break;case 32:this.popState(),this.begin("point_y");break;case 33:return this.popState(),46;break;case 34:return 28;case 35:return 4;case 36:return 15;case 37:return 11;case 38:return 64;case 39:return 10;case 40:return 65;case 41:return 65;case 42:return 14;case 43:return 13;case 44:return 67;case 45:return 66;case 46:return 12;case 47:return 8;case 48:return 5;case 49:return 18;case 50:return 56;case 51:return 63;case 52:return 57}},"anonymous"),rules:[/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n\r]+)/i,/^(?:%%[^\n]*)/i,/^(?:title\b)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?: *x-axis *)/i,/^(?: *y-axis *)/i,/^(?: *--+> *)/i,/^(?: *quadrant-1 *)/i,/^(?: *quadrant-2 *)/i,/^(?: *quadrant-3 *)/i,/^(?: *quadrant-4 *)/i,/^(?:classDef\b)/i,/^(?:["][`])/i,/^(?:[^`"]+)/i,/^(?:[`]["])/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?::::)/i,/^(?:^\w+)/i,/^(?:\s*:\s*\[\s*)/i,/^(?:(1)|(0(.\d+)?))/i,/^(?:\s*\] *)/i,/^(?:\s*,\s*)/i,/^(?:(1)|(0(.\d+)?))/i,/^(?: *quadrantChart *)/i,/^(?:[A-Za-z]+)/i,/^(?:[^\x00-\x7F]+)/i,/^(?::)/i,/^(?:\+)/i,/^(?:,)/i,/^(?:=)/i,/^(?:=)/i,/^(?:\*)/i,/^(?:#)/i,/^(?:[\_])/i,/^(?:\.)/i,/^(?:&)/i,/^(?:-)/i,/^(?:[0-9]+)/i,/^(?:\s)/i,/^(?:;)/i,/^(?:[!"#$%&'*+,-.`?\\_/])/i,/^(?:$)/i],conditions:{class_name:{rules:[28],inclusive:!1},point_y:{rules:[33],inclusive:!1},point_x:{rules:[32],inclusive:!1},point_start:{rules:[30,31],inclusive:!1},acc_descr_multiline:{rules:[11,12],inclusive:!1},acc_descr:{rules:[9],inclusive:!1},acc_title:{rules:[7],inclusive:!1},title:{rules:[5],inclusive:!1},md_string:{rules:[22,23],inclusive:!1},string:{rules:[25,26],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,6,8,10,13,14,15,16,17,18,19,20,21,24,27,29,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52],inclusive:!0}}};return j})();ee.lexer=q;function ie(){this.yy={}}return s(ie,"Parser"),ie.prototype=ee,ee.Parser=ie,new ie})();ZU.parser=ZU;YIe=ZU});var ro,j_,XIe=F(()=>{"use strict";Wr();Qi();St();Mc();ro=ka(),j_=class{constructor(){this.classes=new Map;this.config=this.getDefaultConfig(),this.themeConfig=this.getDefaultThemeConfig(),this.data=this.getDefaultData()}static{s(this,"QuadrantBuilder")}getDefaultData(){return{titleText:"",quadrant1Text:"",quadrant2Text:"",quadrant3Text:"",quadrant4Text:"",xAxisLeftText:"",xAxisRightText:"",yAxisBottomText:"",yAxisTopText:"",points:[]}}getDefaultConfig(){return{showXAxis:!0,showYAxis:!0,showTitle:!0,chartHeight:mr.quadrantChart?.chartWidth||500,chartWidth:mr.quadrantChart?.chartHeight||500,titlePadding:mr.quadrantChart?.titlePadding||10,titleFontSize:mr.quadrantChart?.titleFontSize||20,quadrantPadding:mr.quadrantChart?.quadrantPadding||5,xAxisLabelPadding:mr.quadrantChart?.xAxisLabelPadding||5,yAxisLabelPadding:mr.quadrantChart?.yAxisLabelPadding||5,xAxisLabelFontSize:mr.quadrantChart?.xAxisLabelFontSize||16,yAxisLabelFontSize:mr.quadrantChart?.yAxisLabelFontSize||16,quadrantLabelFontSize:mr.quadrantChart?.quadrantLabelFontSize||16,quadrantTextTopPadding:mr.quadrantChart?.quadrantTextTopPadding||5,pointTextPadding:mr.quadrantChart?.pointTextPadding||5,pointLabelFontSize:mr.quadrantChart?.pointLabelFontSize||12,pointRadius:mr.quadrantChart?.pointRadius||5,xAxisPosition:mr.quadrantChart?.xAxisPosition||"top",yAxisPosition:mr.quadrantChart?.yAxisPosition||"left",quadrantInternalBorderStrokeWidth:mr.quadrantChart?.quadrantInternalBorderStrokeWidth||1,quadrantExternalBorderStrokeWidth:mr.quadrantChart?.quadrantExternalBorderStrokeWidth||2}}getDefaultThemeConfig(){return{quadrant1Fill:ro.quadrant1Fill,quadrant2Fill:ro.quadrant2Fill,quadrant3Fill:ro.quadrant3Fill,quadrant4Fill:ro.quadrant4Fill,quadrant1TextFill:ro.quadrant1TextFill,quadrant2TextFill:ro.quadrant2TextFill,quadrant3TextFill:ro.quadrant3TextFill,quadrant4TextFill:ro.quadrant4TextFill,quadrantPointFill:ro.quadrantPointFill,quadrantPointTextFill:ro.quadrantPointTextFill,quadrantXAxisTextFill:ro.quadrantXAxisTextFill,quadrantYAxisTextFill:ro.quadrantYAxisTextFill,quadrantTitleFill:ro.quadrantTitleFill,quadrantInternalBorderStrokeFill:ro.quadrantInternalBorderStrokeFill,quadrantExternalBorderStrokeFill:ro.quadrantExternalBorderStrokeFill}}clear(){this.config=this.getDefaultConfig(),this.themeConfig=this.getDefaultThemeConfig(),this.data=this.getDefaultData(),this.classes=new Map,se.info("clear called")}setData(t){this.data={...this.data,...t}}addPoints(t){this.data.points=[...t,...this.data.points]}addClass(t,r){this.classes.set(t,r)}setConfig(t){se.trace("setConfig called with: ",t),this.config={...this.config,...t}}setThemeConfig(t){se.trace("setThemeConfig called with: ",t),this.themeConfig={...this.themeConfig,...t}}calculateSpace(t,r,n,i){let a=this.config.xAxisLabelPadding*2+this.config.xAxisLabelFontSize,o={top:t==="top"&&r?a:0,bottom:t==="bottom"&&r?a:0},l=this.config.yAxisLabelPadding*2+this.config.yAxisLabelFontSize,u={left:this.config.yAxisPosition==="left"&&n?l:0,right:this.config.yAxisPosition==="right"&&n?l:0},h=this.config.titleFontSize+this.config.titlePadding*2,d={top:i?h:0},f=this.config.quadrantPadding+u.left,p=this.config.quadrantPadding+o.top+d.top,m=this.config.chartWidth-this.config.quadrantPadding*2-u.left-u.right,g=this.config.chartHeight-this.config.quadrantPadding*2-o.top-o.bottom-d.top,y=m/2,v=g/2;return{xAxisSpace:o,yAxisSpace:u,titleSpace:d,quadrantSpace:{quadrantLeft:f,quadrantTop:p,quadrantWidth:m,quadrantHalfWidth:y,quadrantHeight:g,quadrantHalfHeight:v}}}getAxisLabels(t,r,n,i){let{quadrantSpace:a,titleSpace:o}=i,{quadrantHalfHeight:l,quadrantHeight:u,quadrantLeft:h,quadrantHalfWidth:d,quadrantTop:f,quadrantWidth:p}=a,m=!!this.data.xAxisRightText,g=!!this.data.yAxisTopText,y=[];return this.data.xAxisLeftText&&r&&y.push({text:this.data.xAxisLeftText,fill:this.themeConfig.quadrantXAxisTextFill,x:h+(m?d/2:0),y:t==="top"?this.config.xAxisLabelPadding+o.top:this.config.xAxisLabelPadding+f+u+this.config.quadrantPadding,fontSize:this.config.xAxisLabelFontSize,verticalPos:m?"center":"left",horizontalPos:"top",rotation:0}),this.data.xAxisRightText&&r&&y.push({text:this.data.xAxisRightText,fill:this.themeConfig.quadrantXAxisTextFill,x:h+d+(m?d/2:0),y:t==="top"?this.config.xAxisLabelPadding+o.top:this.config.xAxisLabelPadding+f+u+this.config.quadrantPadding,fontSize:this.config.xAxisLabelFontSize,verticalPos:m?"center":"left",horizontalPos:"top",rotation:0}),this.data.yAxisBottomText&&n&&y.push({text:this.data.yAxisBottomText,fill:this.themeConfig.quadrantYAxisTextFill,x:this.config.yAxisPosition==="left"?this.config.yAxisLabelPadding:this.config.yAxisLabelPadding+h+p+this.config.quadrantPadding,y:f+u-(g?l/2:0),fontSize:this.config.yAxisLabelFontSize,verticalPos:g?"center":"left",horizontalPos:"top",rotation:-90}),this.data.yAxisTopText&&n&&y.push({text:this.data.yAxisTopText,fill:this.themeConfig.quadrantYAxisTextFill,x:this.config.yAxisPosition==="left"?this.config.yAxisLabelPadding:this.config.yAxisLabelPadding+h+p+this.config.quadrantPadding,y:f+l-(g?l/2:0),fontSize:this.config.yAxisLabelFontSize,verticalPos:g?"center":"left",horizontalPos:"top",rotation:-90}),y}getQuadrants(t){let{quadrantSpace:r}=t,{quadrantHalfHeight:n,quadrantLeft:i,quadrantHalfWidth:a,quadrantTop:o}=r,l=[{text:{text:this.data.quadrant1Text,fill:this.themeConfig.quadrant1TextFill,x:0,y:0,fontSize:this.config.quadrantLabelFontSize,verticalPos:"center",horizontalPos:"middle",rotation:0},x:i+a,y:o,width:a,height:n,fill:this.themeConfig.quadrant1Fill},{text:{text:this.data.quadrant2Text,fill:this.themeConfig.quadrant2TextFill,x:0,y:0,fontSize:this.config.quadrantLabelFontSize,verticalPos:"center",horizontalPos:"middle",rotation:0},x:i,y:o,width:a,height:n,fill:this.themeConfig.quadrant2Fill},{text:{text:this.data.quadrant3Text,fill:this.themeConfig.quadrant3TextFill,x:0,y:0,fontSize:this.config.quadrantLabelFontSize,verticalPos:"center",horizontalPos:"middle",rotation:0},x:i,y:o+n,width:a,height:n,fill:this.themeConfig.quadrant3Fill},{text:{text:this.data.quadrant4Text,fill:this.themeConfig.quadrant4TextFill,x:0,y:0,fontSize:this.config.quadrantLabelFontSize,verticalPos:"center",horizontalPos:"middle",rotation:0},x:i+a,y:o+n,width:a,height:n,fill:this.themeConfig.quadrant4Fill}];for(let u of l)u.text.x=u.x+u.width/2,this.data.points.length===0?(u.text.y=u.y+u.height/2,u.text.horizontalPos="middle"):(u.text.y=u.y+this.config.quadrantTextTopPadding,u.text.horizontalPos="top");return l}getQuadrantPoints(t){let{quadrantSpace:r}=t,{quadrantHeight:n,quadrantLeft:i,quadrantTop:a,quadrantWidth:o}=r,l=Jl().domain([0,1]).range([i,o+i]),u=Jl().domain([0,1]).range([n+a,a]);return this.data.points.map(d=>{let f=this.classes.get(d.className);return f&&(d={...f,...d}),{x:l(d.x),y:u(d.y),fill:d.color??this.themeConfig.quadrantPointFill,radius:d.radius??this.config.pointRadius,text:{text:d.text,fill:this.themeConfig.quadrantPointTextFill,x:l(d.x),y:u(d.y)+this.config.pointTextPadding,verticalPos:"center",horizontalPos:"top",fontSize:this.config.pointLabelFontSize,rotation:0},strokeColor:d.strokeColor??this.themeConfig.quadrantPointFill,strokeWidth:d.strokeWidth??"0px"}})}getBorders(t){let r=this.config.quadrantExternalBorderStrokeWidth/2,{quadrantSpace:n}=t,{quadrantHalfHeight:i,quadrantHeight:a,quadrantLeft:o,quadrantHalfWidth:l,quadrantTop:u,quadrantWidth:h}=n;return[{strokeFill:this.themeConfig.quadrantExternalBorderStrokeFill,strokeWidth:this.config.quadrantExternalBorderStrokeWidth,x1:o-r,y1:u,x2:o+h+r,y2:u},{strokeFill:this.themeConfig.quadrantExternalBorderStrokeFill,strokeWidth:this.config.quadrantExternalBorderStrokeWidth,x1:o+h,y1:u+r,x2:o+h,y2:u+a-r},{strokeFill:this.themeConfig.quadrantExternalBorderStrokeFill,strokeWidth:this.config.quadrantExternalBorderStrokeWidth,x1:o-r,y1:u+a,x2:o+h+r,y2:u+a},{strokeFill:this.themeConfig.quadrantExternalBorderStrokeFill,strokeWidth:this.config.quadrantExternalBorderStrokeWidth,x1:o,y1:u+r,x2:o,y2:u+a-r},{strokeFill:this.themeConfig.quadrantInternalBorderStrokeFill,strokeWidth:this.config.quadrantInternalBorderStrokeWidth,x1:o+l,y1:u+r,x2:o+l,y2:u+a-r},{strokeFill:this.themeConfig.quadrantInternalBorderStrokeFill,strokeWidth:this.config.quadrantInternalBorderStrokeWidth,x1:o+r,y1:u+i,x2:o+h-r,y2:u+i}]}getTitle(t){if(t)return{text:this.data.titleText,fill:this.themeConfig.quadrantTitleFill,fontSize:this.config.titleFontSize,horizontalPos:"top",verticalPos:"center",rotation:0,y:this.config.titlePadding,x:this.config.chartWidth/2}}build(){let t=this.config.showXAxis&&!!(this.data.xAxisLeftText||this.data.xAxisRightText),r=this.config.showYAxis&&!!(this.data.yAxisTopText||this.data.yAxisBottomText),n=this.config.showTitle&&!!this.data.titleText,i=this.data.points.length>0?"bottom":this.config.xAxisPosition,a=this.calculateSpace(i,t,r,n);return{points:this.getQuadrantPoints(a),quadrants:this.getQuadrants(a),axisLabels:this.getAxisLabels(i,t,r,a),borderLines:this.getBorders(a),title:this.getTitle(n)}}}});function QU(e){return!/^#?([\dA-Fa-f]{6}|[\dA-Fa-f]{3})$/.test(e)}function KIe(e){return!/^\d+$/.test(e)}function ZIe(e){return!/^\d+px$/.test(e)}var S0,QIe=F(()=>{"use strict";S0=class extends Error{static{s(this,"InvalidStyleError")}constructor(t,r,n){super(`value for ${t} ${r} is invalid, please use a valid ${n}`),this.name="InvalidStyleError"}};s(QU,"validateHexCode");s(KIe,"validateNumber");s(ZIe,"validateSizeInPixels")});function hd(e){return xr(e.trim(),Pe())}function o5t(e){us.setData({quadrant1Text:hd(e.text)})}function l5t(e){us.setData({quadrant2Text:hd(e.text)})}function c5t(e){us.setData({quadrant3Text:hd(e.text)})}function u5t(e){us.setData({quadrant4Text:hd(e.text)})}function h5t(e){us.setData({xAxisLeftText:hd(e.text)})}function d5t(e){us.setData({xAxisRightText:hd(e.text)})}function f5t(e){us.setData({yAxisTopText:hd(e.text)})}function p5t(e){us.setData({yAxisBottomText:hd(e.text)})}function JU(e){let t={};for(let r of e){let[n,i]=r.trim().split(/\s*:\s*/);if(n==="radius"){if(KIe(i))throw new S0(n,i,"number");t.radius=parseInt(i)}else if(n==="color"){if(QU(i))throw new S0(n,i,"hex code");t.color=i}else if(n==="stroke-color"){if(QU(i))throw new S0(n,i,"hex code");t.strokeColor=i}else if(n==="stroke-width"){if(ZIe(i))throw new S0(n,i,"number of pixels (eg. 10px)");t.strokeWidth=i}else throw new Error(`style named ${n} is not supported.`)}return t}function m5t(e,t,r,n,i){let a=JU(i);us.addPoints([{x:r,y:n,text:hd(e.text),className:t,...a}])}function g5t(e,t){us.addClass(e,JU(t))}function y5t(e){us.setConfig({chartWidth:e})}function v5t(e){us.setConfig({chartHeight:e})}function x5t(){let e=Pe(),{themeVariables:t,quadrantChart:r}=e;return r&&us.setConfig(r),us.setThemeConfig({quadrant1Fill:t.quadrant1Fill,quadrant2Fill:t.quadrant2Fill,quadrant3Fill:t.quadrant3Fill,quadrant4Fill:t.quadrant4Fill,quadrant1TextFill:t.quadrant1TextFill,quadrant2TextFill:t.quadrant2TextFill,quadrant3TextFill:t.quadrant3TextFill,quadrant4TextFill:t.quadrant4TextFill,quadrantPointFill:t.quadrantPointFill,quadrantPointTextFill:t.quadrantPointTextFill,quadrantXAxisTextFill:t.quadrantXAxisTextFill,quadrantYAxisTextFill:t.quadrantYAxisTextFill,quadrantExternalBorderStrokeFill:t.quadrantExternalBorderStrokeFill,quadrantInternalBorderStrokeFill:t.quadrantInternalBorderStrokeFill,quadrantTitleFill:t.quadrantTitleFill}),us.setData({titleText:Pr()}),us.build()}var us,b5t,JIe,e8e=F(()=>{"use strict";Jt();Yr();$n();XIe();QIe();s(hd,"textSanitizer");us=new j_;s(o5t,"setQuadrant1Text");s(l5t,"setQuadrant2Text");s(c5t,"setQuadrant3Text");s(u5t,"setQuadrant4Text");s(h5t,"setXAxisLeftText");s(d5t,"setXAxisRightText");s(f5t,"setYAxisTopText");s(p5t,"setYAxisBottomText");s(JU,"parseStyles");s(m5t,"addPoint");s(g5t,"addClass");s(y5t,"setWidth");s(v5t,"setHeight");s(x5t,"getQuadrantData");b5t=s(function(){us.clear(),Tr()},"clear"),JIe={setWidth:y5t,setHeight:v5t,setQuadrant1Text:o5t,setQuadrant2Text:l5t,setQuadrant3Text:c5t,setQuadrant4Text:u5t,setXAxisLeftText:h5t,setXAxisRightText:d5t,setYAxisTopText:f5t,setYAxisBottomText:p5t,parseStyles:JU,addPoint:m5t,addClass:g5t,getQuadrantData:x5t,clear:b5t,setAccTitle:Lr,getAccTitle:Ir,setDiagramTitle:Gr,getDiagramTitle:Pr,getAccDescription:Nr,setAccDescription:Mr}});var T5t,t8e,r8e=F(()=>{"use strict";Wr();Jt();St();Vn();T5t=s((e,t,r,n)=>{function i(R){return R==="top"?"hanging":"middle"}s(i,"getDominantBaseLine");function a(R){return R==="left"?"start":"middle"}s(a,"getTextAnchor");function o(R){return`translate(${R.x}, ${R.y}) rotate(${R.rotation||0})`}s(o,"getTransformation");let l=Pe();se.debug(`Rendering quadrant chart +`+e);let u=l.securityLevel,h;u==="sandbox"&&(h=ut("#i"+t));let f=(u==="sandbox"?ut(h.nodes()[0].contentDocument.body):ut("body")).select(`[id="${t}"]`),p=f.append("g").attr("class","main"),m=l.quadrantChart?.chartWidth??500,g=l.quadrantChart?.chartHeight??500;jr(f,g,m,l.quadrantChart?.useMaxWidth??!0),f.attr("viewBox","0 0 "+m+" "+g),n.db.setHeight(g),n.db.setWidth(m);let y=n.db.getQuadrantData(),v=p.append("g").attr("class","quadrants"),x=p.append("g").attr("class","border"),b=p.append("g").attr("class","data-points"),T=p.append("g").attr("class","labels"),w=p.append("g").attr("class","title");y.title&&w.append("text").attr("x",0).attr("y",0).attr("fill",y.title.fill).attr("font-size",y.title.fontSize).attr("dominant-baseline",i(y.title.horizontalPos)).attr("text-anchor",a(y.title.verticalPos)).attr("transform",o(y.title)).text(y.title.text),y.borderLines&&x.selectAll("line").data(y.borderLines).enter().append("line").attr("x1",R=>R.x1).attr("y1",R=>R.y1).attr("x2",R=>R.x2).attr("y2",R=>R.y2).style("stroke",R=>R.strokeFill).style("stroke-width",R=>R.strokeWidth);let C=v.selectAll("g.quadrant").data(y.quadrants).enter().append("g").attr("class","quadrant");C.append("rect").attr("x",R=>R.x).attr("y",R=>R.y).attr("width",R=>R.width).attr("height",R=>R.height).attr("fill",R=>R.fill),C.append("text").attr("x",0).attr("y",0).attr("fill",R=>R.text.fill).attr("font-size",R=>R.text.fontSize).attr("dominant-baseline",R=>i(R.text.horizontalPos)).attr("text-anchor",R=>a(R.text.verticalPos)).attr("transform",R=>o(R.text)).text(R=>R.text.text),T.selectAll("g.label").data(y.axisLabels).enter().append("g").attr("class","label").append("text").attr("x",0).attr("y",0).text(R=>R.text).attr("fill",R=>R.fill).attr("font-size",R=>R.fontSize).attr("dominant-baseline",R=>i(R.horizontalPos)).attr("text-anchor",R=>a(R.verticalPos)).attr("transform",R=>o(R));let k=b.selectAll("g.data-point").data(y.points).enter().append("g").attr("class","data-point");k.append("circle").attr("cx",R=>R.x).attr("cy",R=>R.y).attr("r",R=>R.radius).attr("fill",R=>R.fill).attr("stroke",R=>R.strokeColor).attr("stroke-width",R=>R.strokeWidth),k.append("text").attr("x",0).attr("y",0).text(R=>R.text.text).attr("fill",R=>R.text.fill).attr("font-size",R=>R.text.fontSize).attr("dominant-baseline",R=>i(R.text.horizontalPos)).attr("text-anchor",R=>a(R.text.verticalPos)).attr("transform",R=>o(R.text))},"draw"),t8e={draw:T5t}});var n8e={};sr(n8e,{diagram:()=>C5t});var C5t,i8e=F(()=>{"use strict";jIe();e8e();r8e();C5t={parser:YIe,db:JIe,renderer:t8e,styles:s(()=>"","styles")}});var eY,o8e,l8e=F(()=>{"use strict";eY=(function(){var e=s(function(L,B,O,$){for(O=O||{},$=L.length;$--;O[L[$]]=B);return O},"o"),t=[1,10,12,14,16,18,19,21,23],r=[2,6],n=[1,3],i=[1,5],a=[1,6],o=[1,7],l=[1,5,10,12,14,16,18,19,21,23,36,37,38],u=[1,25],h=[1,26],d=[1,28],f=[1,29],p=[1,30],m=[1,31],g=[1,32],y=[1,33],v=[1,34],x=[1,35],b=[1,36],T=[1,37],w=[1,43],C=[1,42],S=[1,47],k=[1,50],R=[1,10,12,14,16,18,19,21,23,36,37,38],M=[1,10,12,14,16,18,19,21,23,24,26,28,29,36,37,38],P=[1,10,12,14,16,18,19,21,23,24,26,28,29,36,37,38,42,43,44,45,46,47,48,49,50,51],N=[1,65],_=[26,28],A={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,eol:4,XYCHART:5,chartConfig:6,document:7,CHART_ORIENTATION:8,statement:9,title:10,text:11,X_AXIS:12,parseXAxis:13,Y_AXIS:14,parseYAxis:15,LINE:16,plotData:17,BAR:18,acc_title:19,acc_title_value:20,acc_descr:21,acc_descr_value:22,acc_descr_multiline_value:23,SQUARE_BRACES_START:24,dataPoints:25,SQUARE_BRACES_END:26,dataPoint:27,COMMA:28,NUMBER_WITH_DECIMAL:29,STR:30,xAxisData:31,bandData:32,ARROW_DELIMITER:33,commaSeparatedTexts:34,yAxisData:35,NEWLINE:36,SEMI:37,EOF:38,alphaNum:39,MD_STR:40,alphaNumToken:41,AMP:42,NUM:43,ALPHA:44,PLUS:45,EQUALS:46,MULT:47,DOT:48,BRKT:49,MINUS:50,UNDERSCORE:51,$accept:0,$end:1},terminals_:{2:"error",5:"XYCHART",8:"CHART_ORIENTATION",10:"title",12:"X_AXIS",14:"Y_AXIS",16:"LINE",18:"BAR",19:"acc_title",20:"acc_title_value",21:"acc_descr",22:"acc_descr_value",23:"acc_descr_multiline_value",24:"SQUARE_BRACES_START",26:"SQUARE_BRACES_END",28:"COMMA",29:"NUMBER_WITH_DECIMAL",30:"STR",33:"ARROW_DELIMITER",36:"NEWLINE",37:"SEMI",38:"EOF",40:"MD_STR",42:"AMP",43:"NUM",44:"ALPHA",45:"PLUS",46:"EQUALS",47:"MULT",48:"DOT",49:"BRKT",50:"MINUS",51:"UNDERSCORE"},productions_:[0,[3,2],[3,3],[3,2],[3,1],[6,1],[7,0],[7,2],[9,2],[9,2],[9,2],[9,2],[9,2],[9,3],[9,2],[9,3],[9,2],[9,2],[9,1],[17,3],[25,3],[25,1],[27,2],[27,1],[13,1],[13,2],[13,1],[31,1],[31,3],[32,3],[34,3],[34,1],[15,1],[15,2],[15,1],[35,3],[4,1],[4,1],[4,1],[11,1],[11,1],[11,1],[39,1],[39,2],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1]],performAction:s(function(B,O,$,W,V,z,H){var K=z.length-1;switch(V){case 5:W.setOrientation(z[K]);break;case 9:W.setDiagramTitle(z[K].text.trim());break;case 12:W.setLineData({text:"",type:"text"},z[K]);break;case 13:W.setLineData(z[K-1],z[K]);break;case 14:W.setBarData({text:"",type:"text"},z[K]);break;case 15:W.setBarData(z[K-1],z[K]);break;case 16:this.$=z[K].trim(),W.setAccTitle(this.$);break;case 17:case 18:this.$=z[K].trim(),W.setAccDescription(this.$);break;case 19:this.$=z[K-1];break;case 20:case 30:this.$=[z[K-2],...z[K]];break;case 21:case 31:this.$=[z[K]];break;case 22:this.$={value:Number(z[K-1]),label:z[K]};break;case 23:this.$={value:Number(z[K]),label:""};break;case 24:W.setXAxisTitle(z[K]);break;case 25:W.setXAxisTitle(z[K-1]);break;case 26:W.setXAxisTitle({type:"text",text:""});break;case 27:W.setXAxisBand(z[K]);break;case 28:W.setXAxisRangeData(Number(z[K-2]),Number(z[K]));break;case 29:this.$=z[K-1];break;case 32:W.setYAxisTitle(z[K]);break;case 33:W.setYAxisTitle(z[K-1]);break;case 34:W.setYAxisTitle({type:"text",text:""});break;case 35:W.setYAxisRangeData(Number(z[K-2]),Number(z[K]));break;case 39:this.$={text:z[K],type:"text"};break;case 40:this.$={text:z[K],type:"text"};break;case 41:this.$={text:z[K],type:"markdown"};break;case 42:this.$=z[K];break;case 43:this.$=z[K-1]+""+z[K];break}},"anonymous"),table:[e(t,r,{3:1,4:2,7:4,5:n,36:i,37:a,38:o}),{1:[3]},e(t,r,{4:2,7:4,3:8,5:n,36:i,37:a,38:o}),e(t,r,{4:2,7:4,6:9,3:10,5:n,8:[1,11],36:i,37:a,38:o}),{1:[2,4],9:12,10:[1,13],12:[1,14],14:[1,15],16:[1,16],18:[1,17],19:[1,18],21:[1,19],23:[1,20]},e(l,[2,36]),e(l,[2,37]),e(l,[2,38]),{1:[2,1]},e(t,r,{4:2,7:4,3:21,5:n,36:i,37:a,38:o}),{1:[2,3]},e(l,[2,5]),e(t,[2,7],{4:22,36:i,37:a,38:o}),{11:23,30:u,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{11:39,13:38,24:w,29:C,30:u,31:40,32:41,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{11:45,15:44,29:S,30:u,35:46,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{11:49,17:48,24:k,30:u,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{11:52,17:51,24:k,30:u,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{20:[1,53]},{22:[1,54]},e(R,[2,18]),{1:[2,2]},e(R,[2,8]),e(R,[2,9]),e(M,[2,39],{41:55,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T}),e(M,[2,40]),e(M,[2,41]),e(P,[2,42]),e(P,[2,44]),e(P,[2,45]),e(P,[2,46]),e(P,[2,47]),e(P,[2,48]),e(P,[2,49]),e(P,[2,50]),e(P,[2,51]),e(P,[2,52]),e(P,[2,53]),e(R,[2,10]),e(R,[2,24],{32:41,31:56,24:w,29:C}),e(R,[2,26]),e(R,[2,27]),{33:[1,57]},{11:59,30:u,34:58,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},e(R,[2,11]),e(R,[2,32],{35:60,29:S}),e(R,[2,34]),{33:[1,61]},e(R,[2,12]),{17:62,24:k},{25:63,27:64,29:N},e(R,[2,14]),{17:66,24:k},e(R,[2,16]),e(R,[2,17]),e(P,[2,43]),e(R,[2,25]),{29:[1,67]},{26:[1,68]},{26:[2,31],28:[1,69]},e(R,[2,33]),{29:[1,70]},e(R,[2,13]),{26:[1,71]},{26:[2,21],28:[1,72]},e(_,[2,23],{30:[1,73]}),e(R,[2,15]),e(R,[2,28]),e(R,[2,29]),{11:59,30:u,34:74,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},e(R,[2,35]),e(R,[2,19]),{25:75,27:64,29:N},e(_,[2,22]),{26:[2,30]},{26:[2,20]}],defaultActions:{8:[2,1],10:[2,3],21:[2,2],74:[2,30],75:[2,20]},parseError:s(function(B,O){if(O.recoverable)this.trace(B);else{var $=new Error(B);throw $.hash=O,$}},"parseError"),parse:s(function(B){var O=this,$=[0],W=[],V=[null],z=[],H=this.table,K="",J=0,ne=0,Z=0,pe=2,te=1,fe=z.slice.call(arguments,1),ce=Object.create(this.lexer),de={yy:{}};for(var Ee in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Ee)&&(de.yy[Ee]=this.yy[Ee]);ce.setInput(B,de.yy),de.yy.lexer=ce,de.yy.parser=this,typeof ce.yylloc>"u"&&(ce.yylloc={});var we=ce.yylloc;z.push(we);var Fe=ce.options&&ce.options.ranges;typeof de.yy.parseError=="function"?this.parseError=de.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Ye(ee){$.length=$.length-2*ee,V.length=V.length-ee,z.length=z.length-ee}s(Ye,"popStack");function He(){var ee;return ee=W.pop()||ce.lex()||te,typeof ee!="number"&&(ee instanceof Array&&(W=ee,ee=W.pop()),ee=O.symbols_[ee]||ee),ee}s(He,"lex");for(var ye,Be,Ie,Ne,re,ue,le={},he,Te,oe,G;;){if(Ie=$[$.length-1],this.defaultActions[Ie]?Ne=this.defaultActions[Ie]:((ye===null||typeof ye>"u")&&(ye=He()),Ne=H[Ie]&&H[Ie][ye]),typeof Ne>"u"||!Ne.length||!Ne[0]){var X="";G=[];for(he in H[Ie])this.terminals_[he]&&he>pe&&G.push("'"+this.terminals_[he]+"'");ce.showPosition?X="Parse error on line "+(J+1)+`: +`+ce.showPosition()+` +Expecting `+G.join(", ")+", got '"+(this.terminals_[ye]||ye)+"'":X="Parse error on line "+(J+1)+": Unexpected "+(ye==te?"end of input":"'"+(this.terminals_[ye]||ye)+"'"),this.parseError(X,{text:ce.match,token:this.terminals_[ye]||ye,line:ce.yylineno,loc:we,expected:G})}if(Ne[0]instanceof Array&&Ne.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Ie+", token: "+ye);switch(Ne[0]){case 1:$.push(ye),V.push(ce.yytext),z.push(ce.yylloc),$.push(Ne[1]),ye=null,Be?(ye=Be,Be=null):(ne=ce.yyleng,K=ce.yytext,J=ce.yylineno,we=ce.yylloc,Z>0&&Z--);break;case 2:if(Te=this.productions_[Ne[1]][1],le.$=V[V.length-Te],le._$={first_line:z[z.length-(Te||1)].first_line,last_line:z[z.length-1].last_line,first_column:z[z.length-(Te||1)].first_column,last_column:z[z.length-1].last_column},Fe&&(le._$.range=[z[z.length-(Te||1)].range[0],z[z.length-1].range[1]]),ue=this.performAction.apply(le,[K,ne,J,de.yy,Ne[1],V,z].concat(fe)),typeof ue<"u")return ue;Te&&($=$.slice(0,-1*Te*2),V=V.slice(0,-1*Te),z=z.slice(0,-1*Te)),$.push(this.productions_[Ne[1]][0]),V.push(le.$),z.push(le._$),oe=H[$[$.length-2]][$[$.length-1]],$.push(oe);break;case 3:return!0}}return!0},"parse")},I=(function(){var L={EOF:1,parseError:s(function(O,$){if(this.yy.parser)this.yy.parser.parseError(O,$);else throw new Error(O)},"parseError"),setInput:s(function(B,O){return this.yy=O||this.yy||{},this._input=B,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var B=this._input[0];this.yytext+=B,this.yyleng++,this.offset++,this.match+=B,this.matched+=B;var O=B.match(/(?:\r\n?|\n).*/g);return O?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),B},"input"),unput:s(function(B){var O=B.length,$=B.split(/(?:\r\n?|\n)/g);this._input=B+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-O),this.offset-=O;var W=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),$.length-1&&(this.yylineno-=$.length-1);var V=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:$?($.length===W.length?this.yylloc.first_column:0)+W[W.length-$.length].length-$[0].length:this.yylloc.first_column-O},this.options.ranges&&(this.yylloc.range=[V[0],V[0]+this.yyleng-O]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(B){this.unput(this.match.slice(B))},"less"),pastInput:s(function(){var B=this.matched.substr(0,this.matched.length-this.match.length);return(B.length>20?"...":"")+B.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var B=this.match;return B.length<20&&(B+=this._input.substr(0,20-B.length)),(B.substr(0,20)+(B.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var B=this.pastInput(),O=new Array(B.length+1).join("-");return B+this.upcomingInput()+` +`+O+"^"},"showPosition"),test_match:s(function(B,O){var $,W,V;if(this.options.backtrack_lexer&&(V={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(V.yylloc.range=this.yylloc.range.slice(0))),W=B[0].match(/(?:\r\n?|\n).*/g),W&&(this.yylineno+=W.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:W?W[W.length-1].length-W[W.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+B[0].length},this.yytext+=B[0],this.match+=B[0],this.matches=B,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(B[0].length),this.matched+=B[0],$=this.performAction.call(this,this.yy,this,O,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),$)return $;if(this._backtrack){for(var z in V)this[z]=V[z];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var B,O,$,W;this._more||(this.yytext="",this.match="");for(var V=this._currentRules(),z=0;zO[0].length)){if(O=$,W=z,this.options.backtrack_lexer){if(B=this.test_match($,V[z]),B!==!1)return B;if(this._backtrack){O=!1;continue}else return!1}else if(!this.options.flex)break}return O?(B=this.test_match(O,V[W]),B!==!1?B:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var O=this.next();return O||this.lex()},"lex"),begin:s(function(O){this.conditionStack.push(O)},"begin"),popState:s(function(){var O=this.conditionStack.length-1;return O>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(O){return O=this.conditionStack.length-1-Math.abs(O||0),O>=0?this.conditionStack[O]:"INITIAL"},"topState"),pushState:s(function(O){this.begin(O)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(O,$,W,V){var z=V;switch(W){case 0:break;case 1:break;case 2:return this.popState(),36;break;case 3:return this.popState(),36;break;case 4:return 36;case 5:break;case 6:return 10;case 7:return this.pushState("acc_title"),19;break;case 8:return this.popState(),"acc_title_value";break;case 9:return this.pushState("acc_descr"),21;break;case 10:return this.popState(),"acc_descr_value";break;case 11:this.pushState("acc_descr_multiline");break;case 12:this.popState();break;case 13:return"acc_descr_multiline_value";case 14:return 5;case 15:return 5;case 16:return 8;case 17:return this.pushState("axis_data"),"X_AXIS";break;case 18:return this.pushState("axis_data"),"Y_AXIS";break;case 19:return this.pushState("axis_band_data"),24;break;case 20:return 33;case 21:return this.pushState("data"),16;break;case 22:return this.pushState("data"),18;break;case 23:return this.pushState("data_inner"),24;break;case 24:return 29;case 25:return this.popState(),26;break;case 26:this.popState();break;case 27:this.pushState("string");break;case 28:this.popState();break;case 29:return"STR";case 30:return 24;case 31:return 26;case 32:return 44;case 33:return"COLON";case 34:return 45;case 35:return 28;case 36:return 46;case 37:return 47;case 38:return 49;case 39:return 51;case 40:return 48;case 41:return 42;case 42:return 50;case 43:return 43;case 44:break;case 45:return 37;case 46:return 38}},"anonymous"),rules:[/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:(\r?\n))/i,/^(?:(\r?\n))/i,/^(?:[\n\r]+)/i,/^(?:%%[^\n]*)/i,/^(?:title\b)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:\})/i,/^(?:[^\}]*)/i,/^(?:xychart-beta\b)/i,/^(?:xychart\b)/i,/^(?:(?:vertical|horizontal))/i,/^(?:x-axis\b)/i,/^(?:y-axis\b)/i,/^(?:\[)/i,/^(?:-->)/i,/^(?:line\b)/i,/^(?:bar\b)/i,/^(?:\[)/i,/^(?:[+-]?(?:\d+(?:\.\d+)?|\.\d+))/i,/^(?:\])/i,/^(?:(?:`\) \{ this\.pushState\(md_string\); \}\n\(\?:\(\?!`"\)\.\)\+ \{ return MD_STR; \}\n\(\?:`))/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:\[)/i,/^(?:\])/i,/^(?:[A-Za-z]+)/i,/^(?::)/i,/^(?:\+)/i,/^(?:,)/i,/^(?:=)/i,/^(?:\*)/i,/^(?:#)/i,/^(?:[\_])/i,/^(?:\.)/i,/^(?:&)/i,/^(?:-)/i,/^(?:[0-9]+)/i,/^(?:\s+)/i,/^(?:;)/i,/^(?:$)/i],conditions:{data_inner:{rules:[0,1,4,5,6,7,9,11,14,15,16,17,18,21,22,24,25,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0},data:{rules:[0,1,3,4,5,6,7,9,11,14,15,16,17,18,21,22,23,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0},axis_band_data:{rules:[0,1,4,5,6,7,9,11,14,15,16,17,18,21,22,25,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0},axis_data:{rules:[0,1,2,4,5,6,7,9,11,14,15,16,17,18,19,20,21,22,24,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0},acc_descr_multiline:{rules:[12,13],inclusive:!1},acc_descr:{rules:[10],inclusive:!1},acc_title:{rules:[8],inclusive:!1},title:{rules:[],inclusive:!1},md_string:{rules:[],inclusive:!1},string:{rules:[28,29],inclusive:!1},INITIAL:{rules:[0,1,4,5,6,7,9,11,14,15,16,17,18,21,22,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0}}};return L})();A.lexer=I;function D(){this.yy={}}return s(D,"Parser"),D.prototype=A,A.Parser=D,new D})();eY.parser=eY;o8e=eY});function MS(e){return e.type==="bar"}function NS(e){return e.type==="band"}function Xv(e){return e.type==="linear"}var PS=F(()=>{"use strict";s(MS,"isBarPlot");s(NS,"isBandAxisData");s(Xv,"isLinearAxisData")});var Ap,X_=F(()=>{"use strict";qo();Ap=class{constructor(t){this.parentGroup=t}static{s(this,"TextDimensionCalculatorWithFont")}getMaxDimension(t,r){if(!this.parentGroup)return{width:t.reduce((a,o)=>Math.max(o.length,a),0)*r,height:r};let n={width:0,height:0},i=this.parentGroup.append("g").attr("visibility","hidden").attr("font-size",r);for(let a of t){let o=Zae(i,1,a),l=o?o.width:a.length*r,u=o?o.height:r;n.width=Math.max(n.width,l),n.height=Math.max(n.height,u)}return i.remove(),n}}});var Kv,tY=F(()=>{"use strict";Kv=class{constructor(t,r,n,i){this.axisConfig=t;this.title=r;this.textDimensionCalculator=n;this.axisThemeConfig=i;this.boundingRect={x:0,y:0,width:0,height:0};this.axisPosition="left";this.showTitle=!1;this.showLabel=!1;this.showTick=!1;this.showAxisLine=!1;this.outerPadding=0;this.titleTextHeight=0;this.labelTextHeight=0;this.normalizedLabelRotationInRad=0;this.range=[0,10],this.boundingRect={x:0,y:0,width:0,height:0},this.axisPosition="left",this.normalizedLabelRotationInRad=this.axisConfig.labelRotation>=-90&&this.axisConfig.labelRotation<=90?this.axisConfig.labelRotation*Math.PI/180:0}static{s(this,"BaseAxis")}setRange(t){this.range=t,this.axisPosition==="left"||this.axisPosition==="right"?this.boundingRect.height=t[1]-t[0]:this.boundingRect.width=t[1]-t[0],this.recalculateScale()}getRange(){return[this.range[0]+this.outerPadding,this.range[1]-this.outerPadding]}setAxisPosition(t){this.axisPosition=t,this.setRange(this.range)}getTickDistance(){let t=this.getRange();return Math.abs(t[0]-t[1])/this.getTickValues().length}getAxisOuterPadding(){return this.outerPadding}getLabelDimension(){return this.textDimensionCalculator.getMaxDimension(this.getTickValues().map(t=>t.toString()),this.axisConfig.labelFontSize)}recalculateOuterPaddingToDrawBar(){.7*this.getTickDistance()>this.outerPadding*2&&(this.outerPadding=Math.floor(.7*this.getTickDistance()/2)),this.recalculateScale()}calculateSpaceIfDrawnHorizontally(t){let r=t.height;if(this.axisConfig.showAxisLine&&r>this.axisConfig.axisLineWidth&&(r-=this.axisConfig.axisLineWidth,this.showAxisLine=!0),this.axisConfig.showLabel){let n=this.getLabelDimension(),i=.2*t.width;this.outerPadding=Math.min(n.width/2,i);let a=n.height;this.axisPosition==="bottom"&&this.normalizedLabelRotationInRad!==0&&(a=Math.max(a,Math.abs(Math.sin(this.normalizedLabelRotationInRad)*n.width)+Math.abs(Math.cos(this.normalizedLabelRotationInRad)*n.height))),a+=this.axisConfig.labelPadding*2,this.labelTextHeight=n.height,a<=r&&(r-=a,this.showLabel=!0)}if(this.axisConfig.showTick&&r>=this.axisConfig.tickLength&&(this.showTick=!0,r-=this.axisConfig.tickLength),this.axisConfig.showTitle&&this.title){let n=this.textDimensionCalculator.getMaxDimension([this.title],this.axisConfig.titleFontSize),i=n.height+this.axisConfig.titlePadding*2;this.titleTextHeight=n.height,i<=r&&(r-=i,this.showTitle=!0)}this.boundingRect.width=t.width,this.boundingRect.height=t.height-r}calculateSpaceIfDrawnVertical(t){let r=t.width;if(this.axisConfig.showAxisLine&&r>this.axisConfig.axisLineWidth&&(r-=this.axisConfig.axisLineWidth,this.showAxisLine=!0),this.axisConfig.showLabel){let n=this.getLabelDimension(),i=.2*t.height;this.outerPadding=Math.min(n.height/2,i);let a=n.width+this.axisConfig.labelPadding*2;a<=r&&(r-=a,this.showLabel=!0)}if(this.axisConfig.showTick&&r>=this.axisConfig.tickLength&&(this.showTick=!0,r-=this.axisConfig.tickLength),this.axisConfig.showTitle&&this.title){let n=this.textDimensionCalculator.getMaxDimension([this.title],this.axisConfig.titleFontSize),i=n.height+this.axisConfig.titlePadding*2;this.titleTextHeight=n.height,i<=r&&(r-=i,this.showTitle=!0)}this.boundingRect.width=t.width-r,this.boundingRect.height=t.height}calculateSpace(t){return this.axisPosition==="left"||this.axisPosition==="right"?this.calculateSpaceIfDrawnVertical(t):this.calculateSpaceIfDrawnHorizontally(t),this.recalculateScale(),{width:this.boundingRect.width,height:this.boundingRect.height}}setBoundingBoxXY(t){this.boundingRect.x=t.x,this.boundingRect.y=t.y}calculateOffsetByRotation(t){let r=this.normalizedLabelRotationInRad;return r===0?0:Math.sin(r)*this.getLabelDimension()[t]/2}getDrawableElementsForLeftAxis(){let t=[];if(this.showAxisLine){let r=this.boundingRect.x+this.boundingRect.width-this.axisConfig.axisLineWidth/2;t.push({type:"path",groupTexts:["left-axis","axisl-line"],data:[{path:`M ${r},${this.boundingRect.y} L ${r},${this.boundingRect.y+this.boundingRect.height} `,strokeFill:this.axisThemeConfig.axisLineColor,strokeWidth:this.axisConfig.axisLineWidth}]})}if(this.showLabel&&t.push({type:"text",groupTexts:["left-axis","label"],data:this.getTickValues().map(r=>({text:r.toString(),x:this.boundingRect.x+this.boundingRect.width-(this.showLabel?this.axisConfig.labelPadding:0)-(this.showTick?this.axisConfig.tickLength:0)-(this.showAxisLine?this.axisConfig.axisLineWidth:0),y:this.getScaleValue(r),fill:this.axisThemeConfig.labelColor,fontSize:this.axisConfig.labelFontSize,rotation:0,verticalPos:"middle",horizontalPos:"right"}))}),this.showTick){let r=this.boundingRect.x+this.boundingRect.width-(this.showAxisLine?this.axisConfig.axisLineWidth:0);t.push({type:"path",groupTexts:["left-axis","ticks"],data:this.getTickValues().map(n=>({path:`M ${r},${this.getScaleValue(n)} L ${r-this.axisConfig.tickLength},${this.getScaleValue(n)}`,strokeFill:this.axisThemeConfig.tickColor,strokeWidth:this.axisConfig.tickWidth}))})}return this.showTitle&&t.push({type:"text",groupTexts:["left-axis","title"],data:[{text:this.title,x:this.boundingRect.x+this.axisConfig.titlePadding,y:this.boundingRect.y+this.boundingRect.height/2,fill:this.axisThemeConfig.titleColor,fontSize:this.axisConfig.titleFontSize,rotation:270,verticalPos:"top",horizontalPos:"center"}]}),t}getDrawableElementsForBottomAxis(){let t=[];if(this.showAxisLine){let r=this.boundingRect.y+this.axisConfig.axisLineWidth/2;t.push({type:"path",groupTexts:["bottom-axis","axis-line"],data:[{path:`M ${this.boundingRect.x},${r} L ${this.boundingRect.x+this.boundingRect.width},${r}`,strokeFill:this.axisThemeConfig.axisLineColor,strokeWidth:this.axisConfig.axisLineWidth}]})}if(this.showLabel&&t.push({type:"text",groupTexts:["bottom-axis","label"],data:this.getTickValues().map(r=>({text:r.toString(),x:this.getScaleValue(r)+this.calculateOffsetByRotation("height"),y:this.boundingRect.y+this.axisConfig.labelPadding+(this.showTick?this.axisConfig.tickLength:0)+(this.showAxisLine?this.axisConfig.axisLineWidth:0)+Math.abs(this.calculateOffsetByRotation("width")),fill:this.axisThemeConfig.labelColor,fontSize:this.axisConfig.labelFontSize,rotation:this.normalizedLabelRotationInRad*180/Math.PI,verticalPos:"top",horizontalPos:"center"}))}),this.showTick){let r=this.boundingRect.y+(this.showAxisLine?this.axisConfig.axisLineWidth:0);t.push({type:"path",groupTexts:["bottom-axis","ticks"],data:this.getTickValues().map(n=>({path:`M ${this.getScaleValue(n)},${r} L ${this.getScaleValue(n)},${r+this.axisConfig.tickLength}`,strokeFill:this.axisThemeConfig.tickColor,strokeWidth:this.axisConfig.tickWidth}))})}return this.showTitle&&t.push({type:"text",groupTexts:["bottom-axis","title"],data:[{text:this.title,x:this.range[0]+(this.range[1]-this.range[0])/2,y:this.boundingRect.y+this.boundingRect.height-this.axisConfig.titlePadding-this.titleTextHeight,fill:this.axisThemeConfig.titleColor,fontSize:this.axisConfig.titleFontSize,rotation:0,verticalPos:"top",horizontalPos:"center"}]}),t}getDrawableElementsForTopAxis(){let t=[];if(this.showAxisLine){let r=this.boundingRect.y+this.boundingRect.height-this.axisConfig.axisLineWidth/2;t.push({type:"path",groupTexts:["top-axis","axis-line"],data:[{path:`M ${this.boundingRect.x},${r} L ${this.boundingRect.x+this.boundingRect.width},${r}`,strokeFill:this.axisThemeConfig.axisLineColor,strokeWidth:this.axisConfig.axisLineWidth}]})}if(this.showLabel&&t.push({type:"text",groupTexts:["top-axis","label"],data:this.getTickValues().map(r=>({text:r.toString(),x:this.getScaleValue(r),y:this.boundingRect.y+(this.showTitle?this.titleTextHeight+this.axisConfig.titlePadding*2:0)+this.axisConfig.labelPadding,fill:this.axisThemeConfig.labelColor,fontSize:this.axisConfig.labelFontSize,rotation:0,verticalPos:"top",horizontalPos:"center"}))}),this.showTick){let r=this.boundingRect.y;t.push({type:"path",groupTexts:["top-axis","ticks"],data:this.getTickValues().map(n=>({path:`M ${this.getScaleValue(n)},${r+this.boundingRect.height-(this.showAxisLine?this.axisConfig.axisLineWidth:0)} L ${this.getScaleValue(n)},${r+this.boundingRect.height-this.axisConfig.tickLength-(this.showAxisLine?this.axisConfig.axisLineWidth:0)}`,strokeFill:this.axisThemeConfig.tickColor,strokeWidth:this.axisConfig.tickWidth}))})}return this.showTitle&&t.push({type:"text",groupTexts:["top-axis","title"],data:[{text:this.title,x:this.boundingRect.x+this.boundingRect.width/2,y:this.boundingRect.y+this.axisConfig.titlePadding,fill:this.axisThemeConfig.titleColor,fontSize:this.axisConfig.titleFontSize,rotation:0,verticalPos:"top",horizontalPos:"center"}]}),t}getDrawableElements(){if(this.axisPosition==="left")return this.getDrawableElementsForLeftAxis();if(this.axisPosition==="right")throw Error("Drawing of right axis is not implemented");return this.axisPosition==="bottom"?this.getDrawableElementsForBottomAxis():this.axisPosition==="top"?this.getDrawableElementsForTopAxis():[]}}});var K_,c8e=F(()=>{"use strict";Wr();St();tY();K_=class extends Kv{static{s(this,"BandAxis")}constructor(t,r,n,i,a){super(t,i,a,r),this.categories=n,this.scale=xy().domain(this.categories).range(this.getRange())}setRange(t){super.setRange(t)}recalculateScale(){this.scale=xy().domain(this.categories).range(this.getRange()).paddingInner(1).paddingOuter(0).align(.5),se.trace("BandAxis axis final categories, range: ",this.categories,this.getRange())}getTickValues(){return this.categories}getScaleValue(t){return this.scale(t)??this.getRange()[0]}}});var Z_,u8e=F(()=>{"use strict";Wr();tY();Z_=class extends Kv{static{s(this,"LinearAxis")}constructor(t,r,n,i,a){super(t,i,a,r),this.domain=n,this.scale=Jl().domain(this.domain).range(this.getRange())}getTickValues(){return this.scale.ticks()}recalculateScale(){let t=[...this.domain];this.axisPosition==="left"&&t.reverse(),this.scale=Jl().domain(t).range(this.getRange())}getScaleValue(t){return this.scale(t)}}});function rY(e,t,r,n){let i=new Ap(n);return NS(e)?new K_(t,r,e.categories,e.title,i):new Z_(t,r,[e.min,e.max],e.title,i)}var h8e=F(()=>{"use strict";PS();X_();c8e();u8e();s(rY,"getAxis")});function d8e(e,t,r,n){let i=new Ap(n);return new nY(i,e,t,r)}var nY,f8e=F(()=>{"use strict";X_();nY=class{constructor(t,r,n,i){this.textDimensionCalculator=t;this.chartConfig=r;this.chartData=n;this.chartThemeConfig=i;this.boundingRect={x:0,y:0,width:0,height:0},this.showChartTitle=!1}static{s(this,"ChartTitle")}setBoundingBoxXY(t){this.boundingRect.x=t.x,this.boundingRect.y=t.y}calculateSpace(t){let r=this.textDimensionCalculator.getMaxDimension([this.chartData.title],this.chartConfig.titleFontSize),n=Math.max(r.width,t.width),i=r.height+2*this.chartConfig.titlePadding;return r.width<=n&&r.height<=i&&this.chartConfig.showTitle&&this.chartData.title&&(this.boundingRect.width=n,this.boundingRect.height=i,this.showChartTitle=!0),{width:this.boundingRect.width,height:this.boundingRect.height}}getDrawableElements(){let t=[];return this.showChartTitle&&t.push({groupTexts:["chart-title"],type:"text",data:[{fontSize:this.chartConfig.titleFontSize,text:this.chartData.title,verticalPos:"middle",horizontalPos:"center",x:this.boundingRect.x+this.boundingRect.width/2,y:this.boundingRect.y+this.boundingRect.height/2,fill:this.chartThemeConfig.titleColor,rotation:0}]}),t}};s(d8e,"getChartTitleComponent")});function p8e(e){return{fontSize:e,markerSize:e*E5t,markerSpacing:e*R5t,itemSpacing:e*A5t}}function m8e(e,t,r,n){let i=new Ap(n);return new iY(i,e,t,r)}var E5t,A5t,R5t,iY,g8e=F(()=>{"use strict";PS();X_();E5t=.75,A5t=.5,R5t=.35;s(p8e,"getLegendLayout");iY=class{constructor(t,r,n,i){this.textDimensionCalculator=t;this.chartConfig=r;this.chartData=n;this.chartThemeConfig=i;this.boundingRect={x:0,y:0,width:0,height:0};this.visiblePlots=[]}static{s(this,"ChartLegend")}setBoundingBoxXY(t){this.boundingRect.x=t.x,this.boundingRect.y=t.y}calculateSpace(t){if(this.visiblePlots=this.chartConfig.showLegend?this.chartData.plots.filter(h=>h.title):[],this.visiblePlots.length===0)return this.boundingRect.width=0,this.boundingRect.height=0,{width:0,height:0};let{fontSize:r,markerSize:n,markerSpacing:i,itemSpacing:a}=p8e(this.chartConfig.legendFontSize),o=this.textDimensionCalculator.getMaxDimension(this.visiblePlots.map(h=>h.title),r),l=this.chartConfig.legendPadding*2+n+i+o.width,u=this.chartConfig.legendPadding*2+this.visiblePlots.length*r+(this.visiblePlots.length-1)*a;return l<=t.width&&u<=t.height?(this.boundingRect.width=l,this.boundingRect.height=u):(this.visiblePlots=[],this.boundingRect.width=0,this.boundingRect.height=0),{width:this.boundingRect.width,height:this.boundingRect.height}}getDrawableElements(){if(this.visiblePlots.length===0)return[];let{fontSize:t,markerSize:r,markerSpacing:n,itemSpacing:i}=p8e(this.chartConfig.legendFontSize),a=t+i,o=this.boundingRect.x+this.chartConfig.legendPadding,l=this.boundingRect.y+this.chartConfig.legendPadding,u=[],h=[];for(let[d,f]of this.visiblePlots.entries())if(MS(f))u.push({x:o,y:l+d*a,width:r,height:r,fill:f.fill,strokeFill:f.fill,strokeWidth:0});else{let p=l+d*a+r/2;h.push({path:`M ${o},${p} L ${o+r},${p}`,strokeFill:f.strokeFill,strokeWidth:f.strokeWidth})}return[{groupTexts:["legend","markers"],type:"rect",data:u},{groupTexts:["legend","markers"],type:"path",data:h},{groupTexts:["legend","label"],type:"text",data:this.visiblePlots.map((d,f)=>({text:d.title,x:o+r+n,y:l+f*a+r/2,fill:this.chartThemeConfig.legendTextColor,fontSize:t,rotation:0,verticalPos:"middle",horizontalPos:"left"}))}]}};s(m8e,"getChartLegendComponent")});var Q_,y8e=F(()=>{"use strict";Wr();Q_=class{constructor(t,r,n,i,a){this.plotData=t;this.xAxis=r;this.yAxis=n;this.orientation=i;this.plotIndex=a}static{s(this,"LinePlot")}getDrawableElement(){let t=this.plotData.data.map(i=>[this.xAxis.getScaleValue(i[0]),this.yAxis.getScaleValue(i[1])]),r;if(this.orientation==="horizontal"?r=xh().y(i=>i[0]).x(i=>i[1])(t):r=xh().x(i=>i[0]).y(i=>i[1])(t),!r)return[];let n=[{groupTexts:["plot",`line-plot-${this.plotIndex}`],type:"path",data:[{path:r,strokeFill:this.plotData.strokeFill,strokeWidth:this.plotData.strokeWidth}]}];if(this.plotData.pointLabels&&this.plotData.pointLabels.length>0){let o=[];for(let[l,[u,h]]of t.entries()){let d=this.plotData.pointLabels[l];d&&(this.orientation==="horizontal"?o.push({x:h+10,y:u,text:d,fill:this.plotData.strokeFill,verticalPos:"middle",horizontalPos:"left",fontSize:12,rotation:0}):o.push({x:u,y:h-10,text:d,fill:this.plotData.strokeFill,verticalPos:"middle",horizontalPos:"center",fontSize:12,rotation:0}))}o.length>0&&n.push({groupTexts:["plot",`line-plot-${this.plotIndex}`,"labels"],type:"text",data:o})}return n}}});var J_,v8e=F(()=>{"use strict";J_=class{constructor(t,r,n,i,a,o){this.barData=t;this.boundingRect=r;this.xAxis=n;this.yAxis=i;this.orientation=a;this.plotIndex=o}static{s(this,"BarPlot")}getDrawableElement(){let t=this.barData.data.map(a=>[this.xAxis.getScaleValue(a[0]),this.yAxis.getScaleValue(a[1])]),n=Math.min(this.xAxis.getAxisOuterPadding()*2,this.xAxis.getTickDistance())*(1-.05),i=n/2;return this.orientation==="horizontal"?[{groupTexts:["plot",`bar-plot-${this.plotIndex}`],type:"rect",data:t.map(a=>({x:this.boundingRect.x,y:a[0]-i,height:n,width:a[1]-this.boundingRect.x,fill:this.barData.fill,strokeWidth:0,strokeFill:this.barData.fill}))}]:[{groupTexts:["plot",`bar-plot-${this.plotIndex}`],type:"rect",data:t.map(a=>({x:a[0]-i,y:a[1],width:n,height:this.boundingRect.y+this.boundingRect.height-a[1],fill:this.barData.fill,strokeWidth:0,strokeFill:this.barData.fill}))}]}}});function x8e(e,t,r){return new aY(e,t,r)}var aY,b8e=F(()=>{"use strict";y8e();v8e();aY=class{constructor(t,r,n){this.chartConfig=t;this.chartData=r;this.chartThemeConfig=n;this.boundingRect={x:0,y:0,width:0,height:0}}static{s(this,"BasePlot")}setAxes(t,r){this.xAxis=t,this.yAxis=r}setBoundingBoxXY(t){this.boundingRect.x=t.x,this.boundingRect.y=t.y}calculateSpace(t){return this.boundingRect.width=t.width,this.boundingRect.height=t.height,{width:this.boundingRect.width,height:this.boundingRect.height}}getDrawableElements(){if(!(this.xAxis&&this.yAxis))throw Error("Axes must be passed to render Plots");let t=[];for(let[r,n]of this.chartData.plots.entries())switch(n.type){case"line":{let i=new Q_(n,this.xAxis,this.yAxis,this.chartConfig.chartOrientation,r);t.push(...i.getDrawableElement())}break;case"bar":{let i=new J_(n,this.boundingRect,this.xAxis,this.yAxis,this.chartConfig.chartOrientation,r);t.push(...i.getDrawableElement())}break}return t}};s(x8e,"getPlotComponent")});var eL,T8e=F(()=>{"use strict";h8e();f8e();g8e();b8e();PS();eL=class{constructor(t,r,n,i){this.chartConfig=t;this.chartData=r;this.componentStore={title:d8e(t,r,n,i),plot:x8e(t,r,n),legend:m8e(t,r,n,i),xAxis:rY(r.xAxis,t.xAxis,{titleColor:n.xAxisTitleColor,labelColor:n.xAxisLabelColor,tickColor:n.xAxisTickColor,axisLineColor:n.xAxisLineColor},i),yAxis:rY(r.yAxis,t.yAxis,{titleColor:n.yAxisTitleColor,labelColor:n.yAxisLabelColor,tickColor:n.yAxisTickColor,axisLineColor:n.yAxisLineColor},i)}}static{s(this,"Orchestrator")}calculateVerticalSpace(){let t=this.chartConfig.width,r=this.chartConfig.height,n=0,i=0,a={width:0,height:0},o=Math.floor(t*this.chartConfig.plotReservedSpacePercent/100),l=Math.floor(r*this.chartConfig.plotReservedSpacePercent/100),u=this.componentStore.plot.calculateSpace({width:o,height:l});t-=u.width,r-=u.height,u=this.componentStore.title.calculateSpace({width:this.chartConfig.width,height:r}),i=u.height,r-=u.height,this.componentStore.xAxis.setAxisPosition("bottom"),u=this.componentStore.xAxis.calculateSpace({width:t,height:r}),r-=u.height,this.componentStore.yAxis.setAxisPosition("left"),u=this.componentStore.yAxis.calculateSpace({width:t,height:r}),n=u.width,t-=u.width,a=this.componentStore.legend.calculateSpace({width:t,height:l}),t-=a.width,t>0&&(o+=t,t=0),r>0&&(l+=r,r=0),this.componentStore.plot.calculateSpace({width:o,height:l}),this.componentStore.plot.setBoundingBoxXY({x:n,y:i}),this.componentStore.legend.setBoundingBoxXY({x:n+o,y:i+Math.max((l-a.height)/2,0)}),this.componentStore.xAxis.setRange([n,n+o]),this.componentStore.xAxis.setBoundingBoxXY({x:n,y:i+l}),this.componentStore.yAxis.setRange([i,i+l]),this.componentStore.yAxis.setBoundingBoxXY({x:0,y:i}),this.chartData.plots.some(h=>MS(h))&&this.componentStore.xAxis.recalculateOuterPaddingToDrawBar()}calculateHorizontalSpace(){let t=this.chartConfig.width,r=this.chartConfig.height,n=0,i=0,a=0,o={width:0,height:0},l=Math.floor(t*this.chartConfig.plotReservedSpacePercent/100),u=Math.floor(r*this.chartConfig.plotReservedSpacePercent/100),h=this.componentStore.plot.calculateSpace({width:l,height:u});t-=h.width,r-=h.height,h=this.componentStore.title.calculateSpace({width:this.chartConfig.width,height:r}),n=h.height,r-=h.height,this.componentStore.xAxis.setAxisPosition("left"),h=this.componentStore.xAxis.calculateSpace({width:t,height:r}),t-=h.width,i=h.width,this.componentStore.yAxis.setAxisPosition("top"),h=this.componentStore.yAxis.calculateSpace({width:t,height:r}),r-=h.height,a=n+h.height,o=this.componentStore.legend.calculateSpace({width:t,height:u}),t-=o.width,t>0&&(l+=t,t=0),r>0&&(u+=r,r=0),this.componentStore.plot.calculateSpace({width:l,height:u}),this.componentStore.plot.setBoundingBoxXY({x:i,y:a}),this.componentStore.legend.setBoundingBoxXY({x:i+l,y:a+Math.max((u-o.height)/2,0)}),this.componentStore.yAxis.setRange([i,i+l]),this.componentStore.yAxis.setBoundingBoxXY({x:i,y:n}),this.componentStore.xAxis.setRange([a,a+u]),this.componentStore.xAxis.setBoundingBoxXY({x:0,y:a}),this.chartData.plots.some(d=>MS(d))&&this.componentStore.xAxis.recalculateOuterPaddingToDrawBar()}calculateSpace(){this.chartConfig.chartOrientation==="horizontal"?this.calculateHorizontalSpace():this.calculateVerticalSpace()}getDrawableElement(){this.calculateSpace();let t=[];this.componentStore.plot.setAxes(this.componentStore.xAxis,this.componentStore.yAxis);for(let r of Object.values(this.componentStore))t.push(...r.getDrawableElements());return t}}});var tL,C8e=F(()=>{"use strict";T8e();tL=class{static{s(this,"XYChartBuilder")}static build(t,r,n,i){return new eL(t,r,n,i).getDrawableElement()}}});function w8e(){let e=ka(),t=Lt();return Xr(e.xyChart,t.themeVariables.xyChart)}function k8e(){let e=Lt();return Xr(mr.xyChart,e.xyChart)}function E8e(){return{yAxis:{type:"linear",title:"",min:1/0,max:-1/0},xAxis:{type:"band",title:"",categories:[]},title:"",plots:[]}}function Zv(e){let t=Lt();return xr(e.trim(),t)}function _5t(e){S8e=e}function L5t(e){e==="horizontal"?BS.chartOrientation="horizontal":BS.chartOrientation="vertical"}function D5t(e){xn.xAxis.title=Zv(e.text)}function A8e(e,t){xn.xAxis={type:"linear",title:xn.xAxis.title,min:e,max:t},rL=!0}function I5t(e){xn.xAxis={type:"band",title:xn.xAxis.title,categories:e.map(t=>Zv(t.text))},rL=!0}function M5t(e){xn.yAxis.title=Zv(e.text)}function N5t(e,t){xn.yAxis={type:"linear",title:xn.yAxis.title,min:e,max:t},oY=!0}function P5t(e){let t=Math.min(...e),r=Math.max(...e),n=Xv(xn.yAxis)?xn.yAxis.min:1/0,i=Xv(xn.yAxis)?xn.yAxis.max:-1/0;xn.yAxis={type:"linear",title:xn.yAxis.title,min:Math.min(n,t),max:Math.max(i,r)}}function R8e(e){let t=[];if(e.length===0)return t;if(!rL){let r=Xv(xn.xAxis)?xn.xAxis.min:1/0,n=Xv(xn.xAxis)?xn.xAxis.max:-1/0;A8e(Math.min(r,1),Math.max(n,e.length))}if(NS(xn.xAxis)&&e.length>xn.xAxis.categories.length&&(e=e.slice(0,xn.xAxis.categories.length)),oY||P5t(e),NS(xn.xAxis)&&(t=xn.xAxis.categories.map((r,n)=>[r,e[n]])),Xv(xn.xAxis)){let r=xn.xAxis.min,n=xn.xAxis.max;if(e.length===1)t=[[`${r}`,e[0]]];else{let i=(n-r)/(e.length-1);t=e.map((a,o)=>[`${r+o*i}`,a])}}return t}function _8e(e){return sY[e===0?0:e%sY.length]}function O5t(e,t){let r=t.map(o=>o.value),n=t.map(o=>o.label?Zv(o.label):""),i=R8e(r),a=n.some(o=>o!=="");xn.plots.push({type:"line",title:Zv(e.text),strokeFill:_8e(OS),strokeWidth:2,data:i,...a?{pointLabels:n}:{}}),OS++}function B5t(e,t){let r=t.map(i=>i.value),n=R8e(r);xn.plots.push({type:"bar",title:Zv(e.text),fill:_8e(OS),data:n}),OS++}function $5t(){if(xn.plots.length===0)throw Error("No Plot to render, please provide a plot with some data");return xn.title=Pr(),tL.build(BS,xn,$S,S8e)}function F5t(){return $S}function G5t(){return BS}function z5t(){return xn}var OS,S8e,BS,$S,xn,sY,rL,oY,V5t,L8e,D8e=F(()=>{"use strict";br();Qi();Mc();er();Yr();$n();C8e();PS();OS=0,BS=k8e(),$S=w8e(),xn=E8e(),sY=$S.plotColorPalette.split(",").map(e=>e.trim()),rL=!1,oY=!1;s(w8e,"getChartDefaultThemeConfig");s(k8e,"getChartDefaultConfig");s(E8e,"getChartDefaultData");s(Zv,"textSanitizer");s(_5t,"setTmpSVGG");s(L5t,"setOrientation");s(D5t,"setXAxisTitle");s(A8e,"setXAxisRangeData");s(I5t,"setXAxisBand");s(M5t,"setYAxisTitle");s(N5t,"setYAxisRangeData");s(P5t,"setYAxisRangeFromPlotData");s(R8e,"transformDataWithoutCategory");s(_8e,"getPlotColorFromPalette");s(O5t,"setLineData");s(B5t,"setBarData");s($5t,"getDrawableElem");s(F5t,"getChartThemeConfig");s(G5t,"getChartConfig");s(z5t,"getXYChartData");V5t=s(function(){Tr(),OS=0,BS=k8e(),xn=E8e(),$S=w8e(),sY=$S.plotColorPalette.split(",").map(e=>e.trim()),rL=!1,oY=!1},"clear"),L8e={getDrawableElem:$5t,clear:V5t,setAccTitle:Lr,getAccTitle:Ir,setDiagramTitle:Gr,getDiagramTitle:Pr,getAccDescription:Nr,setAccDescription:Mr,setOrientation:L5t,setXAxisTitle:D5t,setXAxisRangeData:A8e,setXAxisBand:I5t,setYAxisTitle:M5t,setYAxisRangeData:N5t,setLineData:O5t,setBarData:B5t,setTmpSVGG:_5t,getChartThemeConfig:F5t,getChartConfig:G5t,getXYChartData:z5t}});var W5t,I8e,M8e=F(()=>{"use strict";St();cs();Vn();W5t=s((e,t,r,n)=>{let i=n.db,a=i.getChartThemeConfig(),o=i.getChartConfig(),l=i.getXYChartData().plots[0].data.map(T=>T[1]);function u(T){return T==="top"?"text-before-edge":"middle"}s(u,"getDominantBaseLine");function h(T){return T==="left"?"start":T==="right"?"end":"middle"}s(h,"getTextAnchor");function d(T){return`translate(${T.x}, ${T.y}) rotate(${T.rotation||0})`}s(d,"getTextTransformation"),se.debug(`Rendering xychart chart +`+e);let f=wn(t),p=f.append("g").attr("class","main"),m=p.append("rect").attr("width",o.width).attr("height",o.height).attr("class","background");jr(f,o.height,o.width,!0),f.attr("viewBox",`0 0 ${o.width} ${o.height}`),m.attr("fill",a.backgroundColor),i.setTmpSVGG(f.append("g").attr("class","mermaid-tmp-group"));let g=i.getDrawableElem(),y={};function v(T){let w=p,C="";for(let[S]of T.entries()){let k=p;S>0&&y[C]&&(k=y[C]),C+=T[S],w=y[C],w||(w=y[C]=k.append("g").attr("class",T[S]))}return w}s(v,"getGroup");for(let T of g){if(T.data.length===0)continue;let w=v(T.groupTexts);switch(T.type){case"rect":if(w.selectAll("rect").data(T.data).enter().append("rect").attr("x",C=>C.x).attr("y",C=>C.y).attr("width",C=>C.width).attr("height",C=>C.height).attr("fill",C=>C.fill).attr("stroke",C=>C.strokeFill).attr("stroke-width",C=>C.strokeWidth),o.showDataLabel){let C=o.showDataLabelOutsideBar;if(o.chartOrientation==="horizontal"){let M=function(A,I){let{data:D,label:L}=A;return I*L.length*.7<=D.width-10};var x=M;s(M,"fitsHorizontally");let S=.7,k=10,R=T.data.map((A,I)=>({data:A,label:l[I].toString()})).filter(A=>A.data.width>0&&A.data.height>0),P=R.map(A=>{let{data:I}=A,D=I.height*.7;for(;!M(A,D)&&D>0;)D-=1;return D}),N=Math.floor(Math.min(...P)),_=s(A=>C?A.data.x+A.data.width+10:A.data.x+A.data.width-10,"determineLabelXPosition");w.selectAll("text").data(R).enter().append("text").attr("x",_).attr("y",A=>A.data.y+A.data.height/2).attr("text-anchor",C?"start":"end").attr("dominant-baseline","middle").attr("fill",a.dataLabelColor).attr("font-size",`${N}px`).text(A=>A.label)}else{let R=function(_,A,I){let{data:D,label:L}=_,O=A*L.length*.7,$=D.x+D.width/2,W=$-O/2,V=$+O/2,z=W>=D.x&&V<=D.x+D.width,H=D.y+I+A<=D.y+D.height;return z&&H};var b=R;s(R,"fitsInBar");let S=10,k=T.data.map((_,A)=>({data:_,label:l[A].toString()})).filter(_=>_.data.width>0&&_.data.height>0),M=k.map(_=>{let{data:A,label:I}=_,D=A.width/(I.length*.7);for(;!R(_,D,10)&&D>0;)D-=1;return D}),P=Math.floor(Math.min(...M)),N=s(_=>C?_.data.y-10:_.data.y+10,"determineLabelYPosition");w.selectAll("text").data(k).enter().append("text").attr("x",_=>_.data.x+_.data.width/2).attr("y",N).attr("text-anchor","middle").attr("dominant-baseline",C?"auto":"hanging").attr("fill",a.dataLabelColor).attr("font-size",`${P}px`).text(_=>_.label)}}break;case"text":w.selectAll("text").data(T.data).enter().append("text").attr("x",0).attr("y",0).attr("fill",C=>C.fill).attr("font-size",C=>C.fontSize).attr("dominant-baseline",C=>u(C.verticalPos)).attr("text-anchor",C=>h(C.horizontalPos)).attr("transform",C=>d(C)).text(C=>C.text);break;case"path":w.selectAll("path").data(T.data).enter().append("path").attr("d",C=>C.path).attr("fill",C=>C.fill?C.fill:"none").attr("stroke",C=>C.strokeFill).attr("stroke-width",C=>C.strokeWidth);break}}},"draw"),I8e={draw:W5t}});var N8e={};sr(N8e,{diagram:()=>q5t});var q5t,P8e=F(()=>{"use strict";l8e();D8e();M8e();q5t={parser:o8e,db:L8e,renderer:I8e}});var lY,$8e,F8e=F(()=>{"use strict";lY=(function(){var e=s(function(X,ee,q,ie){for(q=q||{},ie=X.length;ie--;q[X[ie]]=ee);return q},"o"),t=[1,3],r=[1,4],n=[1,5],i=[1,6],a=[5,6,8,9,11,13,21,22,23,24,41,42,43,44,45,46,54,72,74,77,89,90],o=[1,22],l=[2,7],u=[1,26],h=[1,27],d=[1,28],f=[1,29],p=[1,33],m=[1,34],g=[1,35],y=[1,36],v=[1,37],x=[1,38],b=[1,24],T=[1,31],w=[1,32],C=[1,30],S=[1,39],k=[1,40],R=[5,8,9,11,13,21,22,23,24,41,42,43,44,45,46,54,72,74,77,89,90],M=[1,61],P=[89,90],N=[5,8,9,11,13,21,22,23,24,27,29,41,42,43,44,45,46,54,61,63,72,74,75,76,77,80,81,82,83,84,85,86,87,88,89,90],_=[27,29],A=[1,70],I=[1,71],D=[1,72],L=[1,73],B=[1,74],O=[1,75],$=[1,76],W=[1,83],V=[1,80],z=[1,84],H=[1,85],K=[1,86],J=[1,87],ne=[1,88],Z=[1,89],pe=[1,90],te=[1,91],fe=[1,92],ce=[5,8,9,11,13,21,22,23,24,27,41,42,43,44,45,46,54,72,74,75,76,77,80,81,82,83,84,85,86,87,88,89,90],de=[63,64],Ee=[1,101],we=[5,8,9,11,13,21,22,23,24,41,42,43,44,45,46,54,72,74,76,77,89,90],Fe=[5,8,9,11,13,21,22,23,24,41,42,43,44,45,46,54,72,74,75,76,77,80,81,82,83,84,85,86,87,88,89,90],Ye=[1,110],He=[1,106],ye=[1,107],Be=[1,108],Ie=[1,109],Ne=[1,111],re=[1,116],ue=[1,117],le=[1,114],he=[1,115],Te={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,directive:4,NEWLINE:5,RD:6,diagram:7,EOF:8,acc_title:9,acc_title_value:10,acc_descr:11,acc_descr_value:12,acc_descr_multiline_value:13,requirementDef:14,elementDef:15,relationshipDef:16,direction:17,styleStatement:18,classDefStatement:19,classStatement:20,direction_tb:21,direction_bt:22,direction_rl:23,direction_lr:24,requirementType:25,requirementName:26,STRUCT_START:27,requirementBody:28,STYLE_SEPARATOR:29,idList:30,ID:31,COLONSEP:32,id:33,TEXT:34,text:35,RISK:36,riskLevel:37,VERIFYMTHD:38,verifyType:39,STRUCT_STOP:40,REQUIREMENT:41,FUNCTIONAL_REQUIREMENT:42,INTERFACE_REQUIREMENT:43,PERFORMANCE_REQUIREMENT:44,PHYSICAL_REQUIREMENT:45,DESIGN_CONSTRAINT:46,LOW_RISK:47,MED_RISK:48,HIGH_RISK:49,VERIFY_ANALYSIS:50,VERIFY_DEMONSTRATION:51,VERIFY_INSPECTION:52,VERIFY_TEST:53,ELEMENT:54,elementName:55,elementBody:56,TYPE:57,type:58,DOCREF:59,ref:60,END_ARROW_L:61,relationship:62,LINE:63,END_ARROW_R:64,CONTAINS:65,COPIES:66,DERIVES:67,SATISFIES:68,VERIFIES:69,REFINES:70,TRACES:71,CLASSDEF:72,stylesOpt:73,CLASS:74,ALPHA:75,COMMA:76,STYLE:77,style:78,styleComponent:79,NUM:80,COLON:81,UNIT:82,SPACE:83,BRKT:84,PCT:85,MINUS:86,LABEL:87,SEMICOLON:88,unqString:89,qString:90,$accept:0,$end:1},terminals_:{2:"error",5:"NEWLINE",6:"RD",8:"EOF",9:"acc_title",10:"acc_title_value",11:"acc_descr",12:"acc_descr_value",13:"acc_descr_multiline_value",21:"direction_tb",22:"direction_bt",23:"direction_rl",24:"direction_lr",27:"STRUCT_START",29:"STYLE_SEPARATOR",31:"ID",32:"COLONSEP",34:"TEXT",36:"RISK",38:"VERIFYMTHD",40:"STRUCT_STOP",41:"REQUIREMENT",42:"FUNCTIONAL_REQUIREMENT",43:"INTERFACE_REQUIREMENT",44:"PERFORMANCE_REQUIREMENT",45:"PHYSICAL_REQUIREMENT",46:"DESIGN_CONSTRAINT",47:"LOW_RISK",48:"MED_RISK",49:"HIGH_RISK",50:"VERIFY_ANALYSIS",51:"VERIFY_DEMONSTRATION",52:"VERIFY_INSPECTION",53:"VERIFY_TEST",54:"ELEMENT",57:"TYPE",59:"DOCREF",61:"END_ARROW_L",63:"LINE",64:"END_ARROW_R",65:"CONTAINS",66:"COPIES",67:"DERIVES",68:"SATISFIES",69:"VERIFIES",70:"REFINES",71:"TRACES",72:"CLASSDEF",74:"CLASS",75:"ALPHA",76:"COMMA",77:"STYLE",80:"NUM",81:"COLON",82:"UNIT",83:"SPACE",84:"BRKT",85:"PCT",86:"MINUS",87:"LABEL",88:"SEMICOLON",89:"unqString",90:"qString"},productions_:[0,[3,3],[3,2],[3,4],[4,2],[4,2],[4,1],[7,0],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[17,1],[17,1],[17,1],[17,1],[14,5],[14,7],[28,5],[28,5],[28,5],[28,5],[28,2],[28,1],[25,1],[25,1],[25,1],[25,1],[25,1],[25,1],[37,1],[37,1],[37,1],[39,1],[39,1],[39,1],[39,1],[15,5],[15,7],[56,5],[56,5],[56,2],[56,1],[16,5],[16,5],[62,1],[62,1],[62,1],[62,1],[62,1],[62,1],[62,1],[19,3],[20,3],[20,3],[30,1],[30,3],[30,1],[30,3],[18,3],[73,1],[73,3],[78,1],[78,2],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[26,1],[26,1],[33,1],[33,1],[35,1],[35,1],[55,1],[55,1],[58,1],[58,1],[60,1],[60,1]],performAction:s(function(ee,q,ie,j,U,Y,Ae){var me=Y.length-1;switch(U){case 4:this.$=Y[me].trim(),j.setAccTitle(this.$);break;case 5:case 6:this.$=Y[me].trim(),j.setAccDescription(this.$);break;case 7:this.$=[];break;case 17:j.setDirection("TB");break;case 18:j.setDirection("BT");break;case 19:j.setDirection("RL");break;case 20:j.setDirection("LR");break;case 21:j.addRequirement(Y[me-3],Y[me-4]);break;case 22:j.addRequirement(Y[me-5],Y[me-6]),j.setClass([Y[me-5]],Y[me-3]);break;case 23:j.setNewReqId(Y[me-2]);break;case 24:j.setNewReqText(Y[me-2]);break;case 25:j.setNewReqRisk(Y[me-2]);break;case 26:j.setNewReqVerifyMethod(Y[me-2]);break;case 29:this.$=j.RequirementType.REQUIREMENT;break;case 30:this.$=j.RequirementType.FUNCTIONAL_REQUIREMENT;break;case 31:this.$=j.RequirementType.INTERFACE_REQUIREMENT;break;case 32:this.$=j.RequirementType.PERFORMANCE_REQUIREMENT;break;case 33:this.$=j.RequirementType.PHYSICAL_REQUIREMENT;break;case 34:this.$=j.RequirementType.DESIGN_CONSTRAINT;break;case 35:this.$=j.RiskLevel.LOW_RISK;break;case 36:this.$=j.RiskLevel.MED_RISK;break;case 37:this.$=j.RiskLevel.HIGH_RISK;break;case 38:this.$=j.VerifyType.VERIFY_ANALYSIS;break;case 39:this.$=j.VerifyType.VERIFY_DEMONSTRATION;break;case 40:this.$=j.VerifyType.VERIFY_INSPECTION;break;case 41:this.$=j.VerifyType.VERIFY_TEST;break;case 42:j.addElement(Y[me-3]);break;case 43:j.addElement(Y[me-5]),j.setClass([Y[me-5]],Y[me-3]);break;case 44:j.setNewElementType(Y[me-2]);break;case 45:j.setNewElementDocRef(Y[me-2]);break;case 48:j.addRelationship(Y[me-2],Y[me],Y[me-4]);break;case 49:j.addRelationship(Y[me-2],Y[me-4],Y[me]);break;case 50:this.$=j.Relationships.CONTAINS;break;case 51:this.$=j.Relationships.COPIES;break;case 52:this.$=j.Relationships.DERIVES;break;case 53:this.$=j.Relationships.SATISFIES;break;case 54:this.$=j.Relationships.VERIFIES;break;case 55:this.$=j.Relationships.REFINES;break;case 56:this.$=j.Relationships.TRACES;break;case 57:this.$=Y[me-2],j.defineClass(Y[me-1],Y[me]);break;case 58:j.setClass(Y[me-1],Y[me]);break;case 59:j.setClass([Y[me-2]],Y[me]);break;case 60:case 62:this.$=[Y[me]];break;case 61:case 63:this.$=Y[me-2].concat([Y[me]]);break;case 64:this.$=Y[me-2],j.setCssStyle(Y[me-1],Y[me]);break;case 65:this.$=[Y[me]];break;case 66:Y[me-2].push(Y[me]),this.$=Y[me-2];break;case 68:this.$=Y[me-1]+Y[me];break}},"anonymous"),table:[{3:1,4:2,6:t,9:r,11:n,13:i},{1:[3]},{3:8,4:2,5:[1,7],6:t,9:r,11:n,13:i},{5:[1,9]},{10:[1,10]},{12:[1,11]},e(a,[2,6]),{3:12,4:2,6:t,9:r,11:n,13:i},{1:[2,2]},{4:17,5:o,7:13,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},e(a,[2,4]),e(a,[2,5]),{1:[2,1]},{8:[1,41]},{4:17,5:o,7:42,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},{4:17,5:o,7:43,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},{4:17,5:o,7:44,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},{4:17,5:o,7:45,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},{4:17,5:o,7:46,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},{4:17,5:o,7:47,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},{4:17,5:o,7:48,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},{4:17,5:o,7:49,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},{4:17,5:o,7:50,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:w,77:C,89:S,90:k},{26:51,89:[1,52],90:[1,53]},{55:54,89:[1,55],90:[1,56]},{29:[1,59],61:[1,57],63:[1,58]},e(R,[2,17]),e(R,[2,18]),e(R,[2,19]),e(R,[2,20]),{30:60,33:62,75:M,89:S,90:k},{30:63,33:62,75:M,89:S,90:k},{30:64,33:62,75:M,89:S,90:k},e(P,[2,29]),e(P,[2,30]),e(P,[2,31]),e(P,[2,32]),e(P,[2,33]),e(P,[2,34]),e(N,[2,81]),e(N,[2,82]),{1:[2,3]},{8:[2,8]},{8:[2,9]},{8:[2,10]},{8:[2,11]},{8:[2,12]},{8:[2,13]},{8:[2,14]},{8:[2,15]},{8:[2,16]},{27:[1,65],29:[1,66]},e(_,[2,79]),e(_,[2,80]),{27:[1,67],29:[1,68]},e(_,[2,85]),e(_,[2,86]),{62:69,65:A,66:I,67:D,68:L,69:B,70:O,71:$},{62:77,65:A,66:I,67:D,68:L,69:B,70:O,71:$},{30:78,33:62,75:M,89:S,90:k},{73:79,75:W,76:V,78:81,79:82,80:z,81:H,82:K,83:J,84:ne,85:Z,86:pe,87:te,88:fe},e(ce,[2,60]),e(ce,[2,62]),{73:93,75:W,76:V,78:81,79:82,80:z,81:H,82:K,83:J,84:ne,85:Z,86:pe,87:te,88:fe},{30:94,33:62,75:M,76:V,89:S,90:k},{5:[1,95]},{30:96,33:62,75:M,89:S,90:k},{5:[1,97]},{30:98,33:62,75:M,89:S,90:k},{63:[1,99]},e(de,[2,50]),e(de,[2,51]),e(de,[2,52]),e(de,[2,53]),e(de,[2,54]),e(de,[2,55]),e(de,[2,56]),{64:[1,100]},e(R,[2,59],{76:V}),e(R,[2,64],{76:Ee}),{33:103,75:[1,102],89:S,90:k},e(we,[2,65],{79:104,75:W,80:z,81:H,82:K,83:J,84:ne,85:Z,86:pe,87:te,88:fe}),e(Fe,[2,67]),e(Fe,[2,69]),e(Fe,[2,70]),e(Fe,[2,71]),e(Fe,[2,72]),e(Fe,[2,73]),e(Fe,[2,74]),e(Fe,[2,75]),e(Fe,[2,76]),e(Fe,[2,77]),e(Fe,[2,78]),e(R,[2,57],{76:Ee}),e(R,[2,58],{76:V}),{5:Ye,28:105,31:He,34:ye,36:Be,38:Ie,40:Ne},{27:[1,112],76:V},{5:re,40:ue,56:113,57:le,59:he},{27:[1,118],76:V},{33:119,89:S,90:k},{33:120,89:S,90:k},{75:W,78:121,79:82,80:z,81:H,82:K,83:J,84:ne,85:Z,86:pe,87:te,88:fe},e(ce,[2,61]),e(ce,[2,63]),e(Fe,[2,68]),e(R,[2,21]),{32:[1,122]},{32:[1,123]},{32:[1,124]},{32:[1,125]},{5:Ye,28:126,31:He,34:ye,36:Be,38:Ie,40:Ne},e(R,[2,28]),{5:[1,127]},e(R,[2,42]),{32:[1,128]},{32:[1,129]},{5:re,40:ue,56:130,57:le,59:he},e(R,[2,47]),{5:[1,131]},e(R,[2,48]),e(R,[2,49]),e(we,[2,66],{79:104,75:W,80:z,81:H,82:K,83:J,84:ne,85:Z,86:pe,87:te,88:fe}),{33:132,89:S,90:k},{35:133,89:[1,134],90:[1,135]},{37:136,47:[1,137],48:[1,138],49:[1,139]},{39:140,50:[1,141],51:[1,142],52:[1,143],53:[1,144]},e(R,[2,27]),{5:Ye,28:145,31:He,34:ye,36:Be,38:Ie,40:Ne},{58:146,89:[1,147],90:[1,148]},{60:149,89:[1,150],90:[1,151]},e(R,[2,46]),{5:re,40:ue,56:152,57:le,59:he},{5:[1,153]},{5:[1,154]},{5:[2,83]},{5:[2,84]},{5:[1,155]},{5:[2,35]},{5:[2,36]},{5:[2,37]},{5:[1,156]},{5:[2,38]},{5:[2,39]},{5:[2,40]},{5:[2,41]},e(R,[2,22]),{5:[1,157]},{5:[2,87]},{5:[2,88]},{5:[1,158]},{5:[2,89]},{5:[2,90]},e(R,[2,43]),{5:Ye,28:159,31:He,34:ye,36:Be,38:Ie,40:Ne},{5:Ye,28:160,31:He,34:ye,36:Be,38:Ie,40:Ne},{5:Ye,28:161,31:He,34:ye,36:Be,38:Ie,40:Ne},{5:Ye,28:162,31:He,34:ye,36:Be,38:Ie,40:Ne},{5:re,40:ue,56:163,57:le,59:he},{5:re,40:ue,56:164,57:le,59:he},e(R,[2,23]),e(R,[2,24]),e(R,[2,25]),e(R,[2,26]),e(R,[2,44]),e(R,[2,45])],defaultActions:{8:[2,2],12:[2,1],41:[2,3],42:[2,8],43:[2,9],44:[2,10],45:[2,11],46:[2,12],47:[2,13],48:[2,14],49:[2,15],50:[2,16],134:[2,83],135:[2,84],137:[2,35],138:[2,36],139:[2,37],141:[2,38],142:[2,39],143:[2,40],144:[2,41],147:[2,87],148:[2,88],150:[2,89],151:[2,90]},parseError:s(function(ee,q){if(q.recoverable)this.trace(ee);else{var ie=new Error(ee);throw ie.hash=q,ie}},"parseError"),parse:s(function(ee){var q=this,ie=[0],j=[],U=[null],Y=[],Ae=this.table,me="",Se=0,Ce=0,Me=0,_e=2,ze=1,Le=Y.slice.call(arguments,1),qe=Object.create(this.lexer),ve={yy:{}};for(var lt in this.yy)Object.prototype.hasOwnProperty.call(this.yy,lt)&&(ve.yy[lt]=this.yy[lt]);qe.setInput(ee,ve.yy),ve.yy.lexer=qe,ve.yy.parser=this,typeof qe.yylloc>"u"&&(qe.yylloc={});var Oe=qe.yylloc;Y.push(Oe);var mt=qe.options&&qe.options.ranges;typeof ve.yy.parseError=="function"?this.parseError=ve.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function At($t){ie.length=ie.length-2*$t,U.length=U.length-$t,Y.length=Y.length-$t}s(At,"popStack");function Gt(){var $t;return $t=j.pop()||qe.lex()||ze,typeof $t!="number"&&($t instanceof Array&&(j=$t,$t=j.pop()),$t=q.symbols_[$t]||$t),$t}s(Gt,"lex");for(var Ct,Rt,gt,be,ct,xt,Ke={},De,tt,Ze,dt;;){if(gt=ie[ie.length-1],this.defaultActions[gt]?be=this.defaultActions[gt]:((Ct===null||typeof Ct>"u")&&(Ct=Gt()),be=Ae[gt]&&Ae[gt][Ct]),typeof be>"u"||!be.length||!be[0]){var Ut="";dt=[];for(De in Ae[gt])this.terminals_[De]&&De>_e&&dt.push("'"+this.terminals_[De]+"'");qe.showPosition?Ut="Parse error on line "+(Se+1)+`: +`+qe.showPosition()+` +Expecting `+dt.join(", ")+", got '"+(this.terminals_[Ct]||Ct)+"'":Ut="Parse error on line "+(Se+1)+": Unexpected "+(Ct==ze?"end of input":"'"+(this.terminals_[Ct]||Ct)+"'"),this.parseError(Ut,{text:qe.match,token:this.terminals_[Ct]||Ct,line:qe.yylineno,loc:Oe,expected:dt})}if(be[0]instanceof Array&&be.length>1)throw new Error("Parse Error: multiple actions possible at state: "+gt+", token: "+Ct);switch(be[0]){case 1:ie.push(Ct),U.push(qe.yytext),Y.push(qe.yylloc),ie.push(be[1]),Ct=null,Rt?(Ct=Rt,Rt=null):(Ce=qe.yyleng,me=qe.yytext,Se=qe.yylineno,Oe=qe.yylloc,Me>0&&Me--);break;case 2:if(tt=this.productions_[be[1]][1],Ke.$=U[U.length-tt],Ke._$={first_line:Y[Y.length-(tt||1)].first_line,last_line:Y[Y.length-1].last_line,first_column:Y[Y.length-(tt||1)].first_column,last_column:Y[Y.length-1].last_column},mt&&(Ke._$.range=[Y[Y.length-(tt||1)].range[0],Y[Y.length-1].range[1]]),xt=this.performAction.apply(Ke,[me,Ce,Se,ve.yy,be[1],U,Y].concat(Le)),typeof xt<"u")return xt;tt&&(ie=ie.slice(0,-1*tt*2),U=U.slice(0,-1*tt),Y=Y.slice(0,-1*tt)),ie.push(this.productions_[be[1]][0]),U.push(Ke.$),Y.push(Ke._$),Ze=Ae[ie[ie.length-2]][ie[ie.length-1]],ie.push(Ze);break;case 3:return!0}}return!0},"parse")},oe=(function(){var X={EOF:1,parseError:s(function(q,ie){if(this.yy.parser)this.yy.parser.parseError(q,ie);else throw new Error(q)},"parseError"),setInput:s(function(ee,q){return this.yy=q||this.yy||{},this._input=ee,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var ee=this._input[0];this.yytext+=ee,this.yyleng++,this.offset++,this.match+=ee,this.matched+=ee;var q=ee.match(/(?:\r\n?|\n).*/g);return q?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),ee},"input"),unput:s(function(ee){var q=ee.length,ie=ee.split(/(?:\r\n?|\n)/g);this._input=ee+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-q),this.offset-=q;var j=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),ie.length-1&&(this.yylineno-=ie.length-1);var U=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:ie?(ie.length===j.length?this.yylloc.first_column:0)+j[j.length-ie.length].length-ie[0].length:this.yylloc.first_column-q},this.options.ranges&&(this.yylloc.range=[U[0],U[0]+this.yyleng-q]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(ee){this.unput(this.match.slice(ee))},"less"),pastInput:s(function(){var ee=this.matched.substr(0,this.matched.length-this.match.length);return(ee.length>20?"...":"")+ee.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var ee=this.match;return ee.length<20&&(ee+=this._input.substr(0,20-ee.length)),(ee.substr(0,20)+(ee.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var ee=this.pastInput(),q=new Array(ee.length+1).join("-");return ee+this.upcomingInput()+` +`+q+"^"},"showPosition"),test_match:s(function(ee,q){var ie,j,U;if(this.options.backtrack_lexer&&(U={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(U.yylloc.range=this.yylloc.range.slice(0))),j=ee[0].match(/(?:\r\n?|\n).*/g),j&&(this.yylineno+=j.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:j?j[j.length-1].length-j[j.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+ee[0].length},this.yytext+=ee[0],this.match+=ee[0],this.matches=ee,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(ee[0].length),this.matched+=ee[0],ie=this.performAction.call(this,this.yy,this,q,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),ie)return ie;if(this._backtrack){for(var Y in U)this[Y]=U[Y];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var ee,q,ie,j;this._more||(this.yytext="",this.match="");for(var U=this._currentRules(),Y=0;Yq[0].length)){if(q=ie,j=Y,this.options.backtrack_lexer){if(ee=this.test_match(ie,U[Y]),ee!==!1)return ee;if(this._backtrack){q=!1;continue}else return!1}else if(!this.options.flex)break}return q?(ee=this.test_match(q,U[j]),ee!==!1?ee:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var q=this.next();return q||this.lex()},"lex"),begin:s(function(q){this.conditionStack.push(q)},"begin"),popState:s(function(){var q=this.conditionStack.length-1;return q>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(q){return q=this.conditionStack.length-1-Math.abs(q||0),q>=0?this.conditionStack[q]:"INITIAL"},"topState"),pushState:s(function(q){this.begin(q)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(q,ie,j,U){var Y=U;switch(j){case 0:return"title";case 1:return this.begin("acc_title"),9;break;case 2:return this.popState(),"acc_title_value";break;case 3:return this.begin("acc_descr"),11;break;case 4:return this.popState(),"acc_descr_value";break;case 5:this.begin("acc_descr_multiline");break;case 6:this.popState();break;case 7:return"acc_descr_multiline_value";case 8:return 21;case 9:return 22;case 10:return 23;case 11:return 24;case 12:return 5;case 13:break;case 14:break;case 15:break;case 16:return 8;case 17:return 6;case 18:return 27;case 19:return 40;case 20:return 29;case 21:return 32;case 22:return 31;case 23:return 34;case 24:return 36;case 25:return 38;case 26:return 41;case 27:return 42;case 28:return 43;case 29:return 44;case 30:return 45;case 31:return 46;case 32:return 47;case 33:return 48;case 34:return 49;case 35:return 50;case 36:return 51;case 37:return 52;case 38:return 53;case 39:return 54;case 40:return 65;case 41:return 66;case 42:return 67;case 43:return 68;case 44:return 69;case 45:return 70;case 46:return 71;case 47:return 57;case 48:return 59;case 49:return this.begin("style"),77;break;case 50:return 75;case 51:return 81;case 52:return 88;case 53:return"PERCENT";case 54:return 86;case 55:return 84;case 56:break;case 57:this.begin("string");break;case 58:this.popState();break;case 59:return this.begin("style"),72;break;case 60:return this.begin("style"),74;break;case 61:return 61;case 62:return 64;case 63:return 63;case 64:this.begin("string");break;case 65:this.popState();break;case 66:return"qString";case 67:return ie.yytext=ie.yytext.trim(),89;break;case 68:return 75;case 69:return 80;case 70:return 76}},"anonymous"),rules:[/^(?:title\s[^#\n;]+)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:(\r?\n)+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:$)/i,/^(?:requirementDiagram\b)/i,/^(?:\{)/i,/^(?:\})/i,/^(?::{3})/i,/^(?::)/i,/^(?:id\b)/i,/^(?:text\b)/i,/^(?:risk\b)/i,/^(?:verifyMethod\b)/i,/^(?:requirement\b)/i,/^(?:functionalRequirement\b)/i,/^(?:interfaceRequirement\b)/i,/^(?:performanceRequirement\b)/i,/^(?:physicalRequirement\b)/i,/^(?:designConstraint\b)/i,/^(?:low\b)/i,/^(?:medium\b)/i,/^(?:high\b)/i,/^(?:analysis\b)/i,/^(?:demonstration\b)/i,/^(?:inspection\b)/i,/^(?:test\b)/i,/^(?:element\b)/i,/^(?:contains\b)/i,/^(?:copies\b)/i,/^(?:derives\b)/i,/^(?:satisfies\b)/i,/^(?:verifies\b)/i,/^(?:refines\b)/i,/^(?:traces\b)/i,/^(?:type\b)/i,/^(?:docref\b)/i,/^(?:style\b)/i,/^(?:\w+)/i,/^(?::)/i,/^(?:;)/i,/^(?:%)/i,/^(?:-)/i,/^(?:#)/i,/^(?: )/i,/^(?:["])/i,/^(?:\n)/i,/^(?:classDef\b)/i,/^(?:class\b)/i,/^(?:<-)/i,/^(?:->)/i,/^(?:-)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[\w][^:,\r\n\{\<\>\-\=]*)/i,/^(?:\w+)/i,/^(?:[0-9]+)/i,/^(?:,)/i],conditions:{acc_descr_multiline:{rules:[6,7,68,69,70],inclusive:!1},acc_descr:{rules:[4,68,69,70],inclusive:!1},acc_title:{rules:[2,68,69,70],inclusive:!1},style:{rules:[50,51,52,53,54,55,56,57,58,68,69,70],inclusive:!1},unqString:{rules:[68,69,70],inclusive:!1},token:{rules:[68,69,70],inclusive:!1},string:{rules:[65,66,68,69,70],inclusive:!1},INITIAL:{rules:[0,1,3,5,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,59,60,61,62,63,64,67,68,69,70],inclusive:!0}}};return X})();Te.lexer=oe;function G(){this.yy={}}return s(G,"Parser"),G.prototype=Te,Te.Parser=G,new G})();lY.parser=lY;$8e=lY});var nL,G8e=F(()=>{"use strict";Jt();St();$n();nL=class{constructor(){this.relations=[];this.latestRequirement=this.getInitialRequirement();this.requirements=new Map;this.latestElement=this.getInitialElement();this.elements=new Map;this.classes=new Map;this.direction="TB";this.RequirementType={REQUIREMENT:"Requirement",FUNCTIONAL_REQUIREMENT:"Functional Requirement",INTERFACE_REQUIREMENT:"Interface Requirement",PERFORMANCE_REQUIREMENT:"Performance Requirement",PHYSICAL_REQUIREMENT:"Physical Requirement",DESIGN_CONSTRAINT:"Design Constraint"};this.RiskLevel={LOW_RISK:"Low",MED_RISK:"Medium",HIGH_RISK:"High"};this.VerifyType={VERIFY_ANALYSIS:"Analysis",VERIFY_DEMONSTRATION:"Demonstration",VERIFY_INSPECTION:"Inspection",VERIFY_TEST:"Test"};this.Relationships={CONTAINS:"contains",COPIES:"copies",DERIVES:"derives",SATISFIES:"satisfies",VERIFIES:"verifies",REFINES:"refines",TRACES:"traces"};this.setAccTitle=Lr;this.getAccTitle=Ir;this.setAccDescription=Mr;this.getAccDescription=Nr;this.setDiagramTitle=Gr;this.getDiagramTitle=Pr;this.getConfig=s(()=>Pe().requirement,"getConfig");this.clear(),this.setDirection=this.setDirection.bind(this),this.addRequirement=this.addRequirement.bind(this),this.setNewReqId=this.setNewReqId.bind(this),this.setNewReqRisk=this.setNewReqRisk.bind(this),this.setNewReqText=this.setNewReqText.bind(this),this.setNewReqVerifyMethod=this.setNewReqVerifyMethod.bind(this),this.addElement=this.addElement.bind(this),this.setNewElementType=this.setNewElementType.bind(this),this.setNewElementDocRef=this.setNewElementDocRef.bind(this),this.addRelationship=this.addRelationship.bind(this),this.setCssStyle=this.setCssStyle.bind(this),this.setClass=this.setClass.bind(this),this.defineClass=this.defineClass.bind(this),this.setAccTitle=this.setAccTitle.bind(this),this.setAccDescription=this.setAccDescription.bind(this)}static{s(this,"RequirementDB")}getDirection(){return this.direction}setDirection(t){this.direction=t}resetLatestRequirement(){this.latestRequirement=this.getInitialRequirement()}resetLatestElement(){this.latestElement=this.getInitialElement()}getInitialRequirement(){return{requirementId:"",text:"",risk:"",verifyMethod:"",name:"",type:"",cssStyles:[],classes:["default"]}}getInitialElement(){return{name:"",type:"",docRef:"",cssStyles:[],classes:["default"]}}addRequirement(t,r){return this.requirements.has(t)||this.requirements.set(t,{name:t,type:r,requirementId:this.latestRequirement.requirementId,text:this.latestRequirement.text,risk:this.latestRequirement.risk,verifyMethod:this.latestRequirement.verifyMethod,cssStyles:[],classes:["default"]}),this.resetLatestRequirement(),this.requirements.get(t)}getRequirements(){return this.requirements}setNewReqId(t){this.latestRequirement!==void 0&&(this.latestRequirement.requirementId=t)}setNewReqText(t){this.latestRequirement!==void 0&&(this.latestRequirement.text=t)}setNewReqRisk(t){this.latestRequirement!==void 0&&(this.latestRequirement.risk=t)}setNewReqVerifyMethod(t){this.latestRequirement!==void 0&&(this.latestRequirement.verifyMethod=t)}addElement(t){return this.elements.has(t)||(this.elements.set(t,{name:t,type:this.latestElement.type,docRef:this.latestElement.docRef,cssStyles:[],classes:["default"]}),se.info("Added new element: ",t)),this.resetLatestElement(),this.elements.get(t)}getElements(){return this.elements}setNewElementType(t){this.latestElement!==void 0&&(this.latestElement.type=t)}setNewElementDocRef(t){this.latestElement!==void 0&&(this.latestElement.docRef=t)}addRelationship(t,r,n){this.relations.push({type:t,src:r,dst:n})}getRelationships(){return this.relations}clear(){this.relations=[],this.resetLatestRequirement(),this.requirements=new Map,this.resetLatestElement(),this.elements=new Map,this.classes=new Map,Tr()}setCssStyle(t,r){for(let n of t){let i=this.requirements.get(n)??this.elements.get(n);if(!r||!i)return;for(let a of r)a.includes(",")?i.cssStyles.push(...a.split(",")):i.cssStyles.push(a)}}setClass(t,r){for(let n of t){let i=this.requirements.get(n)??this.elements.get(n);if(i)for(let a of r){i.classes.push(a);let o=this.classes.get(a)?.styles;o&&i.cssStyles.push(...o)}}}defineClass(t,r){for(let n of t){let i=this.classes.get(n);i===void 0&&(i={id:n,styles:[],textStyles:[]},this.classes.set(n,i)),r&&r.forEach(function(a){if(/color/.exec(a)){let o=a.replace("fill","bgFill");i.textStyles.push(o)}i.styles.push(a)}),this.requirements.forEach(a=>{a.classes.includes(n)&&a.cssStyles.push(...r.flatMap(o=>o.split(",")))}),this.elements.forEach(a=>{a.classes.includes(n)&&a.cssStyles.push(...r.flatMap(o=>o.split(",")))})}}getClasses(){return this.classes}getData(){let t=Pe(),r=[],n=[];for(let a of this.requirements.values()){let o=a;o.id=a.name,o.cssStyles=a.cssStyles,o.cssClasses=a.classes.join(" "),o.shape="requirementBox",o.look=t.look,o.colorIndex=r.length,r.push(o)}for(let a of this.elements.values()){let o=a;o.shape="requirementBox",o.look=t.look,o.id=a.name,o.cssStyles=a.cssStyles,o.cssClasses=a.classes.join(" "),o.colorIndex=r.length,r.push(o)}let i=0;for(let a of this.relations){let o=a.type===this.Relationships.CONTAINS,l={id:`${a.src}-${a.dst}-${i++}`,start:this.requirements.get(a.src)?.name??this.elements.get(a.src)?.name,end:this.requirements.get(a.dst)?.name??this.elements.get(a.dst)?.name,label:`<<${a.type}>>`,classes:"relationshipLine",style:["fill:none",o?"":"stroke-dasharray: 10,7"],labelpos:"c",thickness:"normal",type:"normal",pattern:o?"normal":"dashed",arrowTypeStart:o?"requirement_contains":"",arrowTypeEnd:o?"":"requirement_arrow",look:t.look,labelType:"markdown"};n.push(l)}return{nodes:r,edges:n,other:{},config:t,direction:this.getDirection()}}}});var j5t,X5t,z8e,V8e=F(()=>{"use strict";br();j5t=s(e=>{let t=Lt(),{themeVariables:r,look:n}=t,{bkgColorArray:i,borderColorArray:a}=r;if(!a?.length)return"";let o="";for(let l=0;l{let t=Lt(),{look:r,themeVariables:n}=t,{requirementEdgeLabelBackground:i}=n;return` + ${j5t(e)} + marker { + fill: ${e.relationColor}; + stroke: ${e.relationColor}; + } + + marker.cross { + stroke: ${e.lineColor}; + } + + svg { + font-family: ${e.fontFamily}; + font-size: ${e.fontSize}; + } + + .reqBox { + fill: ${e.requirementBackground}; + fill-opacity: 1.0; + stroke: ${e.requirementBorderColor}; + stroke-width: ${e.requirementBorderSize}; + } + + .reqTitle, .reqLabel{ + fill: ${e.requirementTextColor}; + } + .reqLabelBox { + fill: ${e.relationLabelBackground}; + fill-opacity: 1.0; + } + + .req-title-line { + stroke: ${e.requirementBorderColor}; + stroke-width: ${e.requirementBorderSize}; + } + .relationshipLine { + stroke: ${e.relationColor}; + stroke-width: ${r==="neo"?e.strokeWidth:"1px"}; + } + .relationshipLabel { + fill: ${e.relationLabelColor}; + } + .edgeLabel { + background-color: ${e.edgeLabelBackground}; + } + .edgeLabel .label rect { + fill: ${e.edgeLabelBackground}; + } + .edgeLabel .label text { + fill: ${e.relationLabelColor}; + } + .divider { + stroke: ${e.nodeBorder}; + stroke-width: 1; + } + .label { + font-family: ${e.fontFamily}; + color: ${e.nodeTextColor||e.textColor}; + } + .label text,span { + fill: ${e.nodeTextColor||e.textColor}; + color: ${e.nodeTextColor||e.textColor}; + } + .labelBkg { + background-color: ${i??e.edgeLabelBackground}; + } + +`},"getStyles"),z8e=X5t});var cY={};sr(cY,{draw:()=>K5t});var K5t,W8e=F(()=>{"use strict";Jt();St();Rm();ep();tp();er();K5t=s(async function(e,t,r,n){se.info("REF0:"),se.info("Drawing requirement diagram (unified)",t);let{securityLevel:i,state:a,layout:o,look:l}=Pe(),u=n.db.getData(),h=xl(t,i);u.type=n.type,u.layoutAlgorithm=Au(o),u.nodeSpacing=a?.nodeSpacing??50,u.rankSpacing=a?.rankSpacing??50,u.markers=l==="neo"?["requirement_contains_neo","requirement_arrow_neo"]:["requirement_contains","requirement_arrow"],u.diagramId=t,await Dl(u,h);let d=8;or.insertTitle(h,"requirementDiagramTitleText",a?.titleTopMargin??25,n.db.getDiagramTitle()),Co(h,d,"requirementDiagram",a?.useMaxWidth??!0)},"draw")});var q8e={};sr(q8e,{diagram:()=>Z5t});var Z5t,H8e=F(()=>{"use strict";F8e();G8e();V8e();W8e();Z5t={parser:$8e,get db(){return new nL},renderer:cY,styles:z8e}});var uY,j8e,X8e=F(()=>{"use strict";uY=(function(){var e=s(function(Ce,Me,_e,ze){for(_e=_e||{},ze=Ce.length;ze--;_e[Ce[ze]]=Me);return _e},"o"),t=[1,2],r=[1,3],n=[1,4],i=[2,4],a=[1,9],o=[1,11],l=[1,12],u=[1,14],h=[1,15],d=[1,17],f=[1,18],p=[1,19],m=[1,25],g=[1,26],y=[1,27],v=[1,28],x=[1,29],b=[1,30],T=[1,31],w=[1,32],C=[1,33],S=[1,34],k=[1,35],R=[1,36],M=[1,37],P=[1,38],N=[1,39],_=[1,40],A=[1,42],I=[1,43],D=[1,44],L=[1,45],B=[1,46],O=[1,47],$=[1,4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,48,49,50,51,53,54,56,61,62,63,64,73],W=[1,74],V=[1,80],z=[1,81],H=[1,82],K=[1,83],J=[1,84],ne=[1,85],Z=[1,86],pe=[1,87],te=[1,88],fe=[1,89],ce=[1,90],de=[1,91],Ee=[1,92],we=[1,93],Fe=[1,94],Ye=[1,95],He=[1,96],ye=[1,97],Be=[1,98],Ie=[1,99],Ne=[1,100],re=[1,101],ue=[1,102],le=[1,103],he=[1,104],Te=[1,105],oe=[2,78],G=[4,5,17,51,53,54],X=[4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,51,53,54,56,61,62,63,64,73],ee=[4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,50,51,53,54,56,61,62,63,64,73],q=[4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,49,51,53,54,56,61,62,63,64,73],ie=[4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,48,51,53,54,56,61,62,63,64,73],j=[5,52],U=[70,71,72,73],Y=[1,151],Ae={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,SPACE:4,NEWLINE:5,SD:6,document:7,line:8,statement:9,INVALID:10,box_section:11,box_line:12,participant_statement:13,create:14,box:15,restOfLine:16,end:17,signal:18,autonumber:19,NUM:20,off:21,activate:22,actor:23,deactivate:24,note_statement:25,links_statement:26,link_statement:27,properties_statement:28,details_statement:29,title:30,legacy_title:31,acc_title:32,acc_title_value:33,acc_descr:34,acc_descr_value:35,acc_descr_multiline_value:36,loop:37,rect:38,opt:39,alt:40,else_sections:41,par:42,par_sections:43,par_over:44,critical:45,option_sections:46,break:47,option:48,and:49,else:50,participant:51,AS:52,participant_actor:53,destroy:54,actor_with_config:55,note:56,placement:57,text2:58,over:59,actor_pair:60,links:61,link:62,properties:63,details:64,spaceList:65,",":66,left_of:67,right_of:68,signaltype:69,"+":70,"-":71,"()":72,ACTOR:73,config_object:74,CONFIG_START:75,CONFIG_CONTENT:76,CONFIG_END:77,SOLID_OPEN_ARROW:78,DOTTED_OPEN_ARROW:79,SOLID_ARROW:80,SOLID_ARROW_TOP:81,SOLID_ARROW_BOTTOM:82,STICK_ARROW_TOP:83,STICK_ARROW_BOTTOM:84,SOLID_ARROW_TOP_DOTTED:85,SOLID_ARROW_BOTTOM_DOTTED:86,STICK_ARROW_TOP_DOTTED:87,STICK_ARROW_BOTTOM_DOTTED:88,SOLID_ARROW_TOP_REVERSE:89,SOLID_ARROW_BOTTOM_REVERSE:90,STICK_ARROW_TOP_REVERSE:91,STICK_ARROW_BOTTOM_REVERSE:92,SOLID_ARROW_TOP_REVERSE_DOTTED:93,SOLID_ARROW_BOTTOM_REVERSE_DOTTED:94,STICK_ARROW_TOP_REVERSE_DOTTED:95,STICK_ARROW_BOTTOM_REVERSE_DOTTED:96,BIDIRECTIONAL_SOLID_ARROW:97,DOTTED_ARROW:98,BIDIRECTIONAL_DOTTED_ARROW:99,SOLID_CROSS:100,DOTTED_CROSS:101,SOLID_POINT:102,DOTTED_POINT:103,TXT:104,$accept:0,$end:1},terminals_:{2:"error",4:"SPACE",5:"NEWLINE",6:"SD",10:"INVALID",14:"create",15:"box",16:"restOfLine",17:"end",19:"autonumber",20:"NUM",21:"off",22:"activate",24:"deactivate",30:"title",31:"legacy_title",32:"acc_title",33:"acc_title_value",34:"acc_descr",35:"acc_descr_value",36:"acc_descr_multiline_value",37:"loop",38:"rect",39:"opt",40:"alt",42:"par",44:"par_over",45:"critical",47:"break",48:"option",49:"and",50:"else",51:"participant",52:"AS",53:"participant_actor",54:"destroy",56:"note",59:"over",61:"links",62:"link",63:"properties",64:"details",66:",",67:"left_of",68:"right_of",70:"+",71:"-",72:"()",73:"ACTOR",75:"CONFIG_START",76:"CONFIG_CONTENT",77:"CONFIG_END",78:"SOLID_OPEN_ARROW",79:"DOTTED_OPEN_ARROW",80:"SOLID_ARROW",81:"SOLID_ARROW_TOP",82:"SOLID_ARROW_BOTTOM",83:"STICK_ARROW_TOP",84:"STICK_ARROW_BOTTOM",85:"SOLID_ARROW_TOP_DOTTED",86:"SOLID_ARROW_BOTTOM_DOTTED",87:"STICK_ARROW_TOP_DOTTED",88:"STICK_ARROW_BOTTOM_DOTTED",89:"SOLID_ARROW_TOP_REVERSE",90:"SOLID_ARROW_BOTTOM_REVERSE",91:"STICK_ARROW_TOP_REVERSE",92:"STICK_ARROW_BOTTOM_REVERSE",93:"SOLID_ARROW_TOP_REVERSE_DOTTED",94:"SOLID_ARROW_BOTTOM_REVERSE_DOTTED",95:"STICK_ARROW_TOP_REVERSE_DOTTED",96:"STICK_ARROW_BOTTOM_REVERSE_DOTTED",97:"BIDIRECTIONAL_SOLID_ARROW",98:"DOTTED_ARROW",99:"BIDIRECTIONAL_DOTTED_ARROW",100:"SOLID_CROSS",101:"DOTTED_CROSS",102:"SOLID_POINT",103:"DOTTED_POINT",104:"TXT"},productions_:[0,[3,2],[3,2],[3,2],[7,0],[7,2],[8,2],[8,1],[8,1],[8,1],[11,0],[11,2],[12,2],[12,1],[12,1],[9,1],[9,2],[9,4],[9,2],[9,4],[9,3],[9,3],[9,2],[9,3],[9,3],[9,2],[9,2],[9,2],[9,2],[9,2],[9,1],[9,1],[9,2],[9,2],[9,1],[9,4],[9,4],[9,4],[9,4],[9,4],[9,4],[9,4],[9,4],[46,1],[46,4],[43,1],[43,4],[41,1],[41,4],[13,5],[13,3],[13,5],[13,3],[13,3],[13,5],[13,3],[13,5],[13,3],[25,4],[25,4],[26,3],[27,3],[28,3],[29,3],[65,2],[65,1],[60,3],[60,1],[57,1],[57,1],[18,5],[18,5],[18,5],[18,5],[18,6],[18,4],[55,2],[74,3],[23,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[58,1]],performAction:s(function(Me,_e,ze,Le,qe,ve,lt){var Oe=ve.length-1;switch(qe){case 3:return Le.apply(ve[Oe]),ve[Oe];break;case 4:case 10:this.$=[];break;case 5:case 11:ve[Oe-1].push(ve[Oe]),this.$=ve[Oe-1];break;case 6:case 7:case 12:case 13:this.$=ve[Oe];break;case 8:case 9:case 14:this.$=[];break;case 16:ve[Oe].type="createParticipant",this.$=ve[Oe];break;case 17:ve[Oe-1].unshift({type:"boxStart",boxData:Le.parseBoxData(ve[Oe-2])}),ve[Oe-1].push({type:"boxEnd",boxText:ve[Oe-2]}),this.$=ve[Oe-1];break;case 19:this.$={type:"sequenceIndex",sequenceIndex:Number(ve[Oe-2]),sequenceIndexStep:Number(ve[Oe-1]),sequenceVisible:!0,signalType:Le.LINETYPE.AUTONUMBER};break;case 20:this.$={type:"sequenceIndex",sequenceIndex:Number(ve[Oe-1]),sequenceIndexStep:1,sequenceVisible:!0,signalType:Le.LINETYPE.AUTONUMBER};break;case 21:this.$={type:"sequenceIndex",sequenceVisible:!1,signalType:Le.LINETYPE.AUTONUMBER};break;case 22:this.$={type:"sequenceIndex",sequenceVisible:!0,signalType:Le.LINETYPE.AUTONUMBER};break;case 23:this.$={type:"activeStart",signalType:Le.LINETYPE.ACTIVE_START,actor:ve[Oe-1].actor};break;case 24:this.$={type:"activeEnd",signalType:Le.LINETYPE.ACTIVE_END,actor:ve[Oe-1].actor};break;case 30:Le.setDiagramTitle(ve[Oe].substring(6)),this.$=ve[Oe].substring(6);break;case 31:Le.setDiagramTitle(ve[Oe].substring(7)),this.$=ve[Oe].substring(7);break;case 32:this.$=ve[Oe].trim(),Le.setAccTitle(this.$);break;case 33:case 34:this.$=ve[Oe].trim(),Le.setAccDescription(this.$);break;case 35:ve[Oe-1].unshift({type:"loopStart",loopText:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.LOOP_START}),ve[Oe-1].push({type:"loopEnd",loopText:ve[Oe-2],signalType:Le.LINETYPE.LOOP_END}),this.$=ve[Oe-1];break;case 36:ve[Oe-1].unshift({type:"rectStart",color:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.RECT_START}),ve[Oe-1].push({type:"rectEnd",color:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.RECT_END}),this.$=ve[Oe-1];break;case 37:ve[Oe-1].unshift({type:"optStart",optText:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.OPT_START}),ve[Oe-1].push({type:"optEnd",optText:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.OPT_END}),this.$=ve[Oe-1];break;case 38:ve[Oe-1].unshift({type:"altStart",altText:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.ALT_START}),ve[Oe-1].push({type:"altEnd",signalType:Le.LINETYPE.ALT_END}),this.$=ve[Oe-1];break;case 39:ve[Oe-1].unshift({type:"parStart",parText:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.PAR_START}),ve[Oe-1].push({type:"parEnd",signalType:Le.LINETYPE.PAR_END}),this.$=ve[Oe-1];break;case 40:ve[Oe-1].unshift({type:"parStart",parText:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.PAR_OVER_START}),ve[Oe-1].push({type:"parEnd",signalType:Le.LINETYPE.PAR_END}),this.$=ve[Oe-1];break;case 41:ve[Oe-1].unshift({type:"criticalStart",criticalText:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.CRITICAL_START}),ve[Oe-1].push({type:"criticalEnd",signalType:Le.LINETYPE.CRITICAL_END}),this.$=ve[Oe-1];break;case 42:ve[Oe-1].unshift({type:"breakStart",breakText:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.BREAK_START}),ve[Oe-1].push({type:"breakEnd",optText:Le.parseMessage(ve[Oe-2]),signalType:Le.LINETYPE.BREAK_END}),this.$=ve[Oe-1];break;case 44:this.$=ve[Oe-3].concat([{type:"option",optionText:Le.parseMessage(ve[Oe-1]),signalType:Le.LINETYPE.CRITICAL_OPTION},ve[Oe]]);break;case 46:this.$=ve[Oe-3].concat([{type:"and",parText:Le.parseMessage(ve[Oe-1]),signalType:Le.LINETYPE.PAR_AND},ve[Oe]]);break;case 48:this.$=ve[Oe-3].concat([{type:"else",altText:Le.parseMessage(ve[Oe-1]),signalType:Le.LINETYPE.ALT_ELSE},ve[Oe]]);break;case 49:ve[Oe-3].draw="participant",ve[Oe-3].type="addParticipant",ve[Oe-3].description=Le.parseMessage(ve[Oe-1]),this.$=ve[Oe-3];break;case 50:ve[Oe-1].draw="participant",ve[Oe-1].type="addParticipant",this.$=ve[Oe-1];break;case 51:ve[Oe-3].draw="actor",ve[Oe-3].type="addParticipant",ve[Oe-3].description=Le.parseMessage(ve[Oe-1]),this.$=ve[Oe-3];break;case 52:case 57:ve[Oe-1].draw="actor",ve[Oe-1].type="addParticipant",this.$=ve[Oe-1];break;case 53:ve[Oe-1].type="destroyParticipant",this.$=ve[Oe-1];break;case 54:ve[Oe-3].draw="participant",ve[Oe-3].type="addParticipant",ve[Oe-3].description=Le.parseMessage(ve[Oe-1]),this.$=ve[Oe-3];break;case 55:ve[Oe-1].draw="participant",ve[Oe-1].type="addParticipant",this.$=ve[Oe-1];break;case 56:ve[Oe-3].draw="actor",ve[Oe-3].type="addParticipant",ve[Oe-3].description=Le.parseMessage(ve[Oe-1]),this.$=ve[Oe-3];break;case 58:this.$=[ve[Oe-1],{type:"addNote",placement:ve[Oe-2],actor:ve[Oe-1].actor,text:ve[Oe]}];break;case 59:ve[Oe-2]=[].concat(ve[Oe-1],ve[Oe-1]).slice(0,2),ve[Oe-2][0]=ve[Oe-2][0].actor,ve[Oe-2][1]=ve[Oe-2][1].actor,this.$=[ve[Oe-1],{type:"addNote",placement:Le.PLACEMENT.OVER,actor:ve[Oe-2].slice(0,2),text:ve[Oe]}];break;case 60:this.$=[ve[Oe-1],{type:"addLinks",actor:ve[Oe-1].actor,text:ve[Oe]}];break;case 61:this.$=[ve[Oe-1],{type:"addALink",actor:ve[Oe-1].actor,text:ve[Oe]}];break;case 62:this.$=[ve[Oe-1],{type:"addProperties",actor:ve[Oe-1].actor,text:ve[Oe]}];break;case 63:this.$=[ve[Oe-1],{type:"addDetails",actor:ve[Oe-1].actor,text:ve[Oe]}];break;case 66:this.$=[ve[Oe-2],ve[Oe]];break;case 67:this.$=ve[Oe];break;case 68:this.$=Le.PLACEMENT.LEFTOF;break;case 69:this.$=Le.PLACEMENT.RIGHTOF;break;case 70:this.$=[ve[Oe-4],ve[Oe-1],{type:"addMessage",from:ve[Oe-4].actor,to:ve[Oe-1].actor,signalType:ve[Oe-3],msg:ve[Oe],activate:!0},{type:"activeStart",signalType:Le.LINETYPE.ACTIVE_START,actor:ve[Oe-1].actor}];break;case 71:this.$=[ve[Oe-4],ve[Oe-1],{type:"addMessage",from:ve[Oe-4].actor,to:ve[Oe-1].actor,signalType:ve[Oe-3],msg:ve[Oe]},{type:"activeEnd",signalType:Le.LINETYPE.ACTIVE_END,actor:ve[Oe-4].actor}];break;case 72:this.$=[ve[Oe-4],ve[Oe-1],{type:"addMessage",from:ve[Oe-4].actor,to:ve[Oe-1].actor,signalType:ve[Oe-3],msg:ve[Oe],activate:!0,centralConnection:Le.LINETYPE.CENTRAL_CONNECTION},{type:"centralConnection",signalType:Le.LINETYPE.CENTRAL_CONNECTION,actor:ve[Oe-1].actor}];break;case 73:this.$=[ve[Oe-4],ve[Oe-1],{type:"addMessage",from:ve[Oe-4].actor,to:ve[Oe-1].actor,signalType:ve[Oe-2],msg:ve[Oe],activate:!1,centralConnection:Le.LINETYPE.CENTRAL_CONNECTION_REVERSE},{type:"centralConnectionReverse",signalType:Le.LINETYPE.CENTRAL_CONNECTION_REVERSE,actor:ve[Oe-4].actor}];break;case 74:this.$=[ve[Oe-5],ve[Oe-1],{type:"addMessage",from:ve[Oe-5].actor,to:ve[Oe-1].actor,signalType:ve[Oe-3],msg:ve[Oe],activate:!0,centralConnection:Le.LINETYPE.CENTRAL_CONNECTION_DUAL},{type:"centralConnection",signalType:Le.LINETYPE.CENTRAL_CONNECTION,actor:ve[Oe-1].actor},{type:"centralConnectionReverse",signalType:Le.LINETYPE.CENTRAL_CONNECTION_REVERSE,actor:ve[Oe-5].actor}];break;case 75:this.$=[ve[Oe-3],ve[Oe-1],{type:"addMessage",from:ve[Oe-3].actor,to:ve[Oe-1].actor,signalType:ve[Oe-2],msg:ve[Oe]}];break;case 76:this.$={type:"addParticipant",actor:ve[Oe-1],config:ve[Oe]};break;case 77:this.$=ve[Oe-1].trim();break;case 78:this.$={type:"addParticipant",actor:ve[Oe]};break;case 79:this.$=Le.LINETYPE.SOLID_OPEN;break;case 80:this.$=Le.LINETYPE.DOTTED_OPEN;break;case 81:this.$=Le.LINETYPE.SOLID;break;case 82:this.$=Le.LINETYPE.SOLID_TOP;break;case 83:this.$=Le.LINETYPE.SOLID_BOTTOM;break;case 84:this.$=Le.LINETYPE.STICK_TOP;break;case 85:this.$=Le.LINETYPE.STICK_BOTTOM;break;case 86:this.$=Le.LINETYPE.SOLID_TOP_DOTTED;break;case 87:this.$=Le.LINETYPE.SOLID_BOTTOM_DOTTED;break;case 88:this.$=Le.LINETYPE.STICK_TOP_DOTTED;break;case 89:this.$=Le.LINETYPE.STICK_BOTTOM_DOTTED;break;case 90:this.$=Le.LINETYPE.SOLID_ARROW_TOP_REVERSE;break;case 91:this.$=Le.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE;break;case 92:this.$=Le.LINETYPE.STICK_ARROW_TOP_REVERSE;break;case 93:this.$=Le.LINETYPE.STICK_ARROW_BOTTOM_REVERSE;break;case 94:this.$=Le.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED;break;case 95:this.$=Le.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED;break;case 96:this.$=Le.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED;break;case 97:this.$=Le.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED;break;case 98:this.$=Le.LINETYPE.BIDIRECTIONAL_SOLID;break;case 99:this.$=Le.LINETYPE.DOTTED;break;case 100:this.$=Le.LINETYPE.BIDIRECTIONAL_DOTTED;break;case 101:this.$=Le.LINETYPE.SOLID_CROSS;break;case 102:this.$=Le.LINETYPE.DOTTED_CROSS;break;case 103:this.$=Le.LINETYPE.SOLID_POINT;break;case 104:this.$=Le.LINETYPE.DOTTED_POINT;break;case 105:this.$=Le.parseMessage(ve[Oe].trim().substring(1));break}},"anonymous"),table:[{3:1,4:t,5:r,6:n},{1:[3]},{3:5,4:t,5:r,6:n},{3:6,4:t,5:r,6:n},e([1,4,5,10,14,15,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,51,53,54,56,61,62,63,64,73],i,{7:7}),{1:[2,1]},{1:[2,2]},{1:[2,3],4:a,5:o,8:8,9:10,10:l,13:13,14:u,15:h,18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:w,40:C,42:S,44:k,45:R,47:M,51:P,53:N,54:_,56:A,61:I,62:D,63:L,64:B,73:O},e($,[2,5]),{9:48,13:13,14:u,15:h,18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:w,40:C,42:S,44:k,45:R,47:M,51:P,53:N,54:_,56:A,61:I,62:D,63:L,64:B,73:O},e($,[2,7]),e($,[2,8]),e($,[2,9]),e($,[2,15]),{13:49,51:P,53:N,54:_},{16:[1,50]},{5:[1,51]},{5:[1,54],20:[1,52],21:[1,53]},{23:55,73:O},{23:56,73:O},{5:[1,57]},{5:[1,58]},{5:[1,59]},{5:[1,60]},{5:[1,61]},e($,[2,30]),e($,[2,31]),{33:[1,62]},{35:[1,63]},e($,[2,34]),{16:[1,64]},{16:[1,65]},{16:[1,66]},{16:[1,67]},{16:[1,68]},{16:[1,69]},{16:[1,70]},{16:[1,71]},{23:72,55:73,73:W},{23:75,55:76,73:W},{23:77,73:O},{69:78,72:[1,79],78:V,79:z,80:H,81:K,82:J,83:ne,84:Z,85:pe,86:te,87:fe,88:ce,89:de,90:Ee,91:we,92:Fe,93:Ye,94:He,95:ye,96:Be,97:Ie,98:Ne,99:re,100:ue,101:le,102:he,103:Te},{57:106,59:[1,107],67:[1,108],68:[1,109]},{23:110,73:O},{23:111,73:O},{23:112,73:O},{23:113,73:O},e([5,66,72,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104],oe),e($,[2,6]),e($,[2,16]),e(G,[2,10],{11:114}),e($,[2,18]),{5:[1,116],20:[1,115]},{5:[1,117]},e($,[2,22]),{5:[1,118]},{5:[1,119]},e($,[2,25]),e($,[2,26]),e($,[2,27]),e($,[2,28]),e($,[2,29]),e($,[2,32]),e($,[2,33]),e(X,i,{7:120}),e(X,i,{7:121}),e(X,i,{7:122}),e(ee,i,{41:123,7:124}),e(q,i,{43:125,7:126}),e(q,i,{7:126,43:127}),e(ie,i,{46:128,7:129}),e(X,i,{7:130}),{5:[1,132],52:[1,131]},{5:[1,134],52:[1,133]},e(j,oe,{74:135,75:[1,136]}),{5:[1,138],52:[1,137]},{5:[1,140],52:[1,139]},{5:[1,141]},{23:145,70:[1,142],71:[1,143],72:[1,144],73:O},{69:146,78:V,79:z,80:H,81:K,82:J,83:ne,84:Z,85:pe,86:te,87:fe,88:ce,89:de,90:Ee,91:we,92:Fe,93:Ye,94:He,95:ye,96:Be,97:Ie,98:Ne,99:re,100:ue,101:le,102:he,103:Te},e(U,[2,79]),e(U,[2,80]),e(U,[2,81]),e(U,[2,82]),e(U,[2,83]),e(U,[2,84]),e(U,[2,85]),e(U,[2,86]),e(U,[2,87]),e(U,[2,88]),e(U,[2,89]),e(U,[2,90]),e(U,[2,91]),e(U,[2,92]),e(U,[2,93]),e(U,[2,94]),e(U,[2,95]),e(U,[2,96]),e(U,[2,97]),e(U,[2,98]),e(U,[2,99]),e(U,[2,100]),e(U,[2,101]),e(U,[2,102]),e(U,[2,103]),e(U,[2,104]),{23:147,73:O},{23:149,60:148,73:O},{73:[2,68]},{73:[2,69]},{58:150,104:Y},{58:152,104:Y},{58:153,104:Y},{58:154,104:Y},{4:[1,157],5:[1,159],12:156,13:158,17:[1,155],51:P,53:N,54:_},{5:[1,160]},e($,[2,20]),e($,[2,21]),e($,[2,23]),e($,[2,24]),{4:a,5:o,8:8,9:10,10:l,13:13,14:u,15:h,17:[1,161],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:w,40:C,42:S,44:k,45:R,47:M,51:P,53:N,54:_,56:A,61:I,62:D,63:L,64:B,73:O},{4:a,5:o,8:8,9:10,10:l,13:13,14:u,15:h,17:[1,162],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:w,40:C,42:S,44:k,45:R,47:M,51:P,53:N,54:_,56:A,61:I,62:D,63:L,64:B,73:O},{4:a,5:o,8:8,9:10,10:l,13:13,14:u,15:h,17:[1,163],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:w,40:C,42:S,44:k,45:R,47:M,51:P,53:N,54:_,56:A,61:I,62:D,63:L,64:B,73:O},{17:[1,164]},{4:a,5:o,8:8,9:10,10:l,13:13,14:u,15:h,17:[2,47],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:w,40:C,42:S,44:k,45:R,47:M,50:[1,165],51:P,53:N,54:_,56:A,61:I,62:D,63:L,64:B,73:O},{17:[1,166]},{4:a,5:o,8:8,9:10,10:l,13:13,14:u,15:h,17:[2,45],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:w,40:C,42:S,44:k,45:R,47:M,49:[1,167],51:P,53:N,54:_,56:A,61:I,62:D,63:L,64:B,73:O},{17:[1,168]},{17:[1,169]},{4:a,5:o,8:8,9:10,10:l,13:13,14:u,15:h,17:[2,43],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:w,40:C,42:S,44:k,45:R,47:M,48:[1,170],51:P,53:N,54:_,56:A,61:I,62:D,63:L,64:B,73:O},{4:a,5:o,8:8,9:10,10:l,13:13,14:u,15:h,17:[1,171],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:w,40:C,42:S,44:k,45:R,47:M,51:P,53:N,54:_,56:A,61:I,62:D,63:L,64:B,73:O},{16:[1,172]},e($,[2,50]),{16:[1,173]},e($,[2,55]),e(j,[2,76]),{76:[1,174]},{16:[1,175]},e($,[2,52]),{16:[1,176]},e($,[2,57]),e($,[2,53]),{23:177,73:O},{23:178,73:O},{23:179,73:O},{58:180,104:Y},{23:181,72:[1,182],73:O},{58:183,104:Y},{58:184,104:Y},{66:[1,185],104:[2,67]},{5:[2,60]},{5:[2,105]},{5:[2,61]},{5:[2,62]},{5:[2,63]},e($,[2,17]),e(G,[2,11]),{13:186,51:P,53:N,54:_},e(G,[2,13]),e(G,[2,14]),e($,[2,19]),e($,[2,35]),e($,[2,36]),e($,[2,37]),e($,[2,38]),{16:[1,187]},e($,[2,39]),{16:[1,188]},e($,[2,40]),e($,[2,41]),{16:[1,189]},e($,[2,42]),{5:[1,190]},{5:[1,191]},{77:[1,192]},{5:[1,193]},{5:[1,194]},{58:195,104:Y},{58:196,104:Y},{58:197,104:Y},{5:[2,75]},{58:198,104:Y},{23:199,73:O},{5:[2,58]},{5:[2,59]},{23:200,73:O},e(G,[2,12]),e(ee,i,{7:124,41:201}),e(q,i,{7:126,43:202}),e(ie,i,{7:129,46:203}),e($,[2,49]),e($,[2,54]),e(j,[2,77]),e($,[2,51]),e($,[2,56]),{5:[2,70]},{5:[2,71]},{5:[2,72]},{5:[2,73]},{58:204,104:Y},{104:[2,66]},{17:[2,48]},{17:[2,46]},{17:[2,44]},{5:[2,74]}],defaultActions:{5:[2,1],6:[2,2],108:[2,68],109:[2,69],150:[2,60],151:[2,105],152:[2,61],153:[2,62],154:[2,63],180:[2,75],183:[2,58],184:[2,59],195:[2,70],196:[2,71],197:[2,72],198:[2,73],200:[2,66],201:[2,48],202:[2,46],203:[2,44],204:[2,74]},parseError:s(function(Me,_e){if(_e.recoverable)this.trace(Me);else{var ze=new Error(Me);throw ze.hash=_e,ze}},"parseError"),parse:s(function(Me){var _e=this,ze=[0],Le=[],qe=[null],ve=[],lt=this.table,Oe="",mt=0,At=0,Gt=0,Ct=2,Rt=1,gt=ve.slice.call(arguments,1),be=Object.create(this.lexer),ct={yy:{}};for(var xt in this.yy)Object.prototype.hasOwnProperty.call(this.yy,xt)&&(ct.yy[xt]=this.yy[xt]);be.setInput(Me,ct.yy),ct.yy.lexer=be,ct.yy.parser=this,typeof be.yylloc>"u"&&(be.yylloc={});var Ke=be.yylloc;ve.push(Ke);var De=be.options&&be.options.ranges;typeof ct.yy.parseError=="function"?this.parseError=ct.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function tt(tr){ze.length=ze.length-2*tr,qe.length=qe.length-tr,ve.length=ve.length-tr}s(tt,"popStack");function Ze(){var tr;return tr=Le.pop()||be.lex()||Rt,typeof tr!="number"&&(tr instanceof Array&&(Le=tr,tr=Le.pop()),tr=_e.symbols_[tr]||tr),tr}s(Ze,"lex");for(var dt,Ut,$t,Yt,Sr,lr,Wt={},ar,et,pt,kt;;){if($t=ze[ze.length-1],this.defaultActions[$t]?Yt=this.defaultActions[$t]:((dt===null||typeof dt>"u")&&(dt=Ze()),Yt=lt[$t]&<[$t][dt]),typeof Yt>"u"||!Yt.length||!Yt[0]){var vr="";kt=[];for(ar in lt[$t])this.terminals_[ar]&&ar>Ct&&kt.push("'"+this.terminals_[ar]+"'");be.showPosition?vr="Parse error on line "+(mt+1)+`: +`+be.showPosition()+` +Expecting `+kt.join(", ")+", got '"+(this.terminals_[dt]||dt)+"'":vr="Parse error on line "+(mt+1)+": Unexpected "+(dt==Rt?"end of input":"'"+(this.terminals_[dt]||dt)+"'"),this.parseError(vr,{text:be.match,token:this.terminals_[dt]||dt,line:be.yylineno,loc:Ke,expected:kt})}if(Yt[0]instanceof Array&&Yt.length>1)throw new Error("Parse Error: multiple actions possible at state: "+$t+", token: "+dt);switch(Yt[0]){case 1:ze.push(dt),qe.push(be.yytext),ve.push(be.yylloc),ze.push(Yt[1]),dt=null,Ut?(dt=Ut,Ut=null):(At=be.yyleng,Oe=be.yytext,mt=be.yylineno,Ke=be.yylloc,Gt>0&&Gt--);break;case 2:if(et=this.productions_[Yt[1]][1],Wt.$=qe[qe.length-et],Wt._$={first_line:ve[ve.length-(et||1)].first_line,last_line:ve[ve.length-1].last_line,first_column:ve[ve.length-(et||1)].first_column,last_column:ve[ve.length-1].last_column},De&&(Wt._$.range=[ve[ve.length-(et||1)].range[0],ve[ve.length-1].range[1]]),lr=this.performAction.apply(Wt,[Oe,At,mt,ct.yy,Yt[1],qe,ve].concat(gt)),typeof lr<"u")return lr;et&&(ze=ze.slice(0,-1*et*2),qe=qe.slice(0,-1*et),ve=ve.slice(0,-1*et)),ze.push(this.productions_[Yt[1]][0]),qe.push(Wt.$),ve.push(Wt._$),pt=lt[ze[ze.length-2]][ze[ze.length-1]],ze.push(pt);break;case 3:return!0}}return!0},"parse")},me=(function(){var Ce={EOF:1,parseError:s(function(_e,ze){if(this.yy.parser)this.yy.parser.parseError(_e,ze);else throw new Error(_e)},"parseError"),setInput:s(function(Me,_e){return this.yy=_e||this.yy||{},this._input=Me,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var Me=this._input[0];this.yytext+=Me,this.yyleng++,this.offset++,this.match+=Me,this.matched+=Me;var _e=Me.match(/(?:\r\n?|\n).*/g);return _e?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),Me},"input"),unput:s(function(Me){var _e=Me.length,ze=Me.split(/(?:\r\n?|\n)/g);this._input=Me+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-_e),this.offset-=_e;var Le=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),ze.length-1&&(this.yylineno-=ze.length-1);var qe=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:ze?(ze.length===Le.length?this.yylloc.first_column:0)+Le[Le.length-ze.length].length-ze[0].length:this.yylloc.first_column-_e},this.options.ranges&&(this.yylloc.range=[qe[0],qe[0]+this.yyleng-_e]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(Me){this.unput(this.match.slice(Me))},"less"),pastInput:s(function(){var Me=this.matched.substr(0,this.matched.length-this.match.length);return(Me.length>20?"...":"")+Me.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var Me=this.match;return Me.length<20&&(Me+=this._input.substr(0,20-Me.length)),(Me.substr(0,20)+(Me.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var Me=this.pastInput(),_e=new Array(Me.length+1).join("-");return Me+this.upcomingInput()+` +`+_e+"^"},"showPosition"),test_match:s(function(Me,_e){var ze,Le,qe;if(this.options.backtrack_lexer&&(qe={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(qe.yylloc.range=this.yylloc.range.slice(0))),Le=Me[0].match(/(?:\r\n?|\n).*/g),Le&&(this.yylineno+=Le.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:Le?Le[Le.length-1].length-Le[Le.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+Me[0].length},this.yytext+=Me[0],this.match+=Me[0],this.matches=Me,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(Me[0].length),this.matched+=Me[0],ze=this.performAction.call(this,this.yy,this,_e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),ze)return ze;if(this._backtrack){for(var ve in qe)this[ve]=qe[ve];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var Me,_e,ze,Le;this._more||(this.yytext="",this.match="");for(var qe=this._currentRules(),ve=0;ve_e[0].length)){if(_e=ze,Le=ve,this.options.backtrack_lexer){if(Me=this.test_match(ze,qe[ve]),Me!==!1)return Me;if(this._backtrack){_e=!1;continue}else return!1}else if(!this.options.flex)break}return _e?(Me=this.test_match(_e,qe[Le]),Me!==!1?Me:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var _e=this.next();return _e||this.lex()},"lex"),begin:s(function(_e){this.conditionStack.push(_e)},"begin"),popState:s(function(){var _e=this.conditionStack.length-1;return _e>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(_e){return _e=this.conditionStack.length-1-Math.abs(_e||0),_e>=0?this.conditionStack[_e]:"INITIAL"},"topState"),pushState:s(function(_e){this.begin(_e)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(_e,ze,Le,qe){var ve=qe;switch(Le){case 0:return 5;case 1:break;case 2:break;case 3:break;case 4:break;case 5:break;case 6:return 20;case 7:return this.begin("CONFIG"),75;break;case 8:return 76;case 9:return this.popState(),this.begin("ALIAS"),77;break;case 10:return this.popState(),this.popState(),77;break;case 11:return ze.yytext=ze.yytext.trim(),73;break;case 12:return ze.yytext=ze.yytext.trim(),this.begin("ALIAS"),73;break;case 13:return ze.yytext=ze.yytext.trim(),this.popState(),73;break;case 14:return this.popState(),10;break;case 15:return ze.yytext=ze.yytext.trim(),this.popState(),10;break;case 16:return this.begin("LINE"),15;break;case 17:return this.begin("ID"),51;break;case 18:return this.begin("ID"),53;break;case 19:return 14;case 20:return this.begin("ID"),54;break;case 21:return this.popState(),this.popState(),this.begin("LINE"),52;break;case 22:return this.popState(),this.popState(),5;break;case 23:return this.begin("LINE"),37;break;case 24:return this.begin("LINE"),38;break;case 25:return this.begin("LINE"),39;break;case 26:return this.begin("LINE"),40;break;case 27:return this.begin("LINE"),50;break;case 28:return this.begin("LINE"),42;break;case 29:return this.begin("LINE"),44;break;case 30:return this.begin("LINE"),49;break;case 31:return this.begin("LINE"),45;break;case 32:return this.begin("LINE"),48;break;case 33:return this.begin("LINE"),47;break;case 34:return this.popState(),16;break;case 35:return 17;case 36:return 67;case 37:return 68;case 38:return 61;case 39:return 62;case 40:return 63;case 41:return 64;case 42:return 59;case 43:return 56;case 44:return this.begin("ID"),22;break;case 45:return this.begin("ID"),24;break;case 46:return 30;case 47:return 31;case 48:return this.begin("acc_title"),32;break;case 49:return this.popState(),"acc_title_value";break;case 50:return this.begin("acc_descr"),34;break;case 51:return this.popState(),"acc_descr_value";break;case 52:this.begin("acc_descr_multiline");break;case 53:this.popState();break;case 54:return"acc_descr_multiline_value";case 55:return 6;case 56:return 19;case 57:return 21;case 58:return 66;case 59:return 5;case 60:return ze.yytext=ze.yytext.trim(),73;break;case 61:return 80;case 62:return 97;case 63:return 98;case 64:return 99;case 65:return 78;case 66:return 79;case 67:return 100;case 68:return 101;case 69:return 102;case 70:return 103;case 71:return 85;case 72:return 86;case 73:return 87;case 74:return 88;case 75:return 93;case 76:return 94;case 77:return 95;case 78:return 96;case 79:return 81;case 80:return 82;case 81:return 83;case 82:return 84;case 83:return 89;case 84:return 90;case 85:return 91;case 86:return 92;case 87:return 104;case 88:return 104;case 89:return 70;case 90:return 71;case 91:return 72;case 92:return 5;case 93:return 10}},"anonymous"),rules:[/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:((?!\n)\s)+)/i,/^(?:#[^\n]*)/i,/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:([0-9]+(\.[0-9]{1,2})?|\.[0-9]{1,2})(?=[ \n]+))/i,/^(?:@\{)/i,/^(?:[^\}]+)/i,/^(?:\}(?=\s+as\s))/i,/^(?:\})/i,/^(?:[^\<->\->:\n,;@\s]+(?=@\{))/i,/^(?:[^<>:\n,;@\s]+(?=\s+as\s))/i,/^(?:[^<>:\n,;@]+(?=\s*[\n;#]|$))/i,/^(?:[^<>:\n,;@]*<[^\n]*)/i,/^(?:[^\n]+)/i,/^(?:box\b)/i,/^(?:participant\b)/i,/^(?:actor\b)/i,/^(?:create\b)/i,/^(?:destroy\b)/i,/^(?:as\b)/i,/^(?:(?:))/i,/^(?:loop\b)/i,/^(?:rect\b)/i,/^(?:opt\b)/i,/^(?:alt\b)/i,/^(?:else\b)/i,/^(?:par\b)/i,/^(?:par_over\b)/i,/^(?:and\b)/i,/^(?:critical\b)/i,/^(?:option\b)/i,/^(?:break\b)/i,/^(?:(?:[:]?(?:no)?wrap)?[^#\n;]*)/i,/^(?:end\b)/i,/^(?:left of\b)/i,/^(?:right of\b)/i,/^(?:links\b)/i,/^(?:link\b)/i,/^(?:properties\b)/i,/^(?:details\b)/i,/^(?:over\b)/i,/^(?:note\b)/i,/^(?:activate\b)/i,/^(?:deactivate\b)/i,/^(?:title\s[^#\n;]+)/i,/^(?:title:\s[^#\n;]+)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:sequenceDiagram\b)/i,/^(?:autonumber\b)/i,/^(?:off\b)/i,/^(?:,)/i,/^(?:;)/i,/^(?:[^\/\\\+\()\+<\->\->:\n,;]+((?!(-x|--x|-\)|--\)|-\|\\|-\\|-\/|-\/\/|-\|\/|\/\|-|\\\|-|\/\/-|\\\\-|\/\|-|--\|\\|--|\(\)))[\-]*[^\+<\->\->:\n,;]+)*)/i,/^(?:->>)/i,/^(?:<<->>)/i,/^(?:-->>)/i,/^(?:<<-->>)/i,/^(?:->)/i,/^(?:-->)/i,/^(?:-[x])/i,/^(?:--[x])/i,/^(?:-[\)])/i,/^(?:--[\)])/i,/^(?:--\|\\)/i,/^(?:--\|\/)/i,/^(?:--\\\\)/i,/^(?:--\/\/)/i,/^(?:\/\|--)/i,/^(?:\\\|--)/i,/^(?:\/\/--)/i,/^(?:\\\\--)/i,/^(?:-\|\\)/i,/^(?:-\|\/)/i,/^(?:-\\\\)/i,/^(?:-\/\/)/i,/^(?:\/\|-)/i,/^(?:\\\|-)/i,/^(?:\/\/-)/i,/^(?:\\\\-)/i,/^(?::(?:(?:no)?wrap)?[^#\n;]*)/i,/^(?::)/i,/^(?:\+)/i,/^(?:-)/i,/^(?:\(\))/i,/^(?:$)/i,/^(?:.)/i],conditions:{acc_descr_multiline:{rules:[53,54],inclusive:!1},acc_descr:{rules:[51],inclusive:!1},acc_title:{rules:[49],inclusive:!1},ID:{rules:[2,3,7,11,12,13,14,15],inclusive:!1},ALIAS:{rules:[2,3,21,22],inclusive:!1},LINE:{rules:[2,3,34],inclusive:!1},CONFIG:{rules:[8,9,10],inclusive:!1},CONFIG_DATA:{rules:[],inclusive:!1},INITIAL:{rules:[0,1,3,4,5,6,16,17,18,19,20,23,24,25,26,27,28,29,30,31,32,33,35,36,37,38,39,40,41,42,43,44,45,46,47,48,50,52,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93],inclusive:!0}}};return Ce})();Ae.lexer=me;function Se(){this.yy={}}return s(Se,"Parser"),Se.prototype=Ae,Ae.Parser=Se,new Se})();uY.parser=uY;j8e=uY});var tAt,rAt,nAt,FS,iL,hY=F(()=>{"use strict";Jt();P2();St();B_();Yr();$n();tAt={SOLID:0,DOTTED:1,NOTE:2,SOLID_CROSS:3,DOTTED_CROSS:4,SOLID_OPEN:5,DOTTED_OPEN:6,LOOP_START:10,LOOP_END:11,ALT_START:12,ALT_ELSE:13,ALT_END:14,OPT_START:15,OPT_END:16,ACTIVE_START:17,ACTIVE_END:18,PAR_START:19,PAR_AND:20,PAR_END:21,RECT_START:22,RECT_END:23,SOLID_POINT:24,DOTTED_POINT:25,AUTONUMBER:26,CRITICAL_START:27,CRITICAL_OPTION:28,CRITICAL_END:29,BREAK_START:30,BREAK_END:31,PAR_OVER_START:32,BIDIRECTIONAL_SOLID:33,BIDIRECTIONAL_DOTTED:34,SOLID_TOP:41,SOLID_BOTTOM:42,STICK_TOP:43,STICK_BOTTOM:44,SOLID_ARROW_TOP_REVERSE:45,SOLID_ARROW_BOTTOM_REVERSE:46,STICK_ARROW_TOP_REVERSE:47,STICK_ARROW_BOTTOM_REVERSE:48,SOLID_TOP_DOTTED:51,SOLID_BOTTOM_DOTTED:52,STICK_TOP_DOTTED:53,STICK_BOTTOM_DOTTED:54,SOLID_ARROW_TOP_REVERSE_DOTTED:55,SOLID_ARROW_BOTTOM_REVERSE_DOTTED:56,STICK_ARROW_TOP_REVERSE_DOTTED:57,STICK_ARROW_BOTTOM_REVERSE_DOTTED:58,CENTRAL_CONNECTION:59,CENTRAL_CONNECTION_REVERSE:60,CENTRAL_CONNECTION_DUAL:61},rAt={FILLED:0,OPEN:1},nAt={LEFTOF:0,RIGHTOF:1,OVER:2},FS={ACTOR:"actor",BOUNDARY:"boundary",COLLECTIONS:"collections",CONTROL:"control",DATABASE:"database",ENTITY:"entity",PARTICIPANT:"participant",QUEUE:"queue"},iL=class{constructor(){this.state=new wp(()=>({prevActor:void 0,actors:new Map,createdActors:new Map,destroyedActors:new Map,boxes:[],messages:[],notes:[],sequenceNumbersEnabled:!1,wrapEnabled:void 0,currentBox:void 0,lastCreated:void 0,lastDestroyed:void 0}));this.setAccTitle=Lr;this.setAccDescription=Mr;this.setDiagramTitle=Gr;this.getAccTitle=Ir;this.getAccDescription=Nr;this.getDiagramTitle=Pr;this.apply=this.apply.bind(this),this.parseBoxData=this.parseBoxData.bind(this),this.parseMessage=this.parseMessage.bind(this),this.clear(),this.setWrap(Pe().wrap),this.LINETYPE=tAt,this.ARROWTYPE=rAt,this.PLACEMENT=nAt}static{s(this,"SequenceDB")}addBox(t){this.state.records.boxes.push({name:t.text,wrap:t.wrap??this.autoWrap(),fill:t.color,actorKeys:[]}),this.state.records.currentBox=this.state.records.boxes.slice(-1)[0]}addActor(t,r,n,i,a){let o=this.state.records.currentBox,l;if(a!==void 0){let h;a.includes(` +`)?h=a+` +`:h=`{ +`+a+` +}`,l=af(h,{schema:nf})}i=l?.type??i,l?.alias&&(!n||n.text===r)&&(n={text:l.alias,wrap:n?.wrap,type:i});let u=this.state.records.actors.get(t);if(u){if(this.state.records.currentBox&&u.box&&this.state.records.currentBox!==u.box)throw new Error(`A same participant should only be defined in one Box: ${u.name} can't be in '${u.box.name}' and in '${this.state.records.currentBox.name}' at the same time.`);if(o=u.box?u.box:this.state.records.currentBox,u.box=o,u&&r===u.name&&n==null)return}if(n?.text==null&&(n={text:r,type:i}),(i==null||n.text==null)&&(n={text:r,type:i}),this.state.records.actors.set(t,{box:o,name:r,description:n.text,wrap:n.wrap??this.autoWrap(),prevActor:this.state.records.prevActor,links:{},properties:{},actorCnt:null,rectData:null,type:i??"participant"}),this.state.records.prevActor){let h=this.state.records.actors.get(this.state.records.prevActor);h&&(h.nextActor=t)}this.state.records.currentBox&&this.state.records.currentBox.actorKeys.push(t),this.state.records.prevActor=t}activationCount(t){let r,n=0;if(!t)return 0;for(r=0;r>-",token:"->>-",line:"1",loc:{first_line:1,last_line:1,first_column:1,last_column:1},expected:["'ACTIVE_PARTICIPANT'"]},u}return this.state.records.messages.push({id:this.state.records.messages.length.toString(),from:t,to:r,message:n?.text??"",wrap:n?.wrap??this.autoWrap(),type:i,activate:a,centralConnection:o??0}),!0}hasAtLeastOneBox(){return this.state.records.boxes.length>0}hasAtLeastOneBoxWithTitle(){return this.state.records.boxes.some(t=>t.name)}getMessages(){return this.state.records.messages}getBoxes(){return this.state.records.boxes}getActors(){return this.state.records.actors}getCreatedActors(){return this.state.records.createdActors}getDestroyedActors(){return this.state.records.destroyedActors}getActor(t){return this.state.records.actors.get(t)}getActorKeys(){return[...this.state.records.actors.keys()]}enableSequenceNumbers(){this.state.records.sequenceNumbersEnabled=!0}disableSequenceNumbers(){this.state.records.sequenceNumbersEnabled=!1}showSequenceNumbers(){return this.state.records.sequenceNumbersEnabled}setWrap(t){this.state.records.wrapEnabled=t}extractWrap(t){if(t===void 0)return{};t=t.trim();let r=/^:?wrap:/.exec(t)!==null?!0:/^:?nowrap:/.exec(t)!==null?!1:void 0;return{cleanedText:(r===void 0?t:t.replace(/^:?(?:no)?wrap:/,"")).trim(),wrap:r}}autoWrap(){return this.state.records.wrapEnabled!==void 0?this.state.records.wrapEnabled:Pe().sequence?.wrap??!1}clear(){this.state.reset(),Tr()}parseMessage(t){let r=t.trim(),{wrap:n,cleanedText:i}=this.extractWrap(r),a={text:i,wrap:n};return se.debug(`parseMessage: ${JSON.stringify(a)}`),a}parseBoxData(t){let r=/^((?:rgba?|hsla?)\s*\(.*\)|\w*)(.*)$/.exec(t),n=r?.[1]?r[1].trim():"transparent",i=r?.[2]?r[2].trim():void 0;if(window?.CSS)window.CSS.supports("color",n)||(n="transparent",i=t.trim());else{let l=new Option().style;l.color=n,l.color!==n&&(n="transparent",i=t.trim())}let{wrap:a,cleanedText:o}=this.extractWrap(i);return{text:o?xr(o,Pe()):void 0,color:n,wrap:a}}addNote(t,r,n){let i={actor:t,placement:r,message:n.text,wrap:n.wrap??this.autoWrap()},a=[].concat(t,t);this.state.records.notes.push(i),this.state.records.messages.push({id:this.state.records.messages.length.toString(),from:a[0],to:a[1],message:n.text,wrap:n.wrap??this.autoWrap(),type:this.LINETYPE.NOTE,placement:r})}addLinks(t,r){let n=this.getActor(t);try{let i=xr(r.text,Pe());i=i.replace(/=/g,"="),i=i.replace(/&/g,"&");let a=JSON.parse(i);this.insertLinks(n,a)}catch(i){se.error("error while parsing actor link text",i)}}addALink(t,r){let n=this.getActor(t);try{let i={},a=xr(r.text,Pe()),o=a.indexOf("@");a=a.replace(/=/g,"="),a=a.replace(/&/g,"&");let l=a.slice(0,o-1).trim(),u=a.slice(o+1).trim();i[l]=u,this.insertLinks(n,i)}catch(i){se.error("error while parsing actor link text",i)}}insertLinks(t,r){if(t.links==null)t.links=r;else for(let n in r)t.links[n]=r[n]}addProperties(t,r){let n=this.getActor(t);try{let i=xr(r.text,Pe()),a=JSON.parse(i);this.insertProperties(n,a)}catch(i){se.error("error while parsing actor properties text",i)}}insertProperties(t,r){if(t.properties==null)t.properties=r;else for(let n in r)t.properties[n]=r[n]}boxEnd(){this.state.records.currentBox=void 0}addDetails(t,r){let n=this.getActor(t),i=document.getElementById(r.text);try{let a=i.innerHTML,o=JSON.parse(a);o.properties&&this.insertProperties(n,o.properties),o.links&&this.insertLinks(n,o.links)}catch(a){se.error("error while parsing actor details text",a)}}getActorProperty(t,r){if(t?.properties!==void 0)return t.properties[r]}apply(t){if(Array.isArray(t))t.forEach(r=>{this.apply(r)});else switch(t.type){case"sequenceIndex":this.state.records.messages.push({id:this.state.records.messages.length.toString(),from:void 0,to:void 0,message:{start:t.sequenceIndex,step:t.sequenceIndexStep,visible:t.sequenceVisible},wrap:!1,type:t.signalType});break;case"addParticipant":this.addActor(t.actor,t.actor,t.description,t.draw,t.config);break;case"createParticipant":if(this.state.records.actors.has(t.actor))throw new Error("It is not possible to have actors with the same id, even if one is destroyed before the next is created. Use 'AS' aliases to simulate the behavior");this.state.records.lastCreated=t.actor,this.addActor(t.actor,t.actor,t.description,t.draw,t.config),this.state.records.createdActors.set(t.actor,this.state.records.messages.length);break;case"destroyParticipant":this.state.records.lastDestroyed=t.actor,this.state.records.destroyedActors.set(t.actor,this.state.records.messages.length);break;case"activeStart":this.addSignal(t.actor,void 0,void 0,t.signalType);break;case"centralConnection":this.addSignal(t.actor,void 0,void 0,t.signalType);break;case"centralConnectionReverse":this.addSignal(t.actor,void 0,void 0,t.signalType);break;case"activeEnd":this.addSignal(t.actor,void 0,void 0,t.signalType);break;case"addNote":this.addNote(t.actor,t.placement,t.text);break;case"addLinks":this.addLinks(t.actor,t.text);break;case"addALink":this.addALink(t.actor,t.text);break;case"addProperties":this.addProperties(t.actor,t.text);break;case"addDetails":this.addDetails(t.actor,t.text);break;case"addMessage":if(this.state.records.lastCreated){if(t.to!==this.state.records.lastCreated)throw new Error("The created participant "+this.state.records.lastCreated.name+" does not have an associated creating message after its declaration. Please check the sequence diagram.");this.state.records.lastCreated=void 0}else if(this.state.records.lastDestroyed){if(t.to!==this.state.records.lastDestroyed&&t.from!==this.state.records.lastDestroyed)throw new Error("The destroyed participant "+this.state.records.lastDestroyed.name+" does not have an associated destroying message after its declaration. Please check the sequence diagram.");this.state.records.lastDestroyed=void 0}this.addSignal(t.from,t.to,t.msg,t.signalType,t.activate,t.centralConnection);break;case"boxStart":this.addBox(t.boxData);break;case"boxEnd":this.boxEnd();break;case"loopStart":this.addSignal(void 0,void 0,t.loopText,t.signalType);break;case"loopEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"rectStart":this.addSignal(void 0,void 0,t.color,t.signalType);break;case"rectEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"optStart":this.addSignal(void 0,void 0,t.optText,t.signalType);break;case"optEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"altStart":this.addSignal(void 0,void 0,t.altText,t.signalType);break;case"else":this.addSignal(void 0,void 0,t.altText,t.signalType);break;case"altEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"setAccTitle":Lr(t.text);break;case"parStart":this.addSignal(void 0,void 0,t.parText,t.signalType);break;case"and":this.addSignal(void 0,void 0,t.parText,t.signalType);break;case"parEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"criticalStart":this.addSignal(void 0,void 0,t.criticalText,t.signalType);break;case"option":this.addSignal(void 0,void 0,t.optionText,t.signalType);break;case"criticalEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"breakStart":this.addSignal(void 0,void 0,t.breakText,t.signalType);break;case"breakEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break}}getConfig(){return Pe().sequence}}});var iAt,K8e,Z8e=F(()=>{"use strict";Jt();iAt=s(e=>{let t=e.dropShadow??"none",{look:r}=Pe();return`.actor { + stroke: ${e.actorBorder}; + fill: ${e.actorBkg}; + stroke-width: ${e.strokeWidth??1}; + } + + rect.actor.outer-path[data-look="neo"] { + filter: ${t}; + } + + rect.note[data-look="neo"] { + stroke:${e.noteBorderColor}; + fill:${e.noteBkgColor}; + filter: ${t}; + } + + text.actor > tspan { + fill: ${e.actorTextColor}; + stroke: none; + } + + .actor-line { + stroke: ${e.actorLineColor}; + } + + .innerArc { + stroke-width: 1.5; + stroke-dasharray: none; + } + + .messageLine0 { + stroke-width: 1.5; + stroke-dasharray: none; + stroke: ${e.signalColor}; + } + + .messageLine1 { + stroke-width: 1.5; + stroke-dasharray: 2, 2; + stroke: ${e.signalColor}; + } + + [id$="-arrowhead"] path { + fill: ${e.signalColor}; + stroke: ${e.signalColor}; + } + + .sequenceNumber { + fill: ${e.sequenceNumberColor}; + } + + [id$="-sequencenumber"] { + fill: ${e.signalColor}; + } + + [id$="-crosshead"] path { + fill: ${e.signalColor}; + stroke: ${e.signalColor}; + } + + .messageText { + fill: ${e.signalTextColor}; + stroke: none; + } + + .labelBox { + stroke: ${e.labelBoxBorderColor}; + fill: ${e.labelBoxBkgColor}; + filter: ${r==="neo"?t:"none"}; + } + + .labelText, .labelText > tspan { + fill: ${e.labelTextColor}; + stroke: none; + } + + .loopText, .loopText > tspan { + fill: ${e.loopTextColor}; + stroke: none; + } + + .sectionTitle, .sectionTitle > tspan { + fill: ${e.loopTextColor}; + stroke: none; + } + + .loopLine { + stroke-width: 2px; + stroke-dasharray: 2, 2; + stroke: ${e.labelBoxBorderColor}; + fill: ${e.labelBoxBorderColor}; + } + + .note { + //stroke: #decc93; + stroke: ${e.noteBorderColor}; + fill: ${e.noteBkgColor}; + } + + .noteText, .noteText > tspan { + fill: ${e.noteTextColor}; + stroke: none; + ${e.noteFontWeight?`font-weight: ${e.noteFontWeight};`:""} + } + + .activation0 { + fill: ${e.activationBkgColor}; + stroke: ${e.activationBorderColor}; + } + + .activation1 { + fill: ${e.activationBkgColor}; + stroke: ${e.activationBorderColor}; + } + + .activation2 { + fill: ${e.activationBkgColor}; + stroke: ${e.activationBorderColor}; + } + + .actorPopupMenu { + position: absolute; + } + + .actorPopupMenuPanel { + position: absolute; + fill: ${e.actorBkg}; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4)); +} + .actor-man circle, line { + fill: ${e.actorBkg}; + stroke-width: 2px; + } + + g rect.rect { + filter: ${t}; + stroke: ${e.nodeBorder}; + } +`},"getStyles"),K8e=iAt});var dY,Rp,_p,Lp,aL,w0,dd,GS,aAt,sL,zS,k0,Q8e,cn,fY,sAt,oAt,lAt,cAt,uAt,hAt,dAt,fAt,pAt,mAt,gAt,yAt,vAt,J8e,xAt,bAt,TAt,CAt,SAt,wAt,kAt,EAt,eMe,AAt,fd,RAt,_At,LAt,DAt,IAt,Kn,tMe=F(()=>{"use strict";dY=Rs(ub(),1);br();er();Yr();Xd();Rp=36,_p="actor-top",Lp="actor-bottom",aL="actor-box",w0="actor-man",dd=new Set(["redux-color","redux-dark-color"]),GS=s(function(e,t){let r=gm(e,t);return Lt().look==="neo"&&r.attr("data-look","neo"),r},"drawRect"),aAt=s(function(e,t,r,n,i){if(t.links===void 0||t.links===null||Object.keys(t.links).length===0)return{height:0,width:0};let a=t.links,o=t.actorCnt,l=t.rectData;var u="none";i&&(u="block !important");let h=e.append("g");h.attr("id","actor"+o+"_popup"),h.attr("class","actorPopupMenu"),h.attr("display",u);var d="";l.class!==void 0&&(d=" "+l.class);let f=l.width>r?l.width:r,p=h.append("rect");if(p.attr("class","actorPopupMenuPanel"+d),p.attr("x",l.x),p.attr("y",l.height),p.attr("fill",l.fill),p.attr("stroke",l.stroke),p.attr("width",f),p.attr("height",l.height),p.attr("rx",l.rx),p.attr("ry",l.ry),a!=null){var m=20;for(let v in a){var g=h.append("a"),y=(0,dY.sanitizeUrl)(a[v]);g.attr("xlink:href",y),g.attr("target","_blank"),RAt(n)(v,g,l.x+10,l.height+m,f,20,{class:"actor"},n),m+=30}}return p.attr("height",m),{height:l.height+m,width:f}},"drawPopup"),sL=s(function(e){return"var pu = document.getElementById('"+e+"'); if (pu != null) { pu.style.display = pu.style.display == 'block' ? 'none' : 'block'; }"},"popupMenuToggle"),zS=s(async function(e,t,r=null){let n=e.append("foreignObject"),i=await J0(t.text,Lt()),o=n.append("xhtml:div").attr("style","width: fit-content;").attr("xmlns","http://www.w3.org/1999/xhtml").html(i).node().getBoundingClientRect();if(n.attr("height",Math.round(o.height)).attr("width",Math.round(o.width)),t.class==="noteText"){let l=e.node().firstChild;l.setAttribute("height",o.height+2*t.textMargin);let u=l.getBBox();n.attr("x",Math.round(u.x+u.width/2-o.width/2)).attr("y",Math.round(u.y+u.height/2-o.height/2))}else if(r){let{startx:l,stopx:u,starty:h}=r;if(l>u){let d=l;l=u,u=d}n.attr("x",Math.round(l+Math.abs(l-u)/2-o.width/2)),t.class==="loopText"?n.attr("y",Math.round(h)):n.attr("y",Math.round(h-o.height))}return[n]},"drawKatex"),k0=s(function(e,t){let r=0,n=0,i=t.text.split(Tt.lineBreakRegex),[a,o]=Bs(t.fontSize),l=[],u=0,h=s(()=>t.y,"yfunc");if(t.valign!==void 0&&t.textMargin!==void 0&&t.textMargin>0)switch(t.valign){case"top":case"start":h=s(()=>Math.round(t.y+t.textMargin),"yfunc");break;case"middle":case"center":h=s(()=>Math.round(t.y+(r+n+t.textMargin)/2),"yfunc");break;case"bottom":case"end":h=s(()=>Math.round(t.y+(r+n+2*t.textMargin)-t.textMargin),"yfunc");break}if(t.anchor!==void 0&&t.textMargin!==void 0&&t.width!==void 0)switch(t.anchor){case"left":case"start":t.x=Math.round(t.x+t.textMargin),t.anchor="start",t.dominantBaseline="middle",t.alignmentBaseline="middle";break;case"middle":case"center":t.x=Math.round(t.x+t.width/2),t.anchor="middle",t.dominantBaseline="middle",t.alignmentBaseline="middle";break;case"right":case"end":t.x=Math.round(t.x+t.width-t.textMargin),t.anchor="end",t.dominantBaseline="middle",t.alignmentBaseline="middle";break}for(let[d,f]of i.entries()){t.textMargin!==void 0&&t.textMargin===0&&a!==void 0&&(u=d*a);let p=e.append("text");p.attr("x",t.x),p.attr("y",h()),t.anchor!==void 0&&p.attr("text-anchor",t.anchor).attr("dominant-baseline",t.dominantBaseline).attr("alignment-baseline",t.alignmentBaseline),t.fontFamily!==void 0&&p.style("font-family",t.fontFamily),o!==void 0&&p.style("font-size",o),t.fontWeight!==void 0&&p.style("font-weight",t.fontWeight),t.fill!==void 0&&p.attr("fill",t.fill),t.class!==void 0&&p.attr("class",t.class),t.dy!==void 0?p.attr("dy",t.dy):u!==0&&p.attr("dy",u);let m=f||WN;if(t.tspan){let g=p.append("tspan");g.attr("x",t.x),t.fill!==void 0&&g.attr("fill",t.fill),g.text(m)}else p.text(m);t.valign!==void 0&&t.textMargin!==void 0&&t.textMargin>0&&(n+=(p._groups||p)[0][0].getBBox().height,r=n),l.push(p)}return l},"drawText"),Q8e=s(function(e,t){function r(i,a,o,l,u){return i+","+a+" "+(i+o)+","+a+" "+(i+o)+","+(a+l-u)+" "+(i+o-u*1.2)+","+(a+l)+" "+i+","+(a+l)}s(r,"genPoints");let n=e.append("polygon");return n.attr("points",r(t.x,t.y,t.width,t.height,7)),n.attr("class","labelBox"),t.y=t.y+t.height/2,k0(e,t),n},"drawLabel"),cn=-1,fY=s((e,t,r,n)=>{e.select&&r.forEach(i=>{let a=t.get(i),o=e.select("#actor"+a.actorCnt);!n.mirrorActors&&a.stopy?o.attr("y2",a.stopy+a.height/2):n.mirrorActors&&o.attr("y2",a.stopy)})},"fixLifeLineHeights"),sAt=s(function(e,t,r,n,i){let a=n?t.stopy:t.starty,o=t.x+t.width/2,l=a+t.height,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p}=d,m=e.append("g").lower();var g=m;n||(cn++,Object.keys(t.links||{}).length&&!r.forceMenus&&g.attr("onclick",sL(`actor${cn}_popup`)).attr("cursor","pointer"),g.append("line").attr("id","actor"+cn).attr("x1",o).attr("y1",l).attr("x2",o).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),g=m.append("g"),t.actorCnt=cn,t.links!=null&&g.attr("id","root-"+cn),u==="neo"&&g.attr("data-look","neo"));let y=Os();var v="actor";t.properties?.class?v=t.properties.class:y.fill="#eaeaea",n?v+=` ${Lp}`:v+=` ${_p}`,y.x=t.x,y.y=a,y.width=t.width,y.height=t.height,y.class=v,y.rx=3,y.ry=3,y.name=t.name,u==="neo"&&(y.rx=6,y.ry=6);let x=GS(g,y),b=i.get(t.name)??0;if(dd.has(h)&&(x.style("stroke",p[b%p.length]),x.style("fill",f[b%p.length])),u==="neo"&&x.attr("filter","url(#drop-shadow)"),t.rectData=y,t.properties?.icon){let w=t.properties.icon.trim();w.charAt(0)==="@"?PE(g,y.x+y.width-20,y.y+10,w.substr(1)):NE(g,y.x+y.width-20,y.y+10,w)}n||(g.attr("data-et","participant"),g.attr("data-type","participant"),g.attr("data-id",t.name)),fd(r,si(t.description))(t.description,g,y.x,y.y,y.width,y.height,{class:`actor ${aL}`},r);let T=t.height;if(x.node){let w=x.node().getBBox();t.height=w.height,T=w.height}return T},"drawActorTypeParticipant"),oAt=s(function(e,t,r,n,i){let a=n?t.stopy:t.starty,o=t.x+t.width/2,l=a+t.height,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p}=d,m=e.append("g").lower();var g=m;n||(cn++,Object.keys(t.links||{}).length&&!r.forceMenus&&g.attr("onclick",sL(`actor${cn}_popup`)).attr("cursor","pointer"),g.append("line").attr("id","actor"+cn).attr("x1",o).attr("y1",l).attr("x2",o).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),g=m.append("g"),t.actorCnt=cn,t.links!=null&&g.attr("id","root-"+cn),u==="neo"&&g.attr("data-look","neo"));let y=Os();var v="actor";t.properties?.class?v=t.properties.class:y.fill="#eaeaea",n?v+=` ${Lp}`:v+=` ${_p}`,y.x=t.x,y.y=a,y.width=t.width,y.height=t.height,y.class=v,y.name=t.name;let x=6,b={...y,x:y.x+-x,y:y.y+ +x,class:"actor"},T=GS(g,y),w=GS(g,b);t.rectData=y,u==="neo"&&g.attr("filter","url(#drop-shadow)");let C=i.get(t.name)??0;if(dd.has(h)&&(T.style("stroke",p[C%p.length]),T.style("fill",f[C%p.length]),w.style("stroke",p[C%p.length]),w.style("fill",f[C%p.length])),t.properties?.icon){let k=t.properties.icon.trim();k.charAt(0)==="@"?PE(g,y.x+y.width-20,y.y+10,k.substr(1)):NE(g,y.x+y.width-20,y.y+10,k)}fd(r,si(t.description))(t.description,g,y.x-x,y.y+x,y.width,y.height,{class:`actor ${aL}`},r);let S=t.height;if(T.node){let k=T.node().getBBox();t.height=k.height,S=k.height}return n||(g.attr("data-et","participant"),g.attr("data-type","collections"),g.attr("data-id",t.name)),S},"drawActorTypeCollections"),lAt=s(function(e,t,r,n,i){let a=n?t.stopy:t.starty,o=t.x+t.width/2,l=a+t.height,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p}=d,m=e.append("g").lower(),g=m;n||(cn++,Object.keys(t.links||{}).length&&!r.forceMenus&&g.attr("onclick",sL(`actor${cn}_popup`)).attr("cursor","pointer"),g.append("line").attr("id","actor"+cn).attr("x1",o).attr("y1",l).attr("x2",o).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),g=m.append("g"),t.actorCnt=cn,t.links!=null&&g.attr("id","root-"+cn),u==="neo"&&g.attr("data-look","neo"));let y=Os(),v="actor";t.properties?.class?v=t.properties.class:y.fill="#eaeaea",n?v+=` ${Lp}`:v+=` ${_p}`,g.attr("class",v),y.x=t.x,y.y=a,y.width=t.width,y.height=t.height,y.name=t.name;let x=y.height/2,b=x/(2.5+y.height/50),T=g.append("g"),w=g.append("g"),C=`M ${y.x},${y.y+x} + a ${b},${x} 0 0 0 0,${y.height} + h ${y.width-2*b} + a ${b},${x} 0 0 0 0,-${y.height} + Z + `;T.append("path").attr("d",C),w.append("path").attr("d",`M ${y.x},${y.y+x} + a ${b},${x} 0 0 0 0,${y.height}`),T.attr("transform",`translate(${b}, ${-(y.height/2)})`),w.attr("transform",`translate(${y.width-b}, ${-y.height/2})`),t.rectData=y,u==="neo"&&T.attr("filter","url(#drop-shadow)");let S=i.get(t.name)??0;if(dd.has(h)&&(T.style("stroke",p[S%p.length]),T.style("fill",f[S%p.length]),w.style("stroke",p[S%p.length]),w.style("fill",f[S%p.length])),t.properties?.icon){let M=t.properties.icon.trim(),P=y.x+y.width-20,N=y.y+10;M.charAt(0)==="@"?PE(g,P,N,M.substr(1)):NE(g,P,N,M)}fd(r,si(t.description))(t.description,g,y.x,y.y,y.width,y.height,{class:`actor ${aL}`},r);let k=t.height,R=T.select("path:last-child");if(R.node()){let M=R.node().getBBox();t.height=M.height,k=M.height}return n||(g.attr("data-et","participant"),g.attr("data-type","queue"),g.attr("data-id",t.name)),k},"drawActorTypeQueue"),cAt=s(function(e,t,r,n,i,a){let o=n?t.stopy:t.starty,l=t.x+t.width/2,u=o+75,{look:h,theme:d,themeVariables:f}=r,{bkgColorArray:p,borderColorArray:m,actorBorder:g,actorBkg:y}=f,v=e.append("g").lower();n||(cn++,v.append("line").attr("id","actor"+cn).attr("x1",l).attr("y1",u).attr("x2",l).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),t.actorCnt=cn);let x=e.append("g"),b=w0;n?b+=` ${Lp}`:b+=` ${_p}`,x.attr("class",b),x.attr("name",t.name);let T=Os();T.x=t.x,T.y=o,T.fill="#eaeaea",T.width=t.width,T.height=t.height,T.class="actor";let w=t.x+t.width/2,C=o+32,S=22;x.append("defs").append("marker").attr("id",i+"-filled-head-control").attr("refX",11).attr("refY",5.8).attr("markerWidth",20).attr("markerHeight",28).attr("orient","172.5").attr("stroke-width",1.2).append("path").attr("d","M 14.4 5.6 L 7.2 10.4 L 8.8 5.6 L 7.2 0.8 Z"),x.append("circle").attr("cx",w).attr("cy",C).attr("r",S).attr("filter",`${h==="neo"?"url(#drop-shadow)":""}`),x.append("line").attr("marker-end","url(#"+i+"-filled-head-control)").attr("transform",`translate(${w}, ${C-S})`);let k=a.get(t.name)??0;dd.has(d)?(x.style("stroke",m[k%m.length]),x.style("fill",p[k%m.length])):(x.style("stroke",g),x.style("fill",y));let R=x.node().getBBox();return t.height=R.height+2*(r?.sequence?.labelBoxHeight??0),fd(r,si(t.description))(t.description,x,T.x,T.y+S+(n?5:12),T.width,T.height,{class:`actor ${w0}`},r),n||(x.attr("data-et","participant"),x.attr("data-type","control"),x.attr("data-id",t.name)),t.height},"drawActorTypeControl"),uAt=s(function(e,t,r,n,i){let a=n?t.stopy:t.starty,o=t.x+t.width/2,l=a+75,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p}=d,m=e.append("g").lower(),g=e.append("g"),y="actor";n?y+=` ${Lp}`:y+=` ${_p}`,g.attr("class",y),g.attr("name",t.name);let v=Os();v.x=t.x,v.y=a,v.fill="#eaeaea",v.width=t.width,v.height=t.height,v.class="actor";let x=t.x+t.width/2,b=a+(n?10:25),T=22;g.append("circle").attr("cx",x).attr("cy",b).attr("r",T).attr("width",t.width).attr("height",t.height),g.append("line").attr("x1",x-T).attr("x2",x+T).attr("y1",b+T).attr("y2",b+T).attr("stroke-width",2),u==="neo"&&g.attr("filter","url(#drop-shadow)");let w=i.get(t.name)??0;dd.has(h)&&(g.style("stroke",p[w%p.length]),g.style("fill",f[w%p.length]));let C=g.node().getBBox();return t.height=C.height+(r?.sequence?.labelBoxHeight??0),n||(cn++,m.append("line").attr("id","actor"+cn).attr("x1",o).attr("y1",l).attr("x2",o).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),t.actorCnt=cn),fd(r,si(t.description))(t.description,g,v.x,v.y+(n?15:30),v.width,v.height,{class:`actor ${w0}`},r),n?g.attr("transform",`translate(0, ${T})`):(g.attr("transform",`translate(0, ${T/2-5})`),g.attr("data-et","participant"),g.attr("data-type","entity"),g.attr("data-id",t.name)),t.height},"drawActorTypeEntity"),hAt=s(function(e,t,r,n,i){let a=n?t.stopy:t.starty,o=t.x+t.width/2,l=a+t.height+2*r.boxTextMargin,{theme:u,themeVariables:h,look:d}=r,{bkgColorArray:f,borderColorArray:p,actorBorder:m}=h,g=e.append("g").lower(),y=g;n||(cn++,Object.keys(t.links||{}).length&&!r.forceMenus&&y.attr("onclick",sL(`actor${cn}_popup`)).attr("cursor","pointer"),y.append("line").attr("id","actor"+cn).attr("x1",o).attr("y1",l).attr("x2",o).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),y=g.append("g"),t.actorCnt=cn,t.links!=null&&y.attr("id","root-"+cn),d==="neo"&&y.attr("data-look","neo"));let v=Os(),x="actor";t.properties?.class?x=t.properties.class:v.fill="#eaeaea",n?x+=` ${Lp}`:x+=` ${_p}`,v.x=t.x,v.y=a,v.width=t.width,v.height=t.height,v.class=x,v.name=t.name,v.x=t.x,v.y=a;let b=v.width/3,T=v.width/3,w=b/2,C=w/(2.5+b/50),S=y.append("g");S.attr("class",x);let k=` + M ${v.x},${v.y+C} + a ${w},${C} 0 0 0 ${b},0 + a ${w},${C} 0 0 0 -${b},0 + l 0,${T-2*C} + a ${w},${C} 0 0 0 ${b},0 + l 0,-${T-2*C} +`;S.append("path").attr("d",k),d==="neo"&&S.attr("filter","url(#drop-shadow)");let R=i.get(t.name)??0;dd.has(u)?(S.style("stroke",p[R%p.length]),S.style("fill",f[R%p.length])):S.style("stroke",m),S.attr("transform",`translate(${b}, ${C})`),t.rectData=v,fd(r,si(t.description))(t.description,y,v.x,v.y+35,v.width,v.height,{class:`actor ${aL}`},r);let M=S.select("path:last-child");if(M.node()){let P=M.node().getBBox();t.height=P.height+(r.sequence.labelBoxHeight??0)}return n||(y.attr("data-et","participant"),y.attr("data-type","database"),y.attr("data-id",t.name)),t.height},"drawActorTypeDatabase"),dAt=s(function(e,t,r,n,i){let a=n?t.stopy:t.starty,o=t.x+t.width/2,l=a+80,u=22,h=e.append("g").lower(),{look:d,theme:f,themeVariables:p}=r,{bkgColorArray:m,borderColorArray:g,actorBorder:y}=p;n||(cn++,h.append("line").attr("id","actor"+cn).attr("x1",o).attr("y1",l).attr("x2",o).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),t.actorCnt=cn);let v=e.append("g"),x=w0;n?x+=` ${Lp}`:x+=` ${_p}`,v.attr("class",x),v.attr("name",t.name);let b=Os();b.x=t.x,b.y=a,b.fill="#eaeaea",b.width=t.width,b.height=t.height,b.class="actor",v.append("line").attr("id","actor-man-torso"+cn).attr("x1",t.x+t.width/2-u*2.5).attr("y1",a+12).attr("x2",t.x+t.width/2-15).attr("y2",a+12),v.append("line").attr("id","actor-man-arms"+cn).attr("x1",t.x+t.width/2-u*2.5).attr("y1",a+2).attr("x2",t.x+t.width/2-u*2.5).attr("y2",a+22),v.append("circle").attr("cx",t.x+t.width/2).attr("cy",a+12).attr("r",u),d==="neo"&&v.attr("filter","url(#drop-shadow)");let T=i.get(t.name)??0;dd.has(f)?(v.style("stroke",g[T%g.length]),v.style("fill",m[T%g.length])):v.style("stroke",y);let w=v.node().getBBox();return t.height=w.height+(r.sequence.labelBoxHeight??0),fd(r,si(t.description))(t.description,v,b.x,b.y+15,b.width,b.height,{class:`actor ${w0}`},r),v.attr("transform",`translate(0,${u/2+10})`),n||(v.attr("data-et","participant"),v.attr("data-type","boundary"),v.attr("data-id",t.name)),t.height},"drawActorTypeBoundary"),fAt=s(function(e,t,r,n,i){let a=n?t.stopy:t.starty,o=t.x+t.width/2,l=a+80,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p,actorBorder:m}=d,g=e.append("g").lower();n||(cn++,g.append("line").attr("id","actor"+cn).attr("x1",o).attr("y1",l).attr("x2",o).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),t.actorCnt=cn);let y=e.append("g"),v=w0;n?v+=` ${Lp}`:v+=` ${_p}`,y.attr("class",v),y.attr("name",t.name),n||y.attr("data-et","participant").attr("data-type","actor").attr("data-id",t.name);let x=u==="neo"?.5:1,b=u==="neo"?a+(1-x)*30:a;y.append("line").attr("id","actor-man-torso"+cn).attr("x1",o).attr("y1",b+25*x).attr("x2",o).attr("y2",b+45*x),y.append("line").attr("id","actor-man-arms"+cn).attr("x1",o-Rp/2*x).attr("y1",b+33*x).attr("x2",o+Rp/2*x).attr("y2",b+33*x),y.append("line").attr("x1",o-Rp/2*x).attr("y1",b+60*x).attr("x2",o).attr("y2",b+45*x),y.append("line").attr("x1",o).attr("y1",b+45*x).attr("x2",o+(Rp/2-2)*x).attr("y2",b+60*x);let T=y.append("circle");T.attr("cx",t.x+t.width/2),T.attr("cy",b+10*x),T.attr("r",15*x),T.attr("width",t.width*x),T.attr("height",t.height*x);let w=y.node().getBBox();t.height=w.height;let C=Os();C.x=t.x,C.y=b,C.fill="#eaeaea",C.width=t.width,C.height=t.height/x,C.class="actor",C.rx=3,C.ry=3;let S=i.get(t.name)??0;return dd.has(h)?(y.style("stroke",p[S%p.length]),y.style("fill",f[S%p.length])):y.style("stroke",m),fd(r,si(t.description))(t.description,y,C.x,b+35*x-(u==="neo"?10:0),C.width,C.height,{class:`actor ${w0}`},r),t.height},"drawActorTypeActor"),pAt=s(async function(e,t,r,n,i,a,o){let l=o??new Map([...a.db.getActors().values()].map((u,h)=>[u.name,h]));switch(t.type){case"actor":return await fAt(e,t,r,n,l);case"participant":return await sAt(e,t,r,n,l);case"boundary":return await dAt(e,t,r,n,l);case"control":return await cAt(e,t,r,n,i,l);case"entity":return await uAt(e,t,r,n,l);case"database":return await hAt(e,t,r,n,l);case"collections":return await oAt(e,t,r,n,l);case"queue":return await lAt(e,t,r,n,l)}},"drawActor"),mAt=s(function(e,t,r){let i=e.append("g");J8e(i,t),t.name&&fd(r)(t.name,i,t.x,t.y+r.boxTextMargin+(t.textMaxHeight||0)/2,t.width,0,{class:"text"},r),i.lower()},"drawBox"),gAt=s(function(e){return e.append("g")},"anchorElement"),yAt=s(function(e,t,r,n,i,a,o){let{theme:l,themeVariables:u}=n,{bkgColorArray:h,borderColorArray:d,mainBkg:f}=u,p=Os(),m=t.anchored,g=t.actor;p.x=t.startx,p.y=t.starty,p.class="activation"+i%3,p.width=t.stopx-t.startx,p.height=r-t.starty;let y=GS(m,p),x=(o??new Map([...a.db.getActors().values()].map((b,T)=>[b.name,T]))).get(g)??0;dd.has(l)&&(y.style("stroke",d[x%d.length]),y.style("fill",h[x%d.length]??f))},"drawActivation"),vAt=s(async function(e,t,r,n,i){let{boxMargin:a,boxTextMargin:o,labelBoxHeight:l,labelBoxWidth:u,messageFontFamily:h,messageFontSize:d,messageFontWeight:f}=n,p=e.append("g").attr("data-et","control-structure").attr("data-id","i"+i.id),m=s(function(v,x,b,T){return p.append("line").attr("x1",v).attr("y1",x).attr("x2",b).attr("y2",T).attr("class","loopLine")},"drawLoopLine");m(t.startx,t.starty,t.stopx,t.starty),m(t.stopx,t.starty,t.stopx,t.stopy),m(t.startx,t.stopy,t.stopx,t.stopy),m(t.startx,t.starty,t.startx,t.stopy),t.sections!==void 0&&t.sections.forEach(function(v){m(t.startx,v.y,t.stopx,v.y).style("stroke-dasharray","3, 3")});let g=i2();g.text=r,g.x=t.startx,g.y=t.starty,g.fontFamily=h,g.fontSize=d,g.fontWeight=f,g.anchor="middle",g.valign="middle",g.tspan=!1,g.width=Math.max(u??0,50),g.height=l+(n.look==="neo"?15:0)||20,g.textMargin=o,g.class="labelText",Q8e(p,g),g=eMe(),g.text=t.title,g.x=t.startx+u/2+(t.stopx-t.startx)/2,g.y=t.starty+a+o,g.anchor="middle",g.valign="middle",g.textMargin=o,g.class="loopText",g.fontFamily=h,g.fontSize=d,g.fontWeight=f,g.wrap=!0;let y=si(g.text)?await zS(p,g,t):k0(p,g);if(t.sectionTitles!==void 0){for(let[v,x]of Object.entries(t.sectionTitles))if(x.message){g.text=x.message,g.x=t.startx+(t.stopx-t.startx)/2,g.y=t.sections[v].y+a+o,g.class="sectionTitle",g.anchor="middle",g.valign="middle",g.tspan=!1,g.fontFamily=h,g.fontSize=d,g.fontWeight=f,g.wrap=t.wrap,si(g.text)?(t.starty=t.sections[v].y,await zS(p,g,t)):k0(p,g);let b=Math.round(y.map(T=>(T._groups||T)[0][0].getBBox().height).reduce((T,w)=>T+w));t.sections[v].height+=b-(a+o)}}return t.height=Math.round(t.stopy-t.starty),p},"drawLoop"),J8e=s(function(e,t){ME(e,t)},"drawBackgroundRect"),xAt=s(function(e,t){e.append("defs").append("symbol").attr("id",t+"-database").attr("fill-rule","evenodd").attr("clip-rule","evenodd").append("path").attr("transform","scale(.5)").attr("d","M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z")},"insertDatabaseIcon"),bAt=s(function(e,t){e.append("defs").append("symbol").attr("id",t+"-computer").attr("width","24").attr("height","24").append("path").attr("transform","scale(.5)").attr("d","M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z")},"insertComputerIcon"),TAt=s(function(e,t){e.append("defs").append("symbol").attr("id",t+"-clock").attr("width","24").attr("height","24").append("path").attr("transform","scale(.5)").attr("d","M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z")},"insertClockIcon"),CAt=s(function(e,t){e.append("defs").append("marker").attr("id",t+"-arrowhead").attr("refX",7.9).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto-start-reverse").append("path").attr("d","M -1 0 L 10 5 L 0 10 z")},"insertArrowHead"),SAt=s(function(e,t){e.append("defs").append("marker").attr("id",t+"-filled-head").attr("refX",15.5).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L14,7 L9,1 Z")},"insertArrowFilledHead"),wAt=s(function(e,t){e.append("defs").append("marker").attr("id",t+"-sequencenumber").attr("refX",15).attr("refY",15).attr("markerWidth",60).attr("markerHeight",40).attr("orient","auto").append("circle").attr("cx",15).attr("cy",15).attr("r",6)},"insertSequenceNumber"),kAt=s(function(e,t){e.append("defs").append("marker").attr("id",t+"-crosshead").attr("markerWidth",15).attr("markerHeight",8).attr("orient","auto").attr("refX",4).attr("refY",4.5).append("path").attr("fill","none").attr("stroke","#000000").style("stroke-dasharray","0, 0").attr("stroke-width","1pt").attr("d","M 1,2 L 6,7 M 6,2 L 1,7")},"insertArrowCrossHead"),EAt=s(function(e,t){let{theme:r}=t;e.append("defs").append("filter").attr("id","drop-shadow").attr("height","130%").attr("width","130%").append("feDropShadow").attr("dx","4").attr("dy","4").attr("stdDeviation",0).attr("flood-opacity","0.06").attr("flood-color",`${r==="redux"||r==="redux-color"?"#000000":"#FFFFFF"}`)},"insertDropShadow"),eMe=s(function(){return{x:0,y:0,fill:void 0,anchor:void 0,style:"#666",width:void 0,height:void 0,textMargin:0,rx:0,ry:0,tspan:!0,valign:void 0}},"getTextObj"),AAt=s(function(){return{x:0,y:0,fill:"#EDF2AE",stroke:"#666",width:100,anchor:"start",height:100,rx:0,ry:0}},"getNoteRect"),fd=(function(){function e(a,o,l,u,h,d,f){let p=o.append("text").attr("x",l+h/2).attr("y",u+d/2+5).style("text-anchor","middle").text(a);i(p,f)}s(e,"byText");function t(a,o,l,u,h,d,f,p){let{actorFontSize:m,actorFontFamily:g,actorFontWeight:y}=p,[v,x]=Bs(m),b=a.split(Tt.lineBreakRegex);for(let T=0;T{let o=E0(Xe),l=a.actorKeys.reduce((f,p)=>f+=e.get(p).width+(e.get(p).margin||0),0),u=Xe.boxMargin*8;l+=u,l-=2*Xe.boxTextMargin,a.wrap&&(a.name=or.wrapLabel(a.name,l-2*Xe.wrapPadding,o));let h=or.calculateTextDimensions(a.name,o);i=Tt.getMax(h.height,i);let d=Tt.getMax(l,h.width+2*Xe.wrapPadding);if(a.margin=Xe.boxTextMargin,la.textMaxHeight=i),Tt.getMax(n,Xe.height)}var Xe,_t,MAt,rMe,E0,Qv,mY,PAt,OAt,gY,iMe,aMe,oL,nMe,$At,GAt,VAt,WAt,qAt,pY,HAt,sMe,UAt,YAt,jAt,oMe,lMe=F(()=>{"use strict";Wr();tMe();St();Yr();Yr();Xd();Jt();$0();er();Vn();hY();Xe={},_t={data:{startx:void 0,stopx:void 0,starty:void 0,stopy:void 0},verticalPos:0,sequenceItems:[],activations:[],models:{getHeight:s(function(){return Math.max.apply(null,this.actors.length===0?[0]:this.actors.map(e=>e.height||0))+(this.loops.length===0?0:this.loops.map(e=>e.height||0).reduce((e,t)=>e+t))+(this.messages.length===0?0:this.messages.map(e=>e.height||0).reduce((e,t)=>e+t))+(this.notes.length===0?0:this.notes.map(e=>e.height||0).reduce((e,t)=>e+t))},"getHeight"),clear:s(function(){this.actors=[],this.boxes=[],this.loops=[],this.messages=[],this.notes=[]},"clear"),addBox:s(function(e){this.boxes.push(e)},"addBox"),addActor:s(function(e){this.actors.push(e)},"addActor"),addLoop:s(function(e){this.loops.push(e)},"addLoop"),addMessage:s(function(e){this.messages.push(e)},"addMessage"),addNote:s(function(e){this.notes.push(e)},"addNote"),lastActor:s(function(){return this.actors[this.actors.length-1]},"lastActor"),lastLoop:s(function(){return this.loops[this.loops.length-1]},"lastLoop"),lastMessage:s(function(){return this.messages[this.messages.length-1]},"lastMessage"),lastNote:s(function(){return this.notes[this.notes.length-1]},"lastNote"),actors:[],boxes:[],loops:[],messages:[],notes:[]},init:s(function(){this.sequenceItems=[],this.activations=[],this.models.clear(),this.data={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0},this.verticalPos=0,aMe(Pe())},"init"),updateVal:s(function(e,t,r,n){e[t]===void 0?e[t]=r:e[t]=n(r,e[t])},"updateVal"),updateBounds:s(function(e,t,r,n){let i=this,a=0;function o(l){return s(function(h){a++;let d=i.sequenceItems.length-a+1;i.updateVal(h,"starty",t-d*Xe.boxMargin,Math.min),i.updateVal(h,"stopy",n+d*Xe.boxMargin,Math.max),i.updateVal(_t.data,"startx",e-d*Xe.boxMargin,Math.min),i.updateVal(_t.data,"stopx",r+d*Xe.boxMargin,Math.max),l!=="activation"&&(i.updateVal(h,"startx",e-d*Xe.boxMargin,Math.min),i.updateVal(h,"stopx",r+d*Xe.boxMargin,Math.max),i.updateVal(_t.data,"starty",t-d*Xe.boxMargin,Math.min),i.updateVal(_t.data,"stopy",n+d*Xe.boxMargin,Math.max))},"updateItemBounds")}s(o,"updateFn"),this.sequenceItems.forEach(o()),this.activations.forEach(o("activation"))},"updateBounds"),insert:s(function(e,t,r,n){let i=Tt.getMin(e,r),a=Tt.getMax(e,r),o=Tt.getMin(t,n),l=Tt.getMax(t,n);this.updateVal(_t.data,"startx",i,Math.min),this.updateVal(_t.data,"starty",o,Math.min),this.updateVal(_t.data,"stopx",a,Math.max),this.updateVal(_t.data,"stopy",l,Math.max),this.updateBounds(i,o,a,l)},"insert"),newActivation:s(function(e,t,r){let n=r.get(e.from),i=oL(e.from).length||0,a=n.x+n.width/2+(i-1)*Xe.activationWidth/2;this.activations.push({startx:a,starty:this.verticalPos+2,stopx:a+Xe.activationWidth,stopy:void 0,actor:e.from,anchored:Kn.anchorElement(t)})},"newActivation"),endActivation:s(function(e){let t=this.activations.map(function(r){return r.actor}).lastIndexOf(e.from);return this.activations.splice(t,1)[0]},"endActivation"),createLoop:s(function(e={message:void 0,wrap:!1,width:void 0},t){return{startx:void 0,starty:this.verticalPos,stopx:void 0,stopy:void 0,title:e.message,wrap:e.wrap,width:e.width,height:0,fill:t}},"createLoop"),newLoop:s(function(e={message:void 0,wrap:!1,width:void 0},t){this.sequenceItems.push(this.createLoop(e,t))},"newLoop"),endLoop:s(function(){return this.sequenceItems.pop()},"endLoop"),isLoopOverlap:s(function(){return this.sequenceItems.length?this.sequenceItems[this.sequenceItems.length-1].overlap:!1},"isLoopOverlap"),addSectionToLoop:s(function(e){let t=this.sequenceItems.pop();t.sections=t.sections||[],t.sectionTitles=t.sectionTitles||[],t.sections.push({y:_t.getVerticalPos(),height:0}),t.sectionTitles.push(e),this.sequenceItems.push(t)},"addSectionToLoop"),saveVerticalPos:s(function(){this.isLoopOverlap()&&(this.savedVerticalPos=this.verticalPos)},"saveVerticalPos"),resetVerticalPos:s(function(){this.isLoopOverlap()&&(this.verticalPos=this.savedVerticalPos)},"resetVerticalPos"),bumpVerticalPos:s(function(e){this.verticalPos=this.verticalPos+e,this.data.stopy=Tt.getMax(this.data.stopy,this.verticalPos)},"bumpVerticalPos"),getVerticalPos:s(function(){return this.verticalPos},"getVerticalPos"),getBounds:s(function(){return{bounds:this.data,models:this.models}},"getBounds")},MAt=s(async function(e,t,r){_t.bumpVerticalPos(Xe.boxMargin),t.height=Xe.boxMargin,t.starty=_t.getVerticalPos();let n=Os();n.x=t.startx,n.y=t.starty,n.width=t.width||Xe.width,n.class="note";let i=e.append("g");i.attr("data-et","note"),i.attr("data-id","i"+r);let a=Kn.drawRect(i,n),o=i2();o.x=t.startx,o.y=t.starty,o.width=n.width,o.dy="1em",o.text=t.message,o.class="noteText",o.fontFamily=Xe.noteFontFamily,o.fontSize=Xe.noteFontSize,o.fontWeight=Xe.noteFontWeight,o.anchor=Xe.noteAlign,o.textMargin=Xe.noteMargin,o.valign="center";let l=si(o.text)?await zS(i,o):k0(i,o),u=Math.round(l.map(h=>(h._groups||h)[0][0].getBBox().height).reduce((h,d)=>h+d));a.attr("height",u+2*Xe.noteMargin),t.height+=u+2*Xe.noteMargin,_t.bumpVerticalPos(u+2*Xe.noteMargin),t.stopy=t.starty+u+2*Xe.noteMargin,t.stopx=t.startx+n.width,_t.insert(t.startx,t.starty,t.stopx,t.stopy),_t.models.addNote(t)},"drawNote"),rMe=s(function(e,t,r,n,i,a,o){let l=n.db.getActors(),u=l.get(t.from),h=l.get(t.to),d=r.sequenceVisible,f=u.x+u.width/2,p=h.x+h.width/2,m=f<=p,g=sMe(t,n),y=e.append("g"),v=16.5,x=s((S,k)=>{let R=S?v:-v;return k?-R:R},"getCircleOffset"),b=s(S=>{y.append("circle").attr("cx",S).attr("cy",o).attr("r",5).attr("width",10).attr("height",10)},"drawCircle"),{CENTRAL_CONNECTION:T,CENTRAL_CONNECTION_REVERSE:w,CENTRAL_CONNECTION_DUAL:C}=n.db.LINETYPE;if(d)switch(t.centralConnection){case T:g&&(p+=x(m,!0));break;case w:g||(f+=x(m,!1));break;case C:g?p+=x(m,!0):f+=x(m,!1);break}switch(t.centralConnection){case T:b(p);break;case w:b(f);break;case C:b(f),b(p);break}},"drawCentralConnection"),E0=s(e=>({fontFamily:e.messageFontFamily,fontSize:e.messageFontSize,fontWeight:e.messageFontWeight}),"messageFont"),Qv=s(e=>({fontFamily:e.noteFontFamily,fontSize:e.noteFontSize,fontWeight:e.noteFontWeight}),"noteFont"),mY=s(e=>({fontFamily:e.actorFontFamily,fontSize:e.actorFontSize,fontWeight:e.actorFontWeight}),"actorFont");s(NAt,"boundMessage");PAt=s(async function(e,t,r,n,i,a){let{startx:o,stopx:l,starty:u,message:h,type:d,sequenceIndex:f,sequenceVisible:p}=t,m=or.calculateTextDimensions(h,E0(Xe)),g=i2();g.x=Math.min(o,l),g.y=u+10,g.width=Math.abs(l-o),g.class="messageText",g.dy="1em",g.text=h,g.fontFamily=Xe.messageFontFamily,g.fontSize=Xe.messageFontSize,g.fontWeight=Xe.messageFontWeight,g.anchor=Xe.messageAlign,g.valign="center",g.textMargin=Xe.wrapPadding,g.tspan=!1,si(g.text)?await zS(e,g,{startx:o,stopx:l,starty:r}):k0(e,g);let y=m.width,v;if(o===l){let b=p||Xe.showSequenceNumbers,T=sMe(i,n),w=UAt(i,n),C=o+(b&&(T||w)?10:0);Xe.rightAngles?v=e.append("path").attr("d",`M ${C},${r} H ${o+Tt.getMax(Xe.width/2,y/2)} V ${r+25} H ${o}`):v=e.append("path").attr("d","M "+C+","+r+" C "+(C+60)+","+(r-10)+" "+(o+60)+","+(r+30)+" "+o+","+(r+20)),pY(i,n)&&rMe(e,i,t,n,o,l,r)}else v=e.append("line"),v.attr("x1",o),v.attr("y1",r),v.attr("x2",l),v.attr("y2",r),pY(i,n)&&rMe(e,i,t,n,o,l,r);d===n.db.LINETYPE.DOTTED||d===n.db.LINETYPE.DOTTED_CROSS||d===n.db.LINETYPE.DOTTED_POINT||d===n.db.LINETYPE.DOTTED_OPEN||d===n.db.LINETYPE.BIDIRECTIONAL_DOTTED||d===n.db.LINETYPE.SOLID_TOP_DOTTED||d===n.db.LINETYPE.SOLID_BOTTOM_DOTTED||d===n.db.LINETYPE.STICK_TOP_DOTTED||d===n.db.LINETYPE.STICK_BOTTOM_DOTTED||d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED||d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED||d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED||d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED?(v.style("stroke-dasharray","3, 3"),v.attr("class","messageLine1")):v.attr("class","messageLine0"),v.attr("data-et","message"),v.attr("data-id","i"+t.id),v.attr("data-from",t.from),v.attr("data-to",t.to);let x="";if(Xe.arrowMarkerAbsolute&&(x=eb(!0)),v.attr("stroke-width",2),v.attr("stroke","none"),v.style("fill","none"),(d===n.db.LINETYPE.SOLID_TOP||d===n.db.LINETYPE.SOLID_TOP_DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-solidTopArrowHead)"),(d===n.db.LINETYPE.SOLID_BOTTOM||d===n.db.LINETYPE.SOLID_BOTTOM_DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-solidBottomArrowHead)"),(d===n.db.LINETYPE.STICK_TOP||d===n.db.LINETYPE.STICK_TOP_DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-stickTopArrowHead)"),(d===n.db.LINETYPE.STICK_BOTTOM||d===n.db.LINETYPE.STICK_BOTTOM_DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-stickBottomArrowHead)"),(d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE||d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED)&&v.attr("marker-start","url("+x+"#"+a+"-solidBottomArrowHead)"),(d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE||d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED)&&v.attr("marker-start","url("+x+"#"+a+"-solidTopArrowHead)"),(d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE||d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED)&&v.attr("marker-start","url("+x+"#"+a+"-stickBottomArrowHead)"),(d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE||d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED)&&v.attr("marker-start","url("+x+"#"+a+"-stickTopArrowHead)"),(d===n.db.LINETYPE.SOLID||d===n.db.LINETYPE.DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-arrowhead)"),(d===n.db.LINETYPE.BIDIRECTIONAL_SOLID||d===n.db.LINETYPE.BIDIRECTIONAL_DOTTED)&&(v.attr("marker-start","url("+x+"#"+a+"-arrowhead)"),v.attr("marker-end","url("+x+"#"+a+"-arrowhead)")),(d===n.db.LINETYPE.SOLID_POINT||d===n.db.LINETYPE.DOTTED_POINT)&&v.attr("marker-end","url("+x+"#"+a+"-filled-head)"),(d===n.db.LINETYPE.SOLID_CROSS||d===n.db.LINETYPE.DOTTED_CROSS)&&v.attr("marker-end","url("+x+"#"+a+"-crosshead)"),p||Xe.showSequenceNumbers){let b=d===n.db.LINETYPE.BIDIRECTIONAL_SOLID||d===n.db.LINETYPE.BIDIRECTIONAL_DOTTED,T=d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE||d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED||d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE||d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED||d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE||d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED||d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE||d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,w=6,C=pY(i,n),S=o,k=l;b?(oo?k=l-2*w:(k=l-w,S+=i?.centralConnection===n.db.LINETYPE.CENTRAL_CONNECTION_DUAL||i?.centralConnection===n.db.LINETYPE.CENTRAL_CONNECTION_REVERSE?-7.5:0),k+=C?15:0,v.attr("x2",k),v.attr("x1",S)):v.attr("x1",o+w);let R=0,M=o===l,P=o<=l;M?R=t.fromBounds+1:T?R=P?t.toBounds-1:t.fromBounds+1:R=P?t.fromBounds+1:t.toBounds-1;let N="12px",_=f.toString().length;_>5?N="7px":_>3&&(N="9px"),e.append("line").attr("x1",R).attr("y1",r).attr("x2",R).attr("y2",r).attr("stroke-width",0).attr("marker-start","url("+x+"#"+a+"-sequencenumber)"),e.append("text").attr("x",R).attr("y",r+4).attr("font-family","sans-serif").attr("font-size",N).attr("text-anchor","middle").attr("class","sequenceNumber").text(f)}},"drawMessage"),OAt=s(function(e,t,r,n,i,a,o){let l=0,u=0,h,d=0;for(let f of n){let p=t.get(f),m=p.box;h&&h!=m&&(o||_t.models.addBox(h),u+=Xe.boxMargin+h.margin),m&&m!=h&&(o||(m.x=l+u,m.y=i),u+=m.margin),p.width=Tt.getMax(p.width||Xe.width,Xe.width),p.height=Tt.getMax(p.height||Xe.height,Xe.height),p.margin=p.margin||Xe.actorMargin,d=Tt.getMax(d,p.height),r.get(p.name)&&(u+=p.width/2),p.x=l+u,p.starty=_t.getVerticalPos(),_t.insert(p.x,i,p.x+p.width,p.height),l+=p.width+u,p.box&&(p.box.width=l+m.margin-p.box.x),u=p.margin,h=p.box,_t.models.addActor(p)}h&&!o&&_t.models.addBox(h),_t.bumpVerticalPos(d)},"addActorRenderingData"),gY=s(async function(e,t,r,n,i,a,o){if(n){let l=0;_t.bumpVerticalPos(Xe.boxMargin*2);for(let u of r){let h=t.get(u);h.stopy||(h.stopy=_t.getVerticalPos());let d=await Kn.drawActor(e,h,Xe,!0,i,a,o);l=Tt.getMax(l,d)}_t.bumpVerticalPos(l+Xe.boxMargin)}else for(let l of r){let u=t.get(l);await Kn.drawActor(e,u,Xe,!1,i,a,o)}},"drawActors"),iMe=s(function(e,t,r,n){let i=0,a=0;for(let o of r){let l=t.get(o),u=GAt(l),h=Kn.drawPopup(e,l,u,Xe,Xe.forceMenus,n);h.height>i&&(i=h.height),h.width+l.x>a&&(a=h.width+l.x)}return{maxHeight:i,maxWidth:a}},"drawActorsPopup"),aMe=s(function(e){ii(Xe,e),e.fontFamily&&(Xe.actorFontFamily=Xe.noteFontFamily=Xe.messageFontFamily=e.fontFamily),e.fontSize&&(Xe.actorFontSize=Xe.noteFontSize=Xe.messageFontSize=e.fontSize),e.fontWeight&&(Xe.actorFontWeight=Xe.noteFontWeight=Xe.messageFontWeight=e.fontWeight)},"setConf"),oL=s(function(e){return _t.activations.filter(function(t){return t.actor===e})},"actorActivations"),nMe=s(function(e,t){let r=t.get(e),n=oL(e),i=n.reduce(function(o,l){return Tt.getMin(o,l.startx)},r.x+r.width/2-1),a=n.reduce(function(o,l){return Tt.getMax(o,l.stopx)},r.x+r.width/2+1);return[i,a]},"activationBounds");s(Yu,"adjustLoopHeightForWrap");s(BAt,"adjustCreatedDestroyedData");$At=s(async function(e,t,r,n){let{securityLevel:i,sequence:a,look:o,themeVariables:l}=Pe();Xe=a;let u;i==="sandbox"&&(u=ut("#i"+t));let h=i==="sandbox"?ut(u.nodes()[0].contentDocument.body):ut("body"),d=i==="sandbox"?u.nodes()[0].contentDocument:document;_t.init(),se.debug(n.db);let f=i==="sandbox"?h.select(`[id="${t}"]`):ut(`[id="${t}"]`),p=n.db.getActors(),m=n.db.getCreatedActors(),g=n.db.getDestroyedActors(),y=n.db.getBoxes(),v=n.db.getActorKeys(),x=n.db.getMessages(),b=n.db.getDiagramTitle(),T=n.db.hasAtLeastOneBox(),w=n.db.hasAtLeastOneBoxWithTitle(),C=await FAt(p,x,n);if(Xe.height=await zAt(p,C,y),Kn.insertComputerIcon(f,t),Kn.insertDatabaseIcon(f,t),Kn.insertClockIcon(f,t),T&&(_t.bumpVerticalPos(Xe.boxMargin),w&&_t.bumpVerticalPos(y[0].textMaxHeight)),Xe.hideUnusedParticipants===!0){let z=new Set;x.forEach(H=>{z.add(H.from),z.add(H.to)}),v=v.filter(H=>z.has(H))}let S=new Map(v.map((z,H)=>[p.get(z)?.name??z,H]));OAt(f,p,m,v,0,x,!1);let k=await jAt(x,p,C,n);Kn.insertArrowHead(f,t),Kn.insertArrowCrossHead(f,t),Kn.insertArrowFilledHead(f,t),Kn.insertSequenceNumber(f,t),Kn.insertSolidTopArrowHead(f,t),Kn.insertSolidBottomArrowHead(f,t),Kn.insertStickTopArrowHead(f,t),Kn.insertStickBottomArrowHead(f,t),o==="neo"&&Kn.insertDropShadow(f,Xe);function R(z,H){let K=_t.endActivation(z);K.starty+18>H&&(K.starty=H-6,H+=12),Kn.drawActivation(f,K,H,Xe,oL(z.from).length,n,S),_t.insert(K.startx,H-10,K.stopx,H)}s(R,"activeEnd");let M=1,P=1,N=[],_=[],A=0;for(let z of x){let H,K,J;switch(z.type){case n.db.LINETYPE.NOTE:_t.resetVerticalPos(),K=z.noteModel,await MAt(f,K,z.id);break;case n.db.LINETYPE.ACTIVE_START:_t.newActivation(z,f,p);break;case n.db.LINETYPE.CENTRAL_CONNECTION:_t.newActivation(z,f,p);break;case n.db.LINETYPE.CENTRAL_CONNECTION_REVERSE:_t.newActivation(z,f,p);break;case n.db.LINETYPE.ACTIVE_END:R(z,_t.getVerticalPos());break;case n.db.LINETYPE.LOOP_START:Yu(k,z,Xe.boxMargin,Xe.boxMargin+Xe.boxTextMargin,ne=>_t.newLoop(ne));break;case n.db.LINETYPE.LOOP_END:H=_t.endLoop(),await Kn.drawLoop(f,H,"loop",Xe,z),_t.bumpVerticalPos(H.stopy-_t.getVerticalPos()),_t.models.addLoop(H);break;case n.db.LINETYPE.RECT_START:Yu(k,z,Xe.boxMargin,Xe.boxMargin,ne=>{let Z=ne.message;Z||(Z=l?.rectBkgColor||l?.actorBkg||"rgba(128, 128, 128, 0.5)"),_t.newLoop(void 0,Z)});break;case n.db.LINETYPE.RECT_END:H=_t.endLoop(),_.push(H),_t.models.addLoop(H),_t.bumpVerticalPos(H.stopy-_t.getVerticalPos());break;case n.db.LINETYPE.OPT_START:Yu(k,z,Xe.boxMargin,Xe.boxMargin+Xe.boxTextMargin,ne=>_t.newLoop(ne));break;case n.db.LINETYPE.OPT_END:H=_t.endLoop(),await Kn.drawLoop(f,H,"opt",Xe,z),_t.bumpVerticalPos(H.stopy-_t.getVerticalPos()),_t.models.addLoop(H);break;case n.db.LINETYPE.ALT_START:Yu(k,z,Xe.boxMargin,Xe.boxMargin+Xe.boxTextMargin,ne=>_t.newLoop(ne));break;case n.db.LINETYPE.ALT_ELSE:Yu(k,z,Xe.boxMargin+Xe.boxTextMargin,Xe.boxMargin,ne=>_t.addSectionToLoop(ne));break;case n.db.LINETYPE.ALT_END:H=_t.endLoop(),await Kn.drawLoop(f,H,"alt",Xe,z),_t.bumpVerticalPos(H.stopy-_t.getVerticalPos()),_t.models.addLoop(H);break;case n.db.LINETYPE.PAR_START:case n.db.LINETYPE.PAR_OVER_START:Yu(k,z,Xe.boxMargin,Xe.boxMargin+Xe.boxTextMargin,ne=>_t.newLoop(ne)),_t.saveVerticalPos();break;case n.db.LINETYPE.PAR_AND:Yu(k,z,Xe.boxMargin+Xe.boxTextMargin,Xe.boxMargin,ne=>_t.addSectionToLoop(ne));break;case n.db.LINETYPE.PAR_END:H=_t.endLoop(),await Kn.drawLoop(f,H,"par",Xe,z),_t.bumpVerticalPos(H.stopy-_t.getVerticalPos()),_t.models.addLoop(H);break;case n.db.LINETYPE.AUTONUMBER:M=z.message.start||M,P=z.message.step||P,z.message.visible?n.db.enableSequenceNumbers():n.db.disableSequenceNumbers();break;case n.db.LINETYPE.CRITICAL_START:Yu(k,z,Xe.boxMargin,Xe.boxMargin+Xe.boxTextMargin,ne=>_t.newLoop(ne));break;case n.db.LINETYPE.CRITICAL_OPTION:Yu(k,z,Xe.boxMargin+Xe.boxTextMargin,Xe.boxMargin,ne=>_t.addSectionToLoop(ne));break;case n.db.LINETYPE.CRITICAL_END:H=_t.endLoop(),await Kn.drawLoop(f,H,"critical",Xe,z),_t.bumpVerticalPos(H.stopy-_t.getVerticalPos()),_t.models.addLoop(H);break;case n.db.LINETYPE.BREAK_START:Yu(k,z,Xe.boxMargin,Xe.boxMargin+Xe.boxTextMargin,ne=>_t.newLoop(ne));break;case n.db.LINETYPE.BREAK_END:H=_t.endLoop(),await Kn.drawLoop(f,H,"break",Xe,z),_t.bumpVerticalPos(H.stopy-_t.getVerticalPos()),_t.models.addLoop(H);break;default:try{J=z.msgModel,J.starty=_t.getVerticalPos(),J.sequenceIndex=M,J.sequenceVisible=n.db.showSequenceNumbers(),J.id=z.id,J.from=z.from,J.to=z.to;let ne=await NAt(f,J);BAt(z,J,ne,A,p,m,g),N.push({messageModel:J,lineStartY:ne,msg:z}),_t.models.addMessage(J)}catch(ne){se.error("error while drawing message",ne)}}[n.db.LINETYPE.SOLID_OPEN,n.db.LINETYPE.DOTTED_OPEN,n.db.LINETYPE.SOLID,n.db.LINETYPE.SOLID_TOP,n.db.LINETYPE.SOLID_BOTTOM,n.db.LINETYPE.STICK_TOP,n.db.LINETYPE.STICK_BOTTOM,n.db.LINETYPE.SOLID_TOP_DOTTED,n.db.LINETYPE.SOLID_BOTTOM_DOTTED,n.db.LINETYPE.STICK_TOP_DOTTED,n.db.LINETYPE.STICK_BOTTOM_DOTTED,n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,n.db.LINETYPE.STICK_ARROW_TOP_REVERSE,n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,n.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,n.db.LINETYPE.DOTTED,n.db.LINETYPE.SOLID_CROSS,n.db.LINETYPE.DOTTED_CROSS,n.db.LINETYPE.SOLID_POINT,n.db.LINETYPE.DOTTED_POINT,n.db.LINETYPE.BIDIRECTIONAL_SOLID,n.db.LINETYPE.BIDIRECTIONAL_DOTTED].includes(z.type)&&(M=Math.round((M+P)*100)/100),A++}se.debug("createdActors",m),se.debug("destroyedActors",g),await gY(f,p,v,!1,t,n,S);for(let z of N)await PAt(f,z.messageModel,z.lineStartY,n,z.msg,t);Xe.mirrorActors&&await gY(f,p,v,!0,t,n,S),_.forEach(z=>Kn.drawBackgroundRect(f,z)),fY(f,p,v,Xe);for(let z of _t.models.boxes){z.height=_t.getVerticalPos()-z.y,_t.insert(z.x,z.y,z.x+z.width,z.height);let H=Xe.boxMargin*2;z.startx=z.x-H,z.starty=z.y-H*.25,z.stopx=z.startx+z.width+2*H,z.stopy=z.starty+z.height+H*.75,z.stroke="rgb(0,0,0, 0.5)",Kn.drawBox(f,z,Xe)}T&&_t.bumpVerticalPos(Xe.boxMargin);let I=iMe(f,p,v,d),{bounds:D}=_t.getBounds();D.startx===void 0&&(D.startx=0),D.starty===void 0&&(D.starty=0),D.stopx===void 0&&(D.stopx=0),D.stopy===void 0&&(D.stopy=0);let L=D.stopy-D.starty;L2,p=s(v=>u?-v:v,"adjustValue");e.from===e.to?d=h:(e.activate&&!f&&(d+=p(Xe.activationWidth/2-1)),[r.db.LINETYPE.SOLID_OPEN,r.db.LINETYPE.DOTTED_OPEN,r.db.LINETYPE.STICK_TOP,r.db.LINETYPE.STICK_BOTTOM,r.db.LINETYPE.STICK_TOP_DOTTED,r.db.LINETYPE.STICK_BOTTOM_DOTTED,r.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,r.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,r.db.LINETYPE.STICK_ARROW_TOP_REVERSE,r.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,r.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,r.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,r.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,r.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE].includes(e.type)||(d+=p(3)),[r.db.LINETYPE.BIDIRECTIONAL_SOLID,r.db.LINETYPE.BIDIRECTIONAL_DOTTED,r.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,r.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,r.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,r.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE].includes(e.type)&&(h-=p(3)));let m=[i,a,o,l],g=Math.abs(h-d);e.wrap&&e.message&&(e.message=or.wrapLabel(e.message,Tt.getMax(g+2*Xe.wrapPadding,Xe.width),E0(Xe)));let y=or.calculateTextDimensions(e.message,E0(Xe));return{width:Tt.getMax(e.wrap?0:y.width+2*Xe.wrapPadding,g+2*Xe.wrapPadding,Xe.width),height:0,startx:h,stopx:d,starty:0,stopy:0,message:e.message,type:e.type,wrap:e.wrap,fromBounds:Math.min.apply(null,m),toBounds:Math.max.apply(null,m)}},"buildMessageModel"),jAt=s(async function(e,t,r,n){let i={},a=[],o,l,u;for(let h of e){switch(h.type){case n.db.LINETYPE.LOOP_START:case n.db.LINETYPE.ALT_START:case n.db.LINETYPE.OPT_START:case n.db.LINETYPE.PAR_START:case n.db.LINETYPE.PAR_OVER_START:case n.db.LINETYPE.CRITICAL_START:case n.db.LINETYPE.BREAK_START:a.push({id:h.id,msg:h.message,from:Number.MAX_SAFE_INTEGER,to:Number.MIN_SAFE_INTEGER,width:0});break;case n.db.LINETYPE.ALT_ELSE:case n.db.LINETYPE.PAR_AND:case n.db.LINETYPE.CRITICAL_OPTION:h.message&&(o=a.pop(),i[o.id]=o,i[h.id]=o,a.push(o));break;case n.db.LINETYPE.LOOP_END:case n.db.LINETYPE.ALT_END:case n.db.LINETYPE.OPT_END:case n.db.LINETYPE.PAR_END:case n.db.LINETYPE.CRITICAL_END:case n.db.LINETYPE.BREAK_END:o=a.pop(),i[o.id]=o;break;case n.db.LINETYPE.ACTIVE_START:{let f=t.get(h.from?h.from:h.to.actor),p=oL(h.from?h.from:h.to.actor).length,m=f.x+f.width/2+(p-1)*Xe.activationWidth/2,g={startx:m,stopx:m+Xe.activationWidth,actor:h.from,enabled:!0};_t.activations.push(g)}break;case n.db.LINETYPE.ACTIVE_END:{let f=_t.activations.map(p=>p.actor).lastIndexOf(h.from);_t.activations.splice(f,1).splice(0,1)}break}h.placement!==void 0?(l=await VAt(h,t,n),h.noteModel=l,a.forEach(f=>{o=f,o.from=Tt.getMin(o.from,l.startx),o.to=Tt.getMax(o.to,l.startx+l.width),o.width=Tt.getMax(o.width,Math.abs(o.from-o.to))-Xe.labelBoxWidth})):(u=YAt(h,t,n),h.msgModel=u,u.startx&&u.stopx&&a.length>0&&a.forEach(f=>{if(o=f,u.startx===u.stopx){let p=t.get(h.from),m=t.get(h.to);o.from=Tt.getMin(p.x-u.width/2,p.x-p.width/2,o.from),o.to=Tt.getMax(m.x+u.width/2,m.x+p.width/2,o.to),o.width=Tt.getMax(o.width,Math.abs(o.to-o.from))-Xe.labelBoxWidth}else o.from=Tt.getMin(u.startx,o.from),o.to=Tt.getMax(u.stopx,o.to),o.width=Tt.getMax(o.width,u.width)-Xe.labelBoxWidth}))}return _t.activations=[],se.debug("Loop type widths:",i),i},"calculateLoopBounds"),oMe={bounds:_t,drawActors:gY,drawActorsPopup:iMe,setConf:aMe,draw:$At}});var cMe={};sr(cMe,{diagram:()=>XAt});var XAt,uMe=F(()=>{"use strict";X8e();hY();Z8e();Jt();lMe();XAt={parser:j8e,get db(){return new iL},renderer:oMe,styles:K8e,init:s(e=>{e.sequence||(e.sequence={}),e.wrap&&(e.sequence.wrap=e.wrap,ib({sequence:{wrap:e.wrap}}))},"init")}});var yY,lL,vY=F(()=>{"use strict";yY=(function(){var e=s(function(oe,G,X,ee){for(X=X||{},ee=oe.length;ee--;X[oe[ee]]=G);return X},"o"),t=[1,18],r=[1,19],n=[1,20],i=[1,41],a=[1,26],o=[1,42],l=[1,24],u=[1,25],h=[1,32],d=[1,33],f=[1,34],p=[1,45],m=[1,35],g=[1,36],y=[1,37],v=[1,38],x=[1,27],b=[1,28],T=[1,29],w=[1,30],C=[1,31],S=[1,44],k=[1,46],R=[1,43],M=[1,47],P=[1,9],N=[1,8,9],_=[1,58],A=[1,59],I=[1,60],D=[1,61],L=[1,62],B=[1,63],O=[1,64],$=[1,8,9,41],W=[1,77],V=[1,8,9,12,13,22,39,41,44,46,68,69,70,71,72,73,74,79,81],z=[1,8,9,12,13,18,20,22,39,41,44,46,47,60,68,69,70,71,72,73,74,79,81,86,100,102,103],H=[13,60,86,100,102,103],K=[13,60,73,74,86,100,102,103],J=[13,60,68,69,70,71,72,86,100,102,103],ne=[1,103],Z=[1,121],pe=[1,117],te=[1,113],fe=[1,119],ce=[1,114],de=[1,115],Ee=[1,116],we=[1,118],Fe=[1,120],Ye=[22,50,60,61,82,86,87,88,89,90],He=[1,128],ye=[12,39],Be=[1,8,9,39,41,44,46],Ie=[1,8,9,22],Ne=[1,153],re=[1,8,9,61],ue=[1,8,9,22,50,60,61,82,86,87,88,89,90],le={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mermaidDoc:4,statements:5,graphConfig:6,CLASS_DIAGRAM:7,NEWLINE:8,EOF:9,statement:10,classLabel:11,SQS:12,STR:13,SQE:14,namespaceName:15,alphaNumToken:16,classLiteralName:17,DOT:18,className:19,GENERICTYPE:20,relationStatement:21,LABEL:22,namespaceStatement:23,classStatement:24,memberStatement:25,annotationStatement:26,clickStatement:27,styleStatement:28,cssClassStatement:29,noteStatement:30,classDefStatement:31,direction:32,acc_title:33,acc_title_value:34,acc_descr:35,acc_descr_value:36,acc_descr_multiline_value:37,namespaceIdentifier:38,STRUCT_START:39,classStatements:40,STRUCT_STOP:41,NAMESPACE:42,classIdentifier:43,STYLE_SEPARATOR:44,members:45,ANNOTATION_START:46,ANNOTATION_END:47,CLASS:48,emptyBody:49,SPACE:50,MEMBER:51,SEPARATOR:52,relation:53,NOTE_FOR:54,noteText:55,NOTE:56,CLASSDEF:57,classList:58,stylesOpt:59,ALPHA:60,COMMA:61,direction_tb:62,direction_bt:63,direction_rl:64,direction_lr:65,relationType:66,lineType:67,AGGREGATION:68,EXTENSION:69,COMPOSITION:70,DEPENDENCY:71,LOLLIPOP:72,LINE:73,DOTTED_LINE:74,CALLBACK:75,LINK:76,LINK_TARGET:77,CLICK:78,CALLBACK_NAME:79,CALLBACK_ARGS:80,HREF:81,STYLE:82,CSSCLASS:83,style:84,styleComponent:85,NUM:86,COLON:87,UNIT:88,BRKT:89,PCT:90,commentToken:91,textToken:92,graphCodeTokens:93,textNoTagsToken:94,TAGSTART:95,TAGEND:96,"==":97,"--":98,DEFAULT:99,MINUS:100,keywords:101,UNICODE_TEXT:102,BQUOTE_STR:103,$accept:0,$end:1},terminals_:{2:"error",7:"CLASS_DIAGRAM",8:"NEWLINE",9:"EOF",12:"SQS",13:"STR",14:"SQE",18:"DOT",20:"GENERICTYPE",22:"LABEL",33:"acc_title",34:"acc_title_value",35:"acc_descr",36:"acc_descr_value",37:"acc_descr_multiline_value",39:"STRUCT_START",41:"STRUCT_STOP",42:"NAMESPACE",44:"STYLE_SEPARATOR",46:"ANNOTATION_START",47:"ANNOTATION_END",48:"CLASS",50:"SPACE",51:"MEMBER",52:"SEPARATOR",54:"NOTE_FOR",56:"NOTE",57:"CLASSDEF",60:"ALPHA",61:"COMMA",62:"direction_tb",63:"direction_bt",64:"direction_rl",65:"direction_lr",68:"AGGREGATION",69:"EXTENSION",70:"COMPOSITION",71:"DEPENDENCY",72:"LOLLIPOP",73:"LINE",74:"DOTTED_LINE",75:"CALLBACK",76:"LINK",77:"LINK_TARGET",78:"CLICK",79:"CALLBACK_NAME",80:"CALLBACK_ARGS",81:"HREF",82:"STYLE",83:"CSSCLASS",86:"NUM",87:"COLON",88:"UNIT",89:"BRKT",90:"PCT",93:"graphCodeTokens",95:"TAGSTART",96:"TAGEND",97:"==",98:"--",99:"DEFAULT",100:"MINUS",101:"keywords",102:"UNICODE_TEXT",103:"BQUOTE_STR"},productions_:[0,[3,1],[3,1],[4,1],[6,4],[5,1],[5,2],[5,3],[11,3],[15,1],[15,1],[15,3],[15,2],[19,1],[19,3],[19,1],[19,2],[19,2],[19,2],[10,1],[10,2],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,2],[10,2],[10,1],[23,4],[23,5],[38,2],[38,3],[40,1],[40,2],[40,3],[40,1],[40,2],[40,3],[40,1],[40,2],[40,3],[24,1],[24,3],[24,4],[24,3],[24,6],[24,4],[24,7],[24,6],[43,2],[43,3],[49,0],[49,2],[49,2],[26,4],[45,1],[45,2],[25,1],[25,2],[25,1],[25,1],[21,3],[21,4],[21,4],[21,5],[30,3],[30,2],[31,3],[58,1],[58,3],[32,1],[32,1],[32,1],[32,1],[53,3],[53,2],[53,2],[53,1],[66,1],[66,1],[66,1],[66,1],[66,1],[67,1],[67,1],[27,3],[27,4],[27,3],[27,4],[27,4],[27,5],[27,3],[27,4],[27,4],[27,5],[27,4],[27,5],[27,5],[27,6],[28,3],[29,3],[59,1],[59,3],[84,1],[84,2],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[91,1],[91,1],[92,1],[92,1],[92,1],[92,1],[92,1],[92,1],[92,1],[94,1],[94,1],[94,1],[94,1],[16,1],[16,1],[16,1],[16,1],[17,1],[55,1]],performAction:s(function(G,X,ee,q,ie,j,U){var Y=j.length-1;switch(ie){case 8:this.$=j[Y-1];break;case 9:case 10:case 13:case 15:this.$=j[Y];break;case 11:case 14:this.$=j[Y-2]+"."+j[Y];break;case 12:case 16:this.$=j[Y-1]+j[Y];break;case 17:case 18:this.$=j[Y-1]+"~"+j[Y]+"~";break;case 19:q.addRelation(j[Y]);break;case 20:j[Y-1].title=q.cleanupLabel(j[Y]),q.addRelation(j[Y-1]);break;case 31:this.$=j[Y].trim(),q.setAccTitle(this.$);break;case 32:case 33:this.$=j[Y].trim(),q.setAccDescription(this.$);break;case 34:q.addClassesToNamespace(j[Y-3],j[Y-1][0],j[Y-1][1]),q.popNamespace();break;case 35:q.addClassesToNamespace(j[Y-4],j[Y-1][0],j[Y-1][1]),q.popNamespace();break;case 36:this.$=q.addNamespace(j[Y]);break;case 37:this.$=q.addNamespace(j[Y-1],j[Y]);break;case 38:this.$=[[j[Y]],[]];break;case 39:this.$=[[j[Y-1]],[]];break;case 40:j[Y][0].unshift(j[Y-2]),this.$=j[Y];break;case 41:this.$=[[],[j[Y]]];break;case 42:this.$=[[],[j[Y-1]]];break;case 43:j[Y][1].unshift(j[Y-2]),this.$=j[Y];break;case 44:case 45:this.$=[[],[]];break;case 46:this.$=j[Y];break;case 48:q.setCssClass(j[Y-2],j[Y]);break;case 49:q.addMembers(j[Y-3],j[Y-1]);break;case 51:q.setCssClass(j[Y-5],j[Y-3]),q.addMembers(j[Y-5],j[Y-1]);break;case 52:q.addAnnotation(j[Y-3],j[Y-1]);break;case 53:q.addAnnotation(j[Y-6],j[Y-4]),q.addMembers(j[Y-6],j[Y-1]);break;case 54:q.addAnnotation(j[Y-5],j[Y-3]);break;case 55:this.$=j[Y],q.addClass(j[Y]);break;case 56:this.$=j[Y-1],q.addClass(j[Y-1]),q.setClassLabel(j[Y-1],j[Y]);break;case 60:q.addAnnotation(j[Y],j[Y-2]);break;case 61:case 74:this.$=[j[Y]];break;case 62:j[Y].push(j[Y-1]),this.$=j[Y];break;case 63:break;case 64:q.addMember(j[Y-1],q.cleanupLabel(j[Y]));break;case 65:break;case 66:break;case 67:this.$={id1:j[Y-2],id2:j[Y],relation:j[Y-1],relationTitle1:"none",relationTitle2:"none"};break;case 68:this.$={id1:j[Y-3],id2:j[Y],relation:j[Y-1],relationTitle1:j[Y-2],relationTitle2:"none"};break;case 69:this.$={id1:j[Y-3],id2:j[Y],relation:j[Y-2],relationTitle1:"none",relationTitle2:j[Y-1]};break;case 70:this.$={id1:j[Y-4],id2:j[Y],relation:j[Y-2],relationTitle1:j[Y-3],relationTitle2:j[Y-1]};break;case 71:this.$=q.addNote(j[Y],j[Y-1]);break;case 72:this.$=q.addNote(j[Y]);break;case 73:this.$=j[Y-2],q.defineClass(j[Y-1],j[Y]);break;case 75:this.$=j[Y-2].concat([j[Y]]);break;case 76:q.setDirection("TB");break;case 77:q.setDirection("BT");break;case 78:q.setDirection("RL");break;case 79:q.setDirection("LR");break;case 80:this.$={type1:j[Y-2],type2:j[Y],lineType:j[Y-1]};break;case 81:this.$={type1:"none",type2:j[Y],lineType:j[Y-1]};break;case 82:this.$={type1:j[Y-1],type2:"none",lineType:j[Y]};break;case 83:this.$={type1:"none",type2:"none",lineType:j[Y]};break;case 84:this.$=q.relationType.AGGREGATION;break;case 85:this.$=q.relationType.EXTENSION;break;case 86:this.$=q.relationType.COMPOSITION;break;case 87:this.$=q.relationType.DEPENDENCY;break;case 88:this.$=q.relationType.LOLLIPOP;break;case 89:this.$=q.lineType.LINE;break;case 90:this.$=q.lineType.DOTTED_LINE;break;case 91:case 97:this.$=j[Y-2],q.setClickEvent(j[Y-1],j[Y]);break;case 92:case 98:this.$=j[Y-3],q.setClickEvent(j[Y-2],j[Y-1]),q.setTooltip(j[Y-2],j[Y]);break;case 93:this.$=j[Y-2],q.setLink(j[Y-1],j[Y]);break;case 94:this.$=j[Y-3],q.setLink(j[Y-2],j[Y-1],j[Y]);break;case 95:this.$=j[Y-3],q.setLink(j[Y-2],j[Y-1]),q.setTooltip(j[Y-2],j[Y]);break;case 96:this.$=j[Y-4],q.setLink(j[Y-3],j[Y-2],j[Y]),q.setTooltip(j[Y-3],j[Y-1]);break;case 99:this.$=j[Y-3],q.setClickEvent(j[Y-2],j[Y-1],j[Y]);break;case 100:this.$=j[Y-4],q.setClickEvent(j[Y-3],j[Y-2],j[Y-1]),q.setTooltip(j[Y-3],j[Y]);break;case 101:this.$=j[Y-3],q.setLink(j[Y-2],j[Y]);break;case 102:this.$=j[Y-4],q.setLink(j[Y-3],j[Y-1],j[Y]);break;case 103:this.$=j[Y-4],q.setLink(j[Y-3],j[Y-1]),q.setTooltip(j[Y-3],j[Y]);break;case 104:this.$=j[Y-5],q.setLink(j[Y-4],j[Y-2],j[Y]),q.setTooltip(j[Y-4],j[Y-1]);break;case 105:this.$=j[Y-2],q.setCssStyle(j[Y-1],j[Y]);break;case 106:q.setCssClass(j[Y-1],j[Y]);break;case 107:this.$=[j[Y]];break;case 108:j[Y-2].push(j[Y]),this.$=j[Y-2];break;case 110:this.$=j[Y-1]+j[Y];break}},"anonymous"),table:[{3:1,4:2,5:3,6:4,7:[1,6],10:5,16:39,17:40,19:21,21:7,23:8,24:9,25:10,26:11,27:12,28:13,29:14,30:15,31:16,32:17,33:t,35:r,37:n,38:22,42:i,43:23,46:a,48:o,51:l,52:u,54:h,56:d,57:f,60:p,62:m,63:g,64:y,65:v,75:x,76:b,78:T,82:w,83:C,86:S,100:k,102:R,103:M},{1:[3]},{1:[2,1]},{1:[2,2]},{1:[2,3]},e(P,[2,5],{8:[1,48]}),{8:[1,49]},e(N,[2,19],{22:[1,50]}),e(N,[2,21]),e(N,[2,22]),e(N,[2,23]),e(N,[2,24]),e(N,[2,25]),e(N,[2,26]),e(N,[2,27]),e(N,[2,28]),e(N,[2,29]),e(N,[2,30]),{34:[1,51]},{36:[1,52]},e(N,[2,33]),e(N,[2,63],{53:53,66:56,67:57,13:[1,54],22:[1,55],68:_,69:A,70:I,71:D,72:L,73:B,74:O}),{39:[1,65]},e($,[2,47],{39:[1,67],44:[1,66],46:[1,68]}),e(N,[2,65]),e(N,[2,66]),{16:69,60:p,86:S,100:k,102:R},{16:39,17:40,19:70,60:p,86:S,100:k,102:R,103:M},{16:39,17:40,19:71,60:p,86:S,100:k,102:R,103:M},{16:39,17:40,19:72,60:p,86:S,100:k,102:R,103:M},{60:[1,73]},{13:[1,74]},{16:39,17:40,19:75,60:p,86:S,100:k,102:R,103:M},{13:W,55:76},{58:78,60:[1,79]},e(N,[2,76]),e(N,[2,77]),e(N,[2,78]),e(N,[2,79]),e(V,[2,13],{16:39,17:40,19:81,18:[1,80],20:[1,82],60:p,86:S,100:k,102:R,103:M}),e(V,[2,15],{20:[1,83]}),{15:84,16:85,17:86,60:p,86:S,100:k,102:R,103:M},{16:39,17:40,19:87,60:p,86:S,100:k,102:R,103:M},e(z,[2,133]),e(z,[2,134]),e(z,[2,135]),e(z,[2,136]),e([1,8,9,12,13,20,22,39,41,44,46,68,69,70,71,72,73,74,79,81],[2,137]),e(P,[2,6],{10:5,21:7,23:8,24:9,25:10,26:11,27:12,28:13,29:14,30:15,31:16,32:17,19:21,38:22,43:23,16:39,17:40,5:88,33:t,35:r,37:n,42:i,46:a,48:o,51:l,52:u,54:h,56:d,57:f,60:p,62:m,63:g,64:y,65:v,75:x,76:b,78:T,82:w,83:C,86:S,100:k,102:R,103:M}),{5:89,10:5,16:39,17:40,19:21,21:7,23:8,24:9,25:10,26:11,27:12,28:13,29:14,30:15,31:16,32:17,33:t,35:r,37:n,38:22,42:i,43:23,46:a,48:o,51:l,52:u,54:h,56:d,57:f,60:p,62:m,63:g,64:y,65:v,75:x,76:b,78:T,82:w,83:C,86:S,100:k,102:R,103:M},e(N,[2,20]),e(N,[2,31]),e(N,[2,32]),{13:[1,91],16:39,17:40,19:90,60:p,86:S,100:k,102:R,103:M},{53:92,66:56,67:57,68:_,69:A,70:I,71:D,72:L,73:B,74:O},e(N,[2,64]),{67:93,73:B,74:O},e(H,[2,83],{66:94,68:_,69:A,70:I,71:D,72:L}),e(K,[2,84]),e(K,[2,85]),e(K,[2,86]),e(K,[2,87]),e(K,[2,88]),e(J,[2,89]),e(J,[2,90]),{8:[1,96],23:99,24:97,30:98,38:22,40:95,42:i,43:23,48:o,54:h,56:d},{16:100,60:p,86:S,100:k,102:R},{41:[1,102],45:101,51:ne},{16:104,60:p,86:S,100:k,102:R},{47:[1,105]},{13:[1,106]},{13:[1,107]},{79:[1,108],81:[1,109]},{22:Z,50:pe,59:110,60:te,82:fe,84:111,85:112,86:ce,87:de,88:Ee,89:we,90:Fe},{60:[1,122]},{13:W,55:123},e($,[2,72]),e($,[2,138]),{22:Z,50:pe,59:124,60:te,61:[1,125],82:fe,84:111,85:112,86:ce,87:de,88:Ee,89:we,90:Fe},e(Ye,[2,74]),{16:39,17:40,19:126,60:p,86:S,100:k,102:R,103:M},e(V,[2,16]),e(V,[2,17]),e(V,[2,18]),{11:127,12:He,39:[2,36]},e(ye,[2,9],{16:85,17:86,15:130,18:[1,129],60:p,86:S,100:k,102:R,103:M}),e(ye,[2,10]),e(Be,[2,55],{11:131,12:He}),e(P,[2,7]),{9:[1,132]},e(Ie,[2,67]),{16:39,17:40,19:133,60:p,86:S,100:k,102:R,103:M},{13:[1,135],16:39,17:40,19:134,60:p,86:S,100:k,102:R,103:M},e(H,[2,82],{66:136,68:_,69:A,70:I,71:D,72:L}),e(H,[2,81]),{41:[1,137]},{23:99,24:97,30:98,38:22,40:138,42:i,43:23,48:o,54:h,56:d},{8:[1,139],41:[2,38]},{8:[1,140],41:[2,41]},{8:[1,141],41:[2,44]},e($,[2,48],{39:[1,142]}),{41:[1,143]},e($,[2,50]),{41:[2,61],45:144,51:ne},{47:[1,145]},{16:39,17:40,19:146,60:p,86:S,100:k,102:R,103:M},e(N,[2,91],{13:[1,147]}),e(N,[2,93],{13:[1,149],77:[1,148]}),e(N,[2,97],{13:[1,150],80:[1,151]}),{13:[1,152]},e(N,[2,105],{61:Ne}),e(re,[2,107],{85:154,22:Z,50:pe,60:te,82:fe,86:ce,87:de,88:Ee,89:we,90:Fe}),e(ue,[2,109]),e(ue,[2,111]),e(ue,[2,112]),e(ue,[2,113]),e(ue,[2,114]),e(ue,[2,115]),e(ue,[2,116]),e(ue,[2,117]),e(ue,[2,118]),e(ue,[2,119]),e(N,[2,106]),e($,[2,71]),e(N,[2,73],{61:Ne}),{60:[1,155]},e(V,[2,14]),{39:[2,37]},{13:[1,156]},{15:157,16:85,17:86,60:p,86:S,100:k,102:R,103:M},e(ye,[2,12]),e(Be,[2,56]),{1:[2,4]},e(Ie,[2,69]),e(Ie,[2,68]),{16:39,17:40,19:158,60:p,86:S,100:k,102:R,103:M},e(H,[2,80]),e($,[2,34]),{41:[1,159]},{23:99,24:97,30:98,38:22,40:160,41:[2,39],42:i,43:23,48:o,54:h,56:d},{23:99,24:97,30:98,38:22,40:161,41:[2,42],42:i,43:23,48:o,54:h,56:d},{23:99,24:97,30:98,38:22,40:162,41:[2,45],42:i,43:23,48:o,54:h,56:d},{45:163,51:ne},e($,[2,49]),{41:[2,62]},e($,[2,52],{39:[1,164]}),e(N,[2,60]),e(N,[2,92]),e(N,[2,94]),e(N,[2,95],{77:[1,165]}),e(N,[2,98]),e(N,[2,99],{13:[1,166]}),e(N,[2,101],{13:[1,168],77:[1,167]}),{22:Z,50:pe,60:te,82:fe,84:169,85:112,86:ce,87:de,88:Ee,89:we,90:Fe},e(ue,[2,110]),e(Ye,[2,75]),{14:[1,170]},e(ye,[2,11]),e(Ie,[2,70]),e($,[2,35]),{41:[2,40]},{41:[2,43]},{41:[2,46]},{41:[1,171]},{41:[1,173],45:172,51:ne},e(N,[2,96]),e(N,[2,100]),e(N,[2,102]),e(N,[2,103],{77:[1,174]}),e(re,[2,108],{85:154,22:Z,50:pe,60:te,82:fe,86:ce,87:de,88:Ee,89:we,90:Fe}),e(Be,[2,8]),e($,[2,51]),{41:[1,175]},e($,[2,54]),e(N,[2,104]),e($,[2,53])],defaultActions:{2:[2,1],3:[2,2],4:[2,3],127:[2,37],132:[2,4],144:[2,62],160:[2,40],161:[2,43],162:[2,46]},parseError:s(function(G,X){if(X.recoverable)this.trace(G);else{var ee=new Error(G);throw ee.hash=X,ee}},"parseError"),parse:s(function(G){var X=this,ee=[0],q=[],ie=[null],j=[],U=this.table,Y="",Ae=0,me=0,Se=0,Ce=2,Me=1,_e=j.slice.call(arguments,1),ze=Object.create(this.lexer),Le={yy:{}};for(var qe in this.yy)Object.prototype.hasOwnProperty.call(this.yy,qe)&&(Le.yy[qe]=this.yy[qe]);ze.setInput(G,Le.yy),Le.yy.lexer=ze,Le.yy.parser=this,typeof ze.yylloc>"u"&&(ze.yylloc={});var ve=ze.yylloc;j.push(ve);var lt=ze.options&&ze.options.ranges;typeof Le.yy.parseError=="function"?this.parseError=Le.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Oe(dt){ee.length=ee.length-2*dt,ie.length=ie.length-dt,j.length=j.length-dt}s(Oe,"popStack");function mt(){var dt;return dt=q.pop()||ze.lex()||Me,typeof dt!="number"&&(dt instanceof Array&&(q=dt,dt=q.pop()),dt=X.symbols_[dt]||dt),dt}s(mt,"lex");for(var At,Gt,Ct,Rt,gt,be,ct={},xt,Ke,De,tt;;){if(Ct=ee[ee.length-1],this.defaultActions[Ct]?Rt=this.defaultActions[Ct]:((At===null||typeof At>"u")&&(At=mt()),Rt=U[Ct]&&U[Ct][At]),typeof Rt>"u"||!Rt.length||!Rt[0]){var Ze="";tt=[];for(xt in U[Ct])this.terminals_[xt]&&xt>Ce&&tt.push("'"+this.terminals_[xt]+"'");ze.showPosition?Ze="Parse error on line "+(Ae+1)+`: +`+ze.showPosition()+` +Expecting `+tt.join(", ")+", got '"+(this.terminals_[At]||At)+"'":Ze="Parse error on line "+(Ae+1)+": Unexpected "+(At==Me?"end of input":"'"+(this.terminals_[At]||At)+"'"),this.parseError(Ze,{text:ze.match,token:this.terminals_[At]||At,line:ze.yylineno,loc:ve,expected:tt})}if(Rt[0]instanceof Array&&Rt.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Ct+", token: "+At);switch(Rt[0]){case 1:ee.push(At),ie.push(ze.yytext),j.push(ze.yylloc),ee.push(Rt[1]),At=null,Gt?(At=Gt,Gt=null):(me=ze.yyleng,Y=ze.yytext,Ae=ze.yylineno,ve=ze.yylloc,Se>0&&Se--);break;case 2:if(Ke=this.productions_[Rt[1]][1],ct.$=ie[ie.length-Ke],ct._$={first_line:j[j.length-(Ke||1)].first_line,last_line:j[j.length-1].last_line,first_column:j[j.length-(Ke||1)].first_column,last_column:j[j.length-1].last_column},lt&&(ct._$.range=[j[j.length-(Ke||1)].range[0],j[j.length-1].range[1]]),be=this.performAction.apply(ct,[Y,me,Ae,Le.yy,Rt[1],ie,j].concat(_e)),typeof be<"u")return be;Ke&&(ee=ee.slice(0,-1*Ke*2),ie=ie.slice(0,-1*Ke),j=j.slice(0,-1*Ke)),ee.push(this.productions_[Rt[1]][0]),ie.push(ct.$),j.push(ct._$),De=U[ee[ee.length-2]][ee[ee.length-1]],ee.push(De);break;case 3:return!0}}return!0},"parse")},he=(function(){var oe={EOF:1,parseError:s(function(X,ee){if(this.yy.parser)this.yy.parser.parseError(X,ee);else throw new Error(X)},"parseError"),setInput:s(function(G,X){return this.yy=X||this.yy||{},this._input=G,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var G=this._input[0];this.yytext+=G,this.yyleng++,this.offset++,this.match+=G,this.matched+=G;var X=G.match(/(?:\r\n?|\n).*/g);return X?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),G},"input"),unput:s(function(G){var X=G.length,ee=G.split(/(?:\r\n?|\n)/g);this._input=G+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-X),this.offset-=X;var q=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),ee.length-1&&(this.yylineno-=ee.length-1);var ie=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:ee?(ee.length===q.length?this.yylloc.first_column:0)+q[q.length-ee.length].length-ee[0].length:this.yylloc.first_column-X},this.options.ranges&&(this.yylloc.range=[ie[0],ie[0]+this.yyleng-X]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(G){this.unput(this.match.slice(G))},"less"),pastInput:s(function(){var G=this.matched.substr(0,this.matched.length-this.match.length);return(G.length>20?"...":"")+G.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var G=this.match;return G.length<20&&(G+=this._input.substr(0,20-G.length)),(G.substr(0,20)+(G.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var G=this.pastInput(),X=new Array(G.length+1).join("-");return G+this.upcomingInput()+` +`+X+"^"},"showPosition"),test_match:s(function(G,X){var ee,q,ie;if(this.options.backtrack_lexer&&(ie={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(ie.yylloc.range=this.yylloc.range.slice(0))),q=G[0].match(/(?:\r\n?|\n).*/g),q&&(this.yylineno+=q.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:q?q[q.length-1].length-q[q.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+G[0].length},this.yytext+=G[0],this.match+=G[0],this.matches=G,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(G[0].length),this.matched+=G[0],ee=this.performAction.call(this,this.yy,this,X,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),ee)return ee;if(this._backtrack){for(var j in ie)this[j]=ie[j];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var G,X,ee,q;this._more||(this.yytext="",this.match="");for(var ie=this._currentRules(),j=0;jX[0].length)){if(X=ee,q=j,this.options.backtrack_lexer){if(G=this.test_match(ee,ie[j]),G!==!1)return G;if(this._backtrack){X=!1;continue}else return!1}else if(!this.options.flex)break}return X?(G=this.test_match(X,ie[q]),G!==!1?G:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var X=this.next();return X||this.lex()},"lex"),begin:s(function(X){this.conditionStack.push(X)},"begin"),popState:s(function(){var X=this.conditionStack.length-1;return X>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(X){return X=this.conditionStack.length-1-Math.abs(X||0),X>=0?this.conditionStack[X]:"INITIAL"},"topState"),pushState:s(function(X){this.begin(X)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:s(function(X,ee,q,ie){var j=ie;switch(q){case 0:return 62;case 1:return 63;case 2:return 64;case 3:return 65;case 4:break;case 5:break;case 6:return this.begin("acc_title"),33;break;case 7:return this.popState(),"acc_title_value";break;case 8:return this.begin("acc_descr"),35;break;case 9:return this.popState(),"acc_descr_value";break;case 10:this.begin("acc_descr_multiline");break;case 11:this.popState();break;case 12:return"acc_descr_multiline_value";case 13:return 8;case 14:break;case 15:return 7;case 16:return 7;case 17:return"EDGE_STATE";case 18:this.begin("callback_name");break;case 19:this.popState();break;case 20:this.popState(),this.begin("callback_args");break;case 21:return 79;case 22:this.popState();break;case 23:return 80;case 24:this.popState();break;case 25:return"STR";case 26:this.begin("string");break;case 27:return 82;case 28:return 57;case 29:return this.begin("namespace"),42;break;case 30:return this.popState(),8;break;case 31:break;case 32:return this.begin("namespace-body"),39;break;case 33:this.popState(),this.less(0);break;case 34:return this.popState(),41;break;case 35:return"EOF_IN_STRUCT";case 36:return 8;case 37:break;case 38:return"EDGE_STATE";case 39:return this.begin("class"),48;break;case 40:return this.popState(),8;break;case 41:break;case 42:return this.popState(),this.popState(),41;break;case 43:return this.begin("class-body"),39;break;case 44:return this.popState(),41;break;case 45:return"EOF_IN_STRUCT";case 46:return"EDGE_STATE";case 47:return"OPEN_IN_STRUCT";case 48:break;case 49:return"MEMBER";case 50:return 83;case 51:return 75;case 52:return 76;case 53:return 78;case 54:return 54;case 55:return 56;case 56:return 46;case 57:return 47;case 58:return 81;case 59:this.popState();break;case 60:return"GENERICTYPE";case 61:this.begin("generic");break;case 62:this.popState();break;case 63:return"BQUOTE_STR";case 64:this.begin("bqstring");break;case 65:return 77;case 66:return 77;case 67:return 77;case 68:return 77;case 69:return 69;case 70:return 69;case 71:return 71;case 72:return 71;case 73:return 70;case 74:return 68;case 75:return 72;case 76:return 73;case 77:return 74;case 78:return 22;case 79:return 44;case 80:return 100;case 81:return 18;case 82:return"PLUS";case 83:return 87;case 84:return 61;case 85:return 89;case 86:return 89;case 87:return 90;case 88:return"EQUALS";case 89:return"EQUALS";case 90:return 60;case 91:return 12;case 92:return 14;case 93:return"PUNCTUATION";case 94:return 86;case 95:return 102;case 96:return 50;case 97:return 50;case 98:return 9}},"anonymous"),rules:[/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:%%(?!\{)*[^\n]*(\r?\n?)+)/,/^(?:%%[^\n]*(\r?\n)*)/,/^(?:accTitle\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*\{\s*)/,/^(?:[\}])/,/^(?:[^\}]*)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:classDiagram-v2\b)/,/^(?:classDiagram\b)/,/^(?:\[\*\])/,/^(?:call[\s]+)/,/^(?:\([\s]*\))/,/^(?:\()/,/^(?:[^(]*)/,/^(?:\))/,/^(?:[^)]*)/,/^(?:["])/,/^(?:[^"]*)/,/^(?:["])/,/^(?:style\b)/,/^(?:classDef\b)/,/^(?:namespace\b)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:[{])/,/^(?:[}])/,/^(?:[}])/,/^(?:$)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:\[\*\])/,/^(?:class\b)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:[}])/,/^(?:[{])/,/^(?:[}])/,/^(?:$)/,/^(?:\[\*\])/,/^(?:[{])/,/^(?:[\n])/,/^(?:[^{}\n]*)/,/^(?:cssClass\b)/,/^(?:callback\b)/,/^(?:link\b)/,/^(?:click\b)/,/^(?:note for\b)/,/^(?:note\b)/,/^(?:<<)/,/^(?:>>)/,/^(?:href\b)/,/^(?:[~])/,/^(?:[^~]*)/,/^(?:~)/,/^(?:[`])/,/^(?:[^`]+)/,/^(?:[`])/,/^(?:_self\b)/,/^(?:_blank\b)/,/^(?:_parent\b)/,/^(?:_top\b)/,/^(?:\s*<\|)/,/^(?:\s*\|>)/,/^(?:\s*>)/,/^(?:\s*<)/,/^(?:\s*\*)/,/^(?:\s*o\b)/,/^(?:\s*\(\))/,/^(?:--)/,/^(?:\.\.)/,/^(?::{1}[^:\n;]+)/,/^(?::{3})/,/^(?:-)/,/^(?:\.)/,/^(?:\+)/,/^(?::)/,/^(?:,)/,/^(?:#)/,/^(?:#)/,/^(?:%)/,/^(?:=)/,/^(?:=)/,/^(?:\w+)/,/^(?:\[)/,/^(?:\])/,/^(?:[!"#$%&'*+,-.`?\\/])/,/^(?:[0-9]+)/,/^(?:[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|[\uFFD2-\uFFD7\uFFDA-\uFFDC])/,/^(?:\s)/,/^(?:\s)/,/^(?:$)/],conditions:{"namespace-body":{rules:[26,29,34,35,36,37,38,39,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},namespace:{rules:[26,29,30,31,32,33,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},"class-body":{rules:[26,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},class:{rules:[26,40,41,42,43,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},acc_descr_multiline:{rules:[11,12,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},acc_descr:{rules:[9,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},acc_title:{rules:[7,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},callback_args:{rules:[22,23,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},callback_name:{rules:[19,20,21,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},href:{rules:[26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},struct:{rules:[26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},generic:{rules:[26,50,51,52,53,54,55,56,57,58,59,60,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},bqstring:{rules:[26,50,51,52,53,54,55,56,57,58,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},string:{rules:[24,25,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,8,10,13,14,15,16,17,18,26,27,28,29,39,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98],inclusive:!0}}};return oe})();le.lexer=he;function Te(){this.yy={}}return s(Te,"Parser"),Te.prototype=le,le.Parser=Te,new Te})();yY.parser=yY;lL=yY});var fMe,VS,pMe=F(()=>{"use strict";Jt();Yr();fMe=["#","+","~","-",""],VS=class{static{s(this,"ClassMember")}constructor(t,r){this.memberType=r,this.visibility="",this.classifier="",this.text="";let n=xr(t,Pe());this.parseMember(n)}getDisplayDetails(){let t=this.visibility+Vc(this.id);this.memberType==="method"&&(t+=`(${Vc(this.parameters.trim())})`,this.returnType&&(t+=" : "+Vc(this.returnType))),t=t.trim();let r=this.parseClassifier();return{displayText:t,cssStyle:r}}parseMember(t){let r="";if(this.memberType==="method"){let a=/([#+~-])?(.+)\((.*)\)([\s$*])?(.*)([$*])?/.exec(t);if(a){let o=a[1]?a[1].trim():"";if(fMe.includes(o)&&(this.visibility=o),this.id=a[2],this.parameters=a[3]?a[3].trim():"",r=a[4]?a[4].trim():"",this.returnType=a[5]?a[5].trim():"",r===""){let l=this.returnType.substring(this.returnType.length-1);/[$*]/.exec(l)&&(r=l,this.returnType=this.returnType.substring(0,this.returnType.length-1))}}}else{let i=t.length,a=t.substring(0,1),o=t.substring(i-1);fMe.includes(a)&&(this.visibility=a),/[$*]/.exec(o)&&(r=o),this.id=t.substring(this.visibility===""?0:1,r===""?i:i-1)}this.classifier=r,this.id=this.id.startsWith(" ")?" "+this.id.trim():this.id.trim();let n=`${this.visibility?"\\"+this.visibility:""}${Vc(this.id)}${this.memberType==="method"?`(${Vc(this.parameters)})${this.returnType?" : "+Vc(this.returnType):""}`:""}`;this.text=n.replaceAll("<","<").replaceAll(">",">"),this.text.startsWith("\\<")&&(this.text=this.text.replace("\\<","~"))}parseClassifier(){switch(this.classifier){case"*":return"font-style:italic;";case"$":return"text-decoration:underline;";default:return""}}}});var cL,mMe,A0,Jv,xY=F(()=>{"use strict";Wr();St();Jt();Yr();er();$n();Xd();pMe();q0();cL="classId-",mMe=0,A0=s(e=>Tt.sanitizeText(e,Pe()),"sanitizeText"),Jv=class e{constructor(){this.relations=[];this.classes=new Map;this.styleClasses=new Map;this.notes=new Map;this.interfaces=[];this.namespaces=new Map;this.namespaceCounter=0;this.namespaceStack=[];this.diagramId="";this.functions=[];this.lineType={LINE:0,DOTTED_LINE:1};this.relationType={AGGREGATION:0,EXTENSION:1,COMPOSITION:2,DEPENDENCY:3,LOLLIPOP:4};this.setupToolTips=s(t=>{let r=_y();ut(t).select("svg").selectAll("g").filter(function(){return ut(this).attr("title")!==null}).on("mouseover",a=>{let o=ut(a.currentTarget),l=o.attr("title");if(!l)return;let u=a.currentTarget.getBoundingClientRect();r.transition().duration(200).style("opacity",".9"),r.html(ao.sanitize(l)).style("left",`${window.scrollX+u.left+u.width/2}px`).style("top",`${window.scrollY+u.bottom+4}px`),o.classed("hover",!0)}).on("mouseout",a=>{r.transition().duration(500).style("opacity",0),ut(a.currentTarget).classed("hover",!1)})},"setupToolTips");this.direction="TB";this.setAccTitle=Lr;this.getAccTitle=Ir;this.setAccDescription=Mr;this.getAccDescription=Nr;this.setDiagramTitle=Gr;this.getDiagramTitle=Pr;this.getConfig=s(()=>Pe().class,"getConfig");this.functions.push(this.setupToolTips.bind(this)),this.clear(),this.addRelation=this.addRelation.bind(this),this.addClassesToNamespace=this.addClassesToNamespace.bind(this),this.addNamespace=this.addNamespace.bind(this),this.popNamespace=this.popNamespace.bind(this),this.setCssClass=this.setCssClass.bind(this),this.addMembers=this.addMembers.bind(this),this.addClass=this.addClass.bind(this),this.setClassLabel=this.setClassLabel.bind(this),this.addAnnotation=this.addAnnotation.bind(this),this.addMember=this.addMember.bind(this),this.cleanupLabel=this.cleanupLabel.bind(this),this.addNote=this.addNote.bind(this),this.defineClass=this.defineClass.bind(this),this.setDirection=this.setDirection.bind(this),this.setLink=this.setLink.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.clear=this.clear.bind(this),this.setTooltip=this.setTooltip.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setCssStyle=this.setCssStyle.bind(this)}static{s(this,"ClassDB")}splitClassNameAndType(t){let r=Tt.sanitizeText(t,Pe()),n="",i=r;if(r.indexOf("~")>0){let a=r.split("~");i=A0(a[0]),n=A0(a[1])}return{className:i,type:n}}setClassLabel(t,r){let n=Tt.sanitizeText(t,Pe());r&&(r=A0(r));let{className:i}=this.splitClassNameAndType(n);this.classes.get(i).label=r,this.classes.get(i).text=`${r}${this.classes.get(i).type?`<${this.classes.get(i).type}>`:""}`}addClass(t){let r=Tt.sanitizeText(t,Pe()),{className:n,type:i}=this.splitClassNameAndType(r);if(this.classes.has(n))return;let a=Tt.sanitizeText(n,Pe());this.classes.set(a,{id:a,type:i,label:a,text:`${a}${i?`<${i}>`:""}`,shape:"classBox",cssClasses:"default",methods:[],members:[],annotations:[],styles:[],domId:cL+a+"-"+mMe}),mMe++}addInterface(t,r){let n={id:`interface${this.interfaces.length}`,label:t,classId:r};this.interfaces.push(n)}setDiagramId(t){this.diagramId=t}lookUpDomId(t){let r=Tt.sanitizeText(t,Pe());if(this.classes.has(r)){let n=this.classes.get(r).domId;return this.diagramId?`${this.diagramId}-${n}`:n}throw new Error("Class not found: "+r)}clear(){this.relations=[],this.classes=new Map,this.notes=new Map,this.interfaces=[],this.functions=[],this.functions.push(this.setupToolTips.bind(this)),this.namespaces=new Map,this.namespaceCounter=0,this.namespaceStack=[],this.diagramId="",this.direction="TB",Tr()}getClass(t){return this.classes.get(t)}getClasses(){return this.classes}getRelations(){return this.relations}getNote(t){let r=typeof t=="number"?`note${t}`:t;return this.notes.get(r)}getNotes(){return this.notes}addRelation(t){se.debug("Adding relation: "+JSON.stringify(t));let r=[this.relationType.LOLLIPOP,this.relationType.AGGREGATION,this.relationType.COMPOSITION,this.relationType.DEPENDENCY,this.relationType.EXTENSION];t.relation.type1===this.relationType.LOLLIPOP&&!r.includes(t.relation.type2)?(this.addClass(t.id2),this.addInterface(t.id1,t.id2),t.id1=`interface${this.interfaces.length-1}`):t.relation.type2===this.relationType.LOLLIPOP&&!r.includes(t.relation.type1)?(this.addClass(t.id1),this.addInterface(t.id2,t.id1),t.id2=`interface${this.interfaces.length-1}`):(this.addClass(t.id1),this.addClass(t.id2)),t.id1=this.splitClassNameAndType(t.id1).className,t.id2=this.splitClassNameAndType(t.id2).className,t.relationTitle1=Tt.sanitizeText(t.relationTitle1.trim(),Pe()),t.relationTitle2=Tt.sanitizeText(t.relationTitle2.trim(),Pe()),this.relations.push(t)}addAnnotation(t,r){let n=this.splitClassNameAndType(t).className;this.classes.get(n).annotations.push(r)}addMember(t,r){this.addClass(t);let n=this.splitClassNameAndType(t).className,i=this.classes.get(n);if(typeof r=="string"){let a=r.trim();a.startsWith("<<")&&a.endsWith(">>")?i.annotations.push(A0(a.substring(2,a.length-2))):a.indexOf(")")>0?i.methods.push(new VS(a,"method")):a&&i.members.push(new VS(a,"attribute"))}}addMembers(t,r){Array.isArray(r)&&(r.reverse(),r.forEach(n=>this.addMember(t,n)))}addNote(t,r){let n=this.notes.size,i={id:`note${n}`,class:r,text:t,index:n};return this.notes.set(i.id,i),i.id}cleanupLabel(t){return t.startsWith(":")&&(t=t.substring(1)),A0(t.trim())}setCssClass(t,r){t.split(",").forEach(n=>{let i=n;/\d/.exec(n[0])&&(i=cL+i),i=this.splitClassNameAndType(i).className;let a=this.classes.get(i);a&&(a.cssClasses+=" "+r)})}defineClass(t,r){for(let n of t){let i=this.styleClasses.get(n);i===void 0&&(i={id:n,styles:[],textStyles:[]},this.styleClasses.set(n,i)),r&&r.forEach(a=>{if(/color/.exec(a)){let o=a.replace("fill","bgFill");i.textStyles.push(o)}i.styles.push(a)}),this.classes.forEach(a=>{a.cssClasses.includes(n)&&a.styles.push(...r.flatMap(o=>o.split(",")))})}}setTooltip(t,r){t.split(",").forEach(n=>{if(r!==void 0){let i=this.splitClassNameAndType(n).className,a=this.classes.get(i);a&&(a.tooltip=A0(r))}})}getTooltip(t,r){return r&&this.namespaces.has(r)?this.namespaces.get(r).classes.get(t).tooltip:this.classes.get(t).tooltip}setLink(t,r,n){let i=Pe();t.split(",").forEach(a=>{let o=a;/\d/.exec(a[0])&&(o=cL+o),o=this.splitClassNameAndType(o).className;let l=this.classes.get(o);l&&(l.link=or.formatUrl(r,i),i.securityLevel==="sandbox"?l.linkTarget="_top":typeof n=="string"?l.linkTarget=A0(n):l.linkTarget="_blank")}),this.setCssClass(t,"clickable")}setClickEvent(t,r,n){t.split(",").forEach(i=>{this.setClickFunc(i,r,n);let a=this.splitClassNameAndType(i).className,o=this.classes.get(a);o&&(o.haveCallback=!0)}),this.setCssClass(t,"clickable")}setClickFunc(t,r,n){let i=Tt.sanitizeText(t,Pe());if(Pe().securityLevel!=="loose"||r===void 0)return;let o=this.splitClassNameAndType(i).className;if(this.classes.has(o)){let l=[];if(typeof n=="string"){l=n.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);for(let u=0;u{let u=this.lookUpDomId(o),h=document.querySelector(`[id="${u}"]`);h!==null&&h.addEventListener("click",()=>{or.runFunc(r,...l)},!1)})}}bindFunctions(t){this.functions.forEach(r=>{r(t)})}escapeHtml(t){return t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}getDirection(){return this.direction}setDirection(t){this.direction=t}static resolveQualifiedId(t,r){let n=r.at(-1);return n?`${n}.${t}`:t}static getAncestorIds(t){let r=t.split("."),n=new Array(r.length);n[0]=r[0];for(let i=1;i0?a[o-1]:void 0,h=o===a.length-1,d=h&&r?r:i[o];this.namespaces.has(l)?h&&(this.namespaces.get(l).explicit=!0):this.namespaces.set(l,this.createNamespaceNode(l,d,u,h)),u&&this.linkParentChild(u,l)}return n}popNamespace(){this.namespaceStack.pop()}getNamespace(t){return this.namespaces.get(t)}getNamespaces(){return this.namespaces}addClassesToNamespace(t,r,n){if(this.namespaces.has(t)){for(let i of r){let{className:a}=this.splitClassNameAndType(i),o=this.getClass(a);o.parent=t,this.namespaces.get(t).classes.set(a,o)}for(let i of n){let a=this.getNote(i);a.parent=t,this.namespaces.get(t).notes.set(i,a)}}}setCssStyle(t,r){let n=this.classes.get(t);if(!(!r||!n))for(let i of r)i.includes(",")?n.styles.push(...i.split(",")):n.styles.push(i)}getArrowMarker(t){let r;switch(t){case 0:r="aggregation";break;case 1:r="extension";break;case 2:r="composition";break;case 3:r="dependency";break;case 4:r="lollipop";break;default:r="none"}return r}resolveExplicitAncestor(t){let r=t;for(;r;){let n=this.namespaces.get(r);if(!n)return;if(n.explicit)return r;r=n.parent}}getData(){let t=[],r=[],n=Pe(),i=n.class?.hierarchicalNamespaces??!0;for(let o of this.namespaces.values()){if(!i&&!o.explicit)continue;let l={id:o.id,label:i?o.label:o.id,isGroup:!0,padding:n.class.padding??16,shape:"rect",cssStyles:[],look:n.look,parentId:i?o.parent:void 0};t.push(l)}for(let o of this.classes.values()){let l=i?o.parent:this.resolveExplicitAncestor(o.parent),u={...o,type:void 0,isGroup:!1,parentId:l,look:n.look};t.push(u)}for(let o of this.notes.values()){let l=i?o.parent:this.resolveExplicitAncestor(o.parent),u={id:o.id,label:o.text,isGroup:!1,shape:"note",padding:n.class.padding??6,cssStyles:["text-align: left","white-space: nowrap",`fill: ${n.themeVariables.noteBkgColor}`,`stroke: ${n.themeVariables.noteBorderColor}`],look:n.look,parentId:l,labelType:"markdown"};t.push(u);let h=this.classes.get(o.class)?.id;if(h){let d={id:`edgeNote${o.index}`,start:o.id,end:h,type:"normal",thickness:"normal",classes:"relation",arrowTypeStart:"none",arrowTypeEnd:"none",arrowheadStyle:"",labelStyle:[""],style:["fill: none"],pattern:"dotted",look:n.look};r.push(d)}}for(let o of this.interfaces){let l={id:o.id,label:o.label,isGroup:!1,shape:"rect",cssStyles:["opacity: 0;"],look:n.look};t.push(l)}let a=0;for(let o of this.relations){a++;let l={id:Jc(o.id1,o.id2,{prefix:"id",counter:a}),start:o.id1,end:o.id2,type:"normal",label:o.title,labelpos:"c",thickness:"normal",classes:"relation",arrowTypeStart:this.getArrowMarker(o.relation.type1),arrowTypeEnd:this.getArrowMarker(o.relation.type2),startLabelRight:o.relationTitle1==="none"?"":o.relationTitle1,endLabelLeft:o.relationTitle2==="none"?"":o.relationTitle2,arrowheadStyle:"",labelStyle:["display: inline-block"],style:o.style||"",pattern:o.relation.lineType==1?"dashed":"solid",look:n.look,labelType:"markdown"};r.push(l)}return{nodes:t,edges:r,other:{},config:n,direction:this.getDirection()}}}});var JAt,uL,bY=F(()=>{"use strict";U1();JAt=s(e=>`g.classGroup text { + fill: ${e.nodeBorder||e.classText}; + stroke: none; + font-family: ${e.fontFamily}; + font-size: 10px; + + .title { + font-weight: bolder; + } + +} + + .cluster-label text { + fill: ${e.titleColor}; + } + .cluster-label span { + color: ${e.titleColor}; + } + .cluster-label span p { + background-color: transparent; + } + + .cluster rect { + fill: ${e.clusterBkg}; + stroke: ${e.clusterBorder}; + stroke-width: 1px; + } + + .cluster text { + fill: ${e.titleColor}; + } + + .cluster span { + color: ${e.titleColor}; + } + +.nodeLabel, .edgeLabel { + color: ${e.classText}; +} + +.noteLabel .nodeLabel, .noteLabel .edgeLabel { + color: ${e.noteTextColor}; +} +.edgeLabel .label rect { + fill: ${e.mainBkg}; +} +.label text { + fill: ${e.classText}; +} + +.labelBkg { + background: ${e.mainBkg}; +} +.edgeLabel .label span { + background: ${e.mainBkg}; +} + +.classTitle { + font-weight: bolder; +} +.node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: ${e.strokeWidth}; + } + + +.divider { + stroke: ${e.nodeBorder}; + stroke-width: 1; +} + +g.clickable { + cursor: pointer; +} + +g.classGroup rect { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; +} + +g.classGroup line { + stroke: ${e.nodeBorder}; + stroke-width: 1; +} + +.classLabel .box { + stroke: none; + stroke-width: 0; + fill: ${e.mainBkg}; + opacity: 0.5; +} + +.classLabel .label { + fill: ${e.nodeBorder}; + font-size: 10px; +} + +.relation { + stroke: ${e.lineColor}; + stroke-width: ${e.strokeWidth}; + fill: none; +} + +.dashed-line{ + stroke-dasharray: 3; +} + +.dotted-line{ + stroke-dasharray: 1 2; +} + +[id$="-compositionStart"], .composition { + fill: ${e.lineColor} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-compositionEnd"], .composition { + fill: ${e.lineColor} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-dependencyStart"], .dependency { + fill: ${e.lineColor} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-dependencyEnd"], .dependency { + fill: ${e.lineColor} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-extensionStart"], .extension { + fill: transparent !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-extensionEnd"], .extension { + fill: transparent !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-aggregationStart"], .aggregation { + fill: transparent !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-aggregationEnd"], .aggregation { + fill: transparent !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-lollipopStart"], .lollipop { + fill: ${e.mainBkg} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-lollipopEnd"], .lollipop { + fill: ${e.mainBkg} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +.edgeTerminals { + font-size: 11px; + line-height: initial; +} + +.classTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; +} + +.edgeLabel[data-look="neo"] { + background-color: ${e.edgeLabelBackground}; + p { + background-color: ${e.edgeLabelBackground}; + } + rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; +} + ${Ru()} +`,"getStyles"),uL=JAt});var eRt,tRt,rRt,hL,TY=F(()=>{"use strict";Jt();St();Rm();ep();tp();er();eRt=s((e,t="TB")=>{if(!e.doc)return t;let r=t;for(let n of e.doc)n.stmt==="dir"&&(r=n.value);return r},"getDir"),tRt=s(function(e,t){return t.db.getClasses()},"getClasses"),rRt=s(async function(e,t,r,n){se.info("REF0:"),se.info("Drawing class diagram (v3)",t);let{securityLevel:i,state:a,layout:o}=Pe();n.db.setDiagramId(t);let l=n.db.getData(),u=xl(t,i);l.type=n.type,l.layoutAlgorithm=Au(o),l.nodeSpacing=a?.nodeSpacing||50,l.rankSpacing=a?.rankSpacing||50,l.markers=["aggregation","extension","composition","dependency","lollipop"],l.diagramId=t,await Dl(l,u);let h=8;or.insertTitle(u,"classDiagramTitleText",a?.titleTopMargin??25,n.db.getDiagramTitle()),Co(u,h,"classDiagram",a?.useMaxWidth??!0)},"draw"),hL={getClasses:tRt,draw:rRt,getDir:eRt}});var gMe={};sr(gMe,{diagram:()=>nRt});var nRt,yMe=F(()=>{"use strict";vY();xY();bY();TY();nRt={parser:lL,get db(){return new Jv},renderer:hL,styles:uL,init:s(e=>{e.class||(e.class={}),e.class.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")}});var bMe={};sr(bMe,{diagram:()=>oRt});var oRt,TMe=F(()=>{"use strict";vY();xY();bY();TY();oRt={parser:lL,get db(){return new Jv},renderer:hL,styles:uL,init:s(e=>{e.class||(e.class={}),e.class.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")}});var CY,dL,SY=F(()=>{"use strict";CY=(function(){var e=s(function($,W,V,z){for(V=V||{},z=$.length;z--;V[$[z]]=W);return V},"o"),t=[1,2],r=[1,3],n=[1,4],i=[2,4],a=[1,9],o=[1,11],l=[1,16],u=[1,17],h=[1,18],d=[1,19],f=[1,33],p=[1,20],m=[1,21],g=[1,22],y=[1,23],v=[1,24],x=[1,26],b=[1,27],T=[1,28],w=[1,29],C=[1,30],S=[1,31],k=[1,32],R=[1,35],M=[1,36],P=[1,37],N=[1,38],_=[1,34],A=[1,4,5,16,17,19,21,22,24,25,26,27,28,29,33,35,37,38,41,45,48,51,52,53,54,57],I=[1,4,5,14,15,16,17,19,21,22,24,25,26,27,28,29,33,35,37,38,39,40,41,45,48,51,52,53,54,57],D=[4,5,16,17,19,21,22,24,25,26,27,28,29,33,35,37,38,41,45,48,51,52,53,54,57],L={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,SPACE:4,NL:5,SD:6,document:7,line:8,statement:9,classDefStatement:10,styleStatement:11,cssClassStatement:12,idStatement:13,DESCR:14,"-->":15,HIDE_EMPTY:16,scale:17,WIDTH:18,COMPOSIT_STATE:19,STRUCT_START:20,STRUCT_STOP:21,STATE_DESCR:22,AS:23,ID:24,FORK:25,JOIN:26,CHOICE:27,CONCURRENT:28,note:29,notePosition:30,NOTE_TEXT:31,direction:32,acc_title:33,acc_title_value:34,acc_descr:35,acc_descr_value:36,acc_descr_multiline_value:37,CLICK:38,STRING:39,HREF:40,classDef:41,CLASSDEF_ID:42,CLASSDEF_STYLEOPTS:43,DEFAULT:44,style:45,STYLE_IDS:46,STYLEDEF_STYLEOPTS:47,class:48,CLASSENTITY_IDS:49,STYLECLASS:50,direction_tb:51,direction_bt:52,direction_rl:53,direction_lr:54,eol:55,";":56,EDGE_STATE:57,STYLE_SEPARATOR:58,left_of:59,right_of:60,$accept:0,$end:1},terminals_:{2:"error",4:"SPACE",5:"NL",6:"SD",14:"DESCR",15:"-->",16:"HIDE_EMPTY",17:"scale",18:"WIDTH",19:"COMPOSIT_STATE",20:"STRUCT_START",21:"STRUCT_STOP",22:"STATE_DESCR",23:"AS",24:"ID",25:"FORK",26:"JOIN",27:"CHOICE",28:"CONCURRENT",29:"note",31:"NOTE_TEXT",33:"acc_title",34:"acc_title_value",35:"acc_descr",36:"acc_descr_value",37:"acc_descr_multiline_value",38:"CLICK",39:"STRING",40:"HREF",41:"classDef",42:"CLASSDEF_ID",43:"CLASSDEF_STYLEOPTS",44:"DEFAULT",45:"style",46:"STYLE_IDS",47:"STYLEDEF_STYLEOPTS",48:"class",49:"CLASSENTITY_IDS",50:"STYLECLASS",51:"direction_tb",52:"direction_bt",53:"direction_rl",54:"direction_lr",56:";",57:"EDGE_STATE",58:"STYLE_SEPARATOR",59:"left_of",60:"right_of"},productions_:[0,[3,2],[3,2],[3,2],[7,0],[7,2],[8,2],[8,1],[8,1],[9,1],[9,1],[9,1],[9,1],[9,2],[9,3],[9,4],[9,1],[9,2],[9,1],[9,4],[9,3],[9,6],[9,1],[9,1],[9,1],[9,1],[9,4],[9,4],[9,1],[9,2],[9,2],[9,1],[9,5],[9,5],[10,3],[10,3],[11,3],[12,3],[32,1],[32,1],[32,1],[32,1],[55,1],[55,1],[13,1],[13,1],[13,3],[13,3],[30,1],[30,1]],performAction:s(function(W,V,z,H,K,J,ne){var Z=J.length-1;switch(K){case 3:return H.setRootDoc(J[Z]),J[Z];break;case 4:this.$=[];break;case 5:J[Z]!="nl"&&(J[Z-1].push(J[Z]),this.$=J[Z-1]);break;case 6:case 7:this.$=J[Z];break;case 8:this.$="nl";break;case 12:this.$=J[Z];break;case 13:let ce=J[Z-1];ce.description=H.trimColon(J[Z]),this.$=ce;break;case 14:this.$={stmt:"relation",state1:J[Z-2],state2:J[Z]};break;case 15:let de=H.trimColon(J[Z]);this.$={stmt:"relation",state1:J[Z-3],state2:J[Z-1],description:de};break;case 19:this.$={stmt:"state",id:J[Z-3],type:"default",description:"",doc:J[Z-1]};break;case 20:var pe=J[Z],te=J[Z-2].trim();if(J[Z].match(":")){var fe=J[Z].split(":");pe=fe[0],te=[te,fe[1]]}this.$={stmt:"state",id:pe,type:"default",description:te};break;case 21:this.$={stmt:"state",id:J[Z-3],type:"default",description:J[Z-5],doc:J[Z-1]};break;case 22:this.$={stmt:"state",id:J[Z],type:"fork"};break;case 23:this.$={stmt:"state",id:J[Z],type:"join"};break;case 24:this.$={stmt:"state",id:J[Z],type:"choice"};break;case 25:this.$={stmt:"state",id:H.getDividerId(),type:"divider"};break;case 26:this.$={stmt:"state",id:J[Z-1].trim(),note:{position:J[Z-2].trim(),text:J[Z].trim()}};break;case 29:this.$=J[Z].trim(),H.setAccTitle(this.$);break;case 30:case 31:this.$=J[Z].trim(),H.setAccDescription(this.$);break;case 32:this.$={stmt:"click",id:J[Z-3],url:J[Z-2],tooltip:J[Z-1]};break;case 33:this.$={stmt:"click",id:J[Z-3],url:J[Z-1],tooltip:""};break;case 34:case 35:this.$={stmt:"classDef",id:J[Z-1].trim(),classes:J[Z].trim()};break;case 36:this.$={stmt:"style",id:J[Z-1].trim(),styleClass:J[Z].trim()};break;case 37:this.$={stmt:"applyClass",id:J[Z-1].trim(),styleClass:J[Z].trim()};break;case 38:H.setDirection("TB"),this.$={stmt:"dir",value:"TB"};break;case 39:H.setDirection("BT"),this.$={stmt:"dir",value:"BT"};break;case 40:H.setDirection("RL"),this.$={stmt:"dir",value:"RL"};break;case 41:H.setDirection("LR"),this.$={stmt:"dir",value:"LR"};break;case 44:case 45:this.$={stmt:"state",id:J[Z].trim(),type:"default",description:""};break;case 46:this.$={stmt:"state",id:J[Z-2].trim(),classes:[J[Z].trim()],type:"default",description:""};break;case 47:this.$={stmt:"state",id:J[Z-2].trim(),classes:[J[Z].trim()],type:"default",description:""};break}},"anonymous"),table:[{3:1,4:t,5:r,6:n},{1:[3]},{3:5,4:t,5:r,6:n},{3:6,4:t,5:r,6:n},e([1,4,5,16,17,19,22,24,25,26,27,28,29,33,35,37,38,41,45,48,51,52,53,54,57],i,{7:7}),{1:[2,1]},{1:[2,2]},{1:[2,3],4:a,5:o,8:8,9:10,10:12,11:13,12:14,13:15,16:l,17:u,19:h,22:d,24:f,25:p,26:m,27:g,28:y,29:v,32:25,33:x,35:b,37:T,38:w,41:C,45:S,48:k,51:R,52:M,53:P,54:N,57:_},e(A,[2,5]),{9:39,10:12,11:13,12:14,13:15,16:l,17:u,19:h,22:d,24:f,25:p,26:m,27:g,28:y,29:v,32:25,33:x,35:b,37:T,38:w,41:C,45:S,48:k,51:R,52:M,53:P,54:N,57:_},e(A,[2,7]),e(A,[2,8]),e(A,[2,9]),e(A,[2,10]),e(A,[2,11]),e(A,[2,12],{14:[1,40],15:[1,41]}),e(A,[2,16]),{18:[1,42]},e(A,[2,18],{20:[1,43]}),{23:[1,44]},e(A,[2,22]),e(A,[2,23]),e(A,[2,24]),e(A,[2,25]),{30:45,31:[1,46],59:[1,47],60:[1,48]},e(A,[2,28]),{34:[1,49]},{36:[1,50]},e(A,[2,31]),{13:51,24:f,57:_},{42:[1,52],44:[1,53]},{46:[1,54]},{49:[1,55]},e(I,[2,44],{58:[1,56]}),e(I,[2,45],{58:[1,57]}),e(A,[2,38]),e(A,[2,39]),e(A,[2,40]),e(A,[2,41]),e(A,[2,6]),e(A,[2,13]),{13:58,24:f,57:_},e(A,[2,17]),e(D,i,{7:59}),{24:[1,60]},{24:[1,61]},{23:[1,62]},{24:[2,48]},{24:[2,49]},e(A,[2,29]),e(A,[2,30]),{39:[1,63],40:[1,64]},{43:[1,65]},{43:[1,66]},{47:[1,67]},{50:[1,68]},{24:[1,69]},{24:[1,70]},e(A,[2,14],{14:[1,71]}),{4:a,5:o,8:8,9:10,10:12,11:13,12:14,13:15,16:l,17:u,19:h,21:[1,72],22:d,24:f,25:p,26:m,27:g,28:y,29:v,32:25,33:x,35:b,37:T,38:w,41:C,45:S,48:k,51:R,52:M,53:P,54:N,57:_},e(A,[2,20],{20:[1,73]}),{31:[1,74]},{24:[1,75]},{39:[1,76]},{39:[1,77]},e(A,[2,34]),e(A,[2,35]),e(A,[2,36]),e(A,[2,37]),e(I,[2,46]),e(I,[2,47]),e(A,[2,15]),e(A,[2,19]),e(D,i,{7:78}),e(A,[2,26]),e(A,[2,27]),{5:[1,79]},{5:[1,80]},{4:a,5:o,8:8,9:10,10:12,11:13,12:14,13:15,16:l,17:u,19:h,21:[1,81],22:d,24:f,25:p,26:m,27:g,28:y,29:v,32:25,33:x,35:b,37:T,38:w,41:C,45:S,48:k,51:R,52:M,53:P,54:N,57:_},e(A,[2,32]),e(A,[2,33]),e(A,[2,21])],defaultActions:{5:[2,1],6:[2,2],47:[2,48],48:[2,49]},parseError:s(function(W,V){if(V.recoverable)this.trace(W);else{var z=new Error(W);throw z.hash=V,z}},"parseError"),parse:s(function(W){var V=this,z=[0],H=[],K=[null],J=[],ne=this.table,Z="",pe=0,te=0,fe=0,ce=2,de=1,Ee=J.slice.call(arguments,1),we=Object.create(this.lexer),Fe={yy:{}};for(var Ye in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Ye)&&(Fe.yy[Ye]=this.yy[Ye]);we.setInput(W,Fe.yy),Fe.yy.lexer=we,Fe.yy.parser=this,typeof we.yylloc>"u"&&(we.yylloc={});var He=we.yylloc;J.push(He);var ye=we.options&&we.options.ranges;typeof Fe.yy.parseError=="function"?this.parseError=Fe.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Be(j){z.length=z.length-2*j,K.length=K.length-j,J.length=J.length-j}s(Be,"popStack");function Ie(){var j;return j=H.pop()||we.lex()||de,typeof j!="number"&&(j instanceof Array&&(H=j,j=H.pop()),j=V.symbols_[j]||j),j}s(Ie,"lex");for(var Ne,re,ue,le,he,Te,oe={},G,X,ee,q;;){if(ue=z[z.length-1],this.defaultActions[ue]?le=this.defaultActions[ue]:((Ne===null||typeof Ne>"u")&&(Ne=Ie()),le=ne[ue]&&ne[ue][Ne]),typeof le>"u"||!le.length||!le[0]){var ie="";q=[];for(G in ne[ue])this.terminals_[G]&&G>ce&&q.push("'"+this.terminals_[G]+"'");we.showPosition?ie="Parse error on line "+(pe+1)+`: +`+we.showPosition()+` +Expecting `+q.join(", ")+", got '"+(this.terminals_[Ne]||Ne)+"'":ie="Parse error on line "+(pe+1)+": Unexpected "+(Ne==de?"end of input":"'"+(this.terminals_[Ne]||Ne)+"'"),this.parseError(ie,{text:we.match,token:this.terminals_[Ne]||Ne,line:we.yylineno,loc:He,expected:q})}if(le[0]instanceof Array&&le.length>1)throw new Error("Parse Error: multiple actions possible at state: "+ue+", token: "+Ne);switch(le[0]){case 1:z.push(Ne),K.push(we.yytext),J.push(we.yylloc),z.push(le[1]),Ne=null,re?(Ne=re,re=null):(te=we.yyleng,Z=we.yytext,pe=we.yylineno,He=we.yylloc,fe>0&&fe--);break;case 2:if(X=this.productions_[le[1]][1],oe.$=K[K.length-X],oe._$={first_line:J[J.length-(X||1)].first_line,last_line:J[J.length-1].last_line,first_column:J[J.length-(X||1)].first_column,last_column:J[J.length-1].last_column},ye&&(oe._$.range=[J[J.length-(X||1)].range[0],J[J.length-1].range[1]]),Te=this.performAction.apply(oe,[Z,te,pe,Fe.yy,le[1],K,J].concat(Ee)),typeof Te<"u")return Te;X&&(z=z.slice(0,-1*X*2),K=K.slice(0,-1*X),J=J.slice(0,-1*X)),z.push(this.productions_[le[1]][0]),K.push(oe.$),J.push(oe._$),ee=ne[z[z.length-2]][z[z.length-1]],z.push(ee);break;case 3:return!0}}return!0},"parse")},B=(function(){var $={EOF:1,parseError:s(function(V,z){if(this.yy.parser)this.yy.parser.parseError(V,z);else throw new Error(V)},"parseError"),setInput:s(function(W,V){return this.yy=V||this.yy||{},this._input=W,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var W=this._input[0];this.yytext+=W,this.yyleng++,this.offset++,this.match+=W,this.matched+=W;var V=W.match(/(?:\r\n?|\n).*/g);return V?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),W},"input"),unput:s(function(W){var V=W.length,z=W.split(/(?:\r\n?|\n)/g);this._input=W+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-V),this.offset-=V;var H=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),z.length-1&&(this.yylineno-=z.length-1);var K=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:z?(z.length===H.length?this.yylloc.first_column:0)+H[H.length-z.length].length-z[0].length:this.yylloc.first_column-V},this.options.ranges&&(this.yylloc.range=[K[0],K[0]+this.yyleng-V]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(W){this.unput(this.match.slice(W))},"less"),pastInput:s(function(){var W=this.matched.substr(0,this.matched.length-this.match.length);return(W.length>20?"...":"")+W.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var W=this.match;return W.length<20&&(W+=this._input.substr(0,20-W.length)),(W.substr(0,20)+(W.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var W=this.pastInput(),V=new Array(W.length+1).join("-");return W+this.upcomingInput()+` +`+V+"^"},"showPosition"),test_match:s(function(W,V){var z,H,K;if(this.options.backtrack_lexer&&(K={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(K.yylloc.range=this.yylloc.range.slice(0))),H=W[0].match(/(?:\r\n?|\n).*/g),H&&(this.yylineno+=H.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:H?H[H.length-1].length-H[H.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+W[0].length},this.yytext+=W[0],this.match+=W[0],this.matches=W,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(W[0].length),this.matched+=W[0],z=this.performAction.call(this,this.yy,this,V,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),z)return z;if(this._backtrack){for(var J in K)this[J]=K[J];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var W,V,z,H;this._more||(this.yytext="",this.match="");for(var K=this._currentRules(),J=0;JV[0].length)){if(V=z,H=J,this.options.backtrack_lexer){if(W=this.test_match(z,K[J]),W!==!1)return W;if(this._backtrack){V=!1;continue}else return!1}else if(!this.options.flex)break}return V?(W=this.test_match(V,K[H]),W!==!1?W:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var V=this.next();return V||this.lex()},"lex"),begin:s(function(V){this.conditionStack.push(V)},"begin"),popState:s(function(){var V=this.conditionStack.length-1;return V>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(V){return V=this.conditionStack.length-1-Math.abs(V||0),V>=0?this.conditionStack[V]:"INITIAL"},"topState"),pushState:s(function(V){this.begin(V)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(V,z,H,K){function J(){let Z=z.yytext.indexOf("%%");if(Z===0)return!1;if(Z>0){let pe=z.yytext.slice(0,Z),te=z.yytext.slice(Z);te&&V.lexer.unput(te),z.yytext=pe}return!0}s(J,"processId");var ne=K;switch(H){case 0:return 38;case 1:return 40;case 2:return 39;case 3:return 44;case 4:return 51;case 5:return 52;case 6:return 53;case 7:return 54;case 8:return 5;case 9:break;case 10:break;case 11:break;case 12:break;case 13:return this.pushState("SCALE"),17;break;case 14:return 18;case 15:this.popState();break;case 16:return this.begin("acc_title"),33;break;case 17:return this.popState(),"acc_title_value";break;case 18:return this.begin("acc_descr"),35;break;case 19:return this.popState(),"acc_descr_value";break;case 20:this.begin("acc_descr_multiline");break;case 21:this.popState();break;case 22:return"acc_descr_multiline_value";case 23:return this.pushState("CLASSDEF"),41;break;case 24:return this.popState(),this.pushState("CLASSDEFID"),"DEFAULT_CLASSDEF_ID";break;case 25:return this.popState(),this.pushState("CLASSDEFID"),42;break;case 26:return this.popState(),43;break;case 27:return this.pushState("CLASS"),48;break;case 28:return this.popState(),this.pushState("CLASS_STYLE"),49;break;case 29:return this.popState(),50;break;case 30:return this.pushState("STYLE"),45;break;case 31:return this.popState(),this.pushState("STYLEDEF_STYLES"),46;break;case 32:return this.popState(),47;break;case 33:return this.pushState("SCALE"),17;break;case 34:return 18;case 35:this.popState();break;case 36:this.pushState("STATE");break;case 37:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),25;break;case 38:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),26;break;case 39:return this.popState(),z.yytext=z.yytext.slice(0,-10).trim(),27;break;case 40:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),25;break;case 41:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),26;break;case 42:return this.popState(),z.yytext=z.yytext.slice(0,-10).trim(),27;break;case 43:return 51;case 44:return 52;case 45:return 53;case 46:return 54;case 47:this.pushState("STATE_STRING");break;case 48:return this.pushState("STATE_ID"),"AS";break;case 49:if(!J())return;return this.popState(),"ID";break;case 50:this.popState();break;case 51:return"STATE_DESCR";case 52:throw new Error('Error: State name must be a single word. Found: "'+z.yytext.trim()+'"');case 53:return 19;case 54:this.popState();break;case 55:return this.popState(),this.pushState("struct"),20;break;case 56:return this.popState(),21;break;case 57:break;case 58:return this.begin("NOTE"),29;break;case 59:return this.popState(),this.pushState("NOTE_ID"),59;break;case 60:return this.popState(),this.pushState("NOTE_ID"),60;break;case 61:this.popState(),this.pushState("FLOATING_NOTE");break;case 62:return this.popState(),this.pushState("FLOATING_NOTE_ID"),"AS";break;case 63:break;case 64:return"NOTE_TEXT";case 65:if(!J())return;return this.popState(),"ID";break;case 66:if(!J())return;return this.popState(),this.pushState("NOTE_TEXT"),24;break;case 67:return this.popState(),z.yytext=z.yytext.substr(2).trim(),31;break;case 68:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),31;break;case 69:return 6;case 70:return 6;case 71:return 16;case 72:return 57;case 73:return J()?24:void 0;case 74:return z.yytext=z.yytext.trim(),14;break;case 75:return 15;case 76:return 28;case 77:return 58;case 78:return 5;case 79:return"INVALID"}},"anonymous"),rules:[/^(?:click\b)/i,/^(?:href\b)/i,/^(?:"[^"]*")/i,/^(?:default\b)/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:[\n]+)/i,/^(?:[\s]+)/i,/^(?:((?!\n)\s)+)/i,/^(?:#[^\n]*)/i,/^(?:%%(?!\{)[^\n]*)/i,/^(?:scale\s+)/i,/^(?:\d+)/i,/^(?:\s+width\b)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:classDef\s+)/i,/^(?:DEFAULT\s+)/i,/^(?:\w+\s+)/i,/^(?:[^\n]*)/i,/^(?:class\s+)/i,/^(?:(\w+)+((,\s*\w+)*))/i,/^(?:[^\n]*)/i,/^(?:style\s+)/i,/^(?:[\w,]+\s+)/i,/^(?:[^\n]*)/i,/^(?:scale\s+)/i,/^(?:\d+)/i,/^(?:\s+width\b)/i,/^(?:state\s+)/i,/^(?:.*<>)/i,/^(?:.*<>)/i,/^(?:.*<>)/i,/^(?:.*\[\[fork\]\])/i,/^(?:.*\[\[join\]\])/i,/^(?:.*\[\[choice\]\])/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:["])/i,/^(?:\s*as\s+)/i,/^(?:[^\n\{]*)/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:\w+\s+\w+.*?\{)/i,/^(?:[^\n\s\{]+)/i,/^(?:\n)/i,/^(?:\{)/i,/^(?:\})/i,/^(?:[\n])/i,/^(?:note\s+)/i,/^(?:left of\b)/i,/^(?:right of\b)/i,/^(?:")/i,/^(?:\s*as\s*)/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[^\n]*)/i,/^(?:\s*[^:\n\s\-]+)/i,/^(?:\s*:[^:\n;]+)/i,/^(?:[\s\S]*?\n\s*end note\b)/i,/^(?:stateDiagram\s+)/i,/^(?:stateDiagram-v2\s+)/i,/^(?:hide empty description\b)/i,/^(?:\[\*\])/i,/^(?:[^:\n\s\-\{]+)/i,/^(?:\s*:(?:[^:\n;]|:[^:\n;])+)/i,/^(?:-->)/i,/^(?:--)/i,/^(?::::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{LINE:{rules:[10,11,12],inclusive:!1},struct:{rules:[10,11,12,23,27,30,36,43,44,45,46,56,57,58,72,73,74,75,76,77],inclusive:!1},FLOATING_NOTE_ID:{rules:[65],inclusive:!1},FLOATING_NOTE:{rules:[62,63,64],inclusive:!1},NOTE_TEXT:{rules:[67,68],inclusive:!1},NOTE_ID:{rules:[66],inclusive:!1},NOTE:{rules:[59,60,61],inclusive:!1},STYLEDEF_STYLEOPTS:{rules:[],inclusive:!1},STYLEDEF_STYLES:{rules:[32],inclusive:!1},STYLE_IDS:{rules:[],inclusive:!1},STYLE:{rules:[31],inclusive:!1},CLASS_STYLE:{rules:[29],inclusive:!1},CLASS:{rules:[28],inclusive:!1},CLASSDEFID:{rules:[26],inclusive:!1},CLASSDEF:{rules:[24,25],inclusive:!1},acc_descr_multiline:{rules:[21,22],inclusive:!1},acc_descr:{rules:[19],inclusive:!1},acc_title:{rules:[17],inclusive:!1},SCALE:{rules:[14,15,34,35],inclusive:!1},ALIAS:{rules:[],inclusive:!1},STATE_ID:{rules:[49],inclusive:!1},STATE_STRING:{rules:[50,51],inclusive:!1},FORK_STATE:{rules:[],inclusive:!1},STATE:{rules:[10,11,12,37,38,39,40,41,42,47,48,52,53,54,55],inclusive:!1},ID:{rules:[10,11,12],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,11,12,13,16,18,20,23,27,30,33,36,55,58,69,70,71,72,73,74,75,77,78,79],inclusive:!0}}};return $})();L.lexer=B;function O(){this.yy={}}return s(O,"Parser"),O.prototype=L,L.Parser=O,new O})();CY.parser=CY;dL=CY});var Dp,R0,WS,wMe,kMe,EMe,_0,fL,wY,kY,EY,AY,pL,mL,AMe,RMe,RY,_Y,_Me,LMe,ex,hRt,DMe,LY,dRt,fRt,IMe,MMe,pRt,NMe,mRt,PMe,DY,IY,OMe,gL,BMe,MY,yL=F(()=>{"use strict";Dp="state",R0="root",WS="relation",wMe="classDef",kMe="style",EMe="applyClass",_0="default",fL="divider",wY="fill:none",kY="fill: #333",EY="markdown",AY="normal",pL="rect",mL="rectWithTitle",AMe="stateStart",RMe="stateEnd",RY="divider",_Y="roundedWithTitle",_Me="note",LMe="noteGroup",ex="statediagram",hRt="state",DMe=`${ex}-${hRt}`,LY="transition",dRt="note",fRt="note-edge",IMe=`${LY} ${fRt}`,MMe=`${ex}-${dRt}`,pRt="cluster",NMe=`${ex}-${pRt}`,mRt="cluster-alt",PMe=`${ex}-${mRt}`,DY="parent",IY="note",OMe="state",gL="----",BMe=`${gL}${IY}`,MY=`${gL}${DY}`});function NY(e="",t=0,r="",n=gL){let i=r!==null&&r.length>0?`${n}${r}`:"";return`${OMe}-${e}${i}-${t}`}function vL(e,t,r){if(!t.id||t.id===""||t.id==="")return;t.cssClasses&&(Array.isArray(t.cssCompiledStyles)||(t.cssCompiledStyles=[]),t.cssClasses.split(" ").forEach(i=>{let a=r.get(i);a&&(t.cssCompiledStyles=[...t.cssCompiledStyles??[],...a.styles])}));let n=e.find(i=>i.id===t.id);n?Object.assign(n,t):e.push(t)}function yRt(e){return e?.classes?.join(" ")??""}function vRt(e){return e?.styles??[]}var xL,Ip,gRt,$Me,tx,GMe,zMe=F(()=>{"use strict";Jt();St();Yr();yL();xL=new Map,Ip=0;s(NY,"stateDomId");gRt=s((e,t,r,n,i,a,o,l)=>{se.trace("items",t),t.forEach(u=>{switch(u.stmt){case Dp:tx(e,u,r,n,i,a,o,l);break;case _0:tx(e,u,r,n,i,a,o,l);break;case WS:{tx(e,u.state1,r,n,i,a,o,l),tx(e,u.state2,r,n,i,a,o,l);let h=o==="neo",d={id:"edge"+Ip,start:u.state1.id,end:u.state2.id,arrowhead:"normal",arrowTypeEnd:h?"arrow_barb_neo":"arrow_barb",style:wY,labelStyle:"",label:Tt.sanitizeText(u.description??"",Pe()),arrowheadStyle:kY,labelpos:"c",labelType:EY,thickness:AY,classes:LY,look:o};i.push(d),Ip++}break}})},"setupDoc"),$Me=s((e,t="TB")=>{let r=t;if(e.doc)for(let n of e.doc)n.stmt==="dir"&&(r=n.value);return r},"getDir");s(vL,"insertOrUpdateNode");s(yRt,"getClassesFromDbInfo");s(vRt,"getStylesFromDbInfo");tx=s((e,t,r,n,i,a,o,l)=>{let u=t.id,h=r.get(u),d=yRt(h),f=vRt(h),p=Pe();if(se.info("dataFetcher parsedItem",t,h,f),u!=="root"){let m=pL;t.start===!0?m=AMe:t.start===!1&&(m=RMe),t.type!==_0&&(m=t.type),xL.get(u)||xL.set(u,{id:u,shape:m,description:Tt.sanitizeText(u,p),cssClasses:`${d} ${DMe}`,cssStyles:f});let g=xL.get(u);t.description&&(Array.isArray(g.description)?(g.shape=mL,g.description.push(t.description)):g.description?.length&&g.description.length>0?(g.shape=mL,g.description===u?g.description=[t.description]:g.description=[g.description,t.description]):(g.shape=pL,g.description=t.description),g.description=Tt.sanitizeTextOrArray(g.description,p)),g.description?.length===1&&g.shape===mL&&(g.type==="group"?g.shape=_Y:g.shape=pL),!g.type&&t.doc&&(se.info("Setting cluster for XCX",u,$Me(t)),g.type="group",g.isGroup=!0,g.dir=$Me(t),g.shape=t.type===fL?RY:_Y,g.cssClasses=`${g.cssClasses} ${NMe} ${a?PMe:""}`);let y={labelStyle:"",shape:g.shape,label:g.description,cssClasses:g.cssClasses,cssCompiledStyles:[],cssStyles:g.cssStyles,id:u,dir:g.dir,domId:NY(u,Ip),type:g.type,isGroup:g.type==="group",padding:8,rx:10,ry:10,look:o,labelType:"markdown"};if(y.shape===RY&&(y.label=""),e&&e.id!=="root"&&(se.trace("Setting node ",u," to be child of its parent ",e.id),y.parentId=e.id),y.centerLabel=!0,t.note){let v={labelStyle:"",shape:_Me,label:t.note.text,labelType:"markdown",cssClasses:MMe,cssStyles:[],cssCompiledStyles:[],id:u+BMe+"-"+Ip,domId:NY(u,Ip,IY),type:g.type,isGroup:g.type==="group",padding:p.flowchart?.padding,look:o,position:t.note.position},x=u+MY,b={labelStyle:"",shape:LMe,label:t.note.text,cssClasses:g.cssClasses,cssStyles:[],id:u+MY,domId:NY(u,Ip,DY),type:"group",isGroup:!0,padding:16,look:o,position:t.note.position};Ip++,b.id=x,v.parentId=x,vL(n,b,l),vL(n,v,l),vL(n,y,l);let T=u,w=v.id;t.note.position==="left of"&&(T=v.id,w=u),i.push({id:T+"-"+w,start:T,end:w,arrowhead:"none",arrowTypeEnd:"",style:wY,labelStyle:"",classes:IMe,arrowheadStyle:kY,labelpos:"c",labelType:EY,thickness:AY,look:o})}else vL(n,y,l)}t.doc&&(se.trace("Adding nodes children "),gRt(t,t.doc,r,n,i,!a,o,l))},"dataFetcher"),GMe=s(()=>{xL.clear(),Ip=0},"reset")});var OY,xRt,bRt,VMe,BY=F(()=>{"use strict";Jt();St();Rm();ep();tp();er();yL();OY=s((e,t="TB")=>{if(!e.doc)return t;let r=t;for(let n of e.doc)n.stmt==="dir"&&(r=n.value);return r},"getDir"),xRt=s(function(e,t){return t.db.getClasses()},"getClasses"),bRt=s(async function(e,t,r,n){se.info("REF0:"),se.info("Drawing state diagram (v2)",t);let{securityLevel:i,state:a,layout:o}=Pe();n.db.extract(n.db.getRootDocV2());let l=n.db.getData(),u=xl(t,i);l.type=n.type,l.layoutAlgorithm=o,l.nodeSpacing=a?.nodeSpacing||50,l.rankSpacing=a?.rankSpacing||50,Pe().look==="neo"?l.markers=["barbNeo"]:l.markers=["barb"],l.diagramId=t,await Dl(l,u);let d=8;try{(typeof n.db.getLinks=="function"?n.db.getLinks():new Map).forEach((p,m)=>{let g=typeof m=="string"?m:typeof m?.id=="string"?m.id:"",y=l.nodes.find(C=>C.id===g);if(!g){se.warn("\u26A0\uFE0F Invalid or missing stateId from key:",JSON.stringify(m));return}let v=u.node()?.querySelectorAll("g.node, g.rough-node"),x;if(v?.forEach(C=>{let S=C.textContent?.trim();(C.id===y?.domId||S===g)&&(x=C)}),!x){se.warn("\u26A0\uFE0F Could not find node matching text:",g);return}let b=x.parentNode;if(!b){se.warn("\u26A0\uFE0F Node has no parent, cannot wrap:",g);return}let T=document.createElementNS("http://www.w3.org/2000/svg","a"),w=p.url.replace(/^"+|"+$/g,"");if(T.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",w),T.setAttribute("target","_blank"),p.tooltip){let C=p.tooltip.replace(/^"+|"+$/g,"");T.setAttribute("title",C),x.setAttribute("title",C)}b.replaceChild(T,x),T.appendChild(x),se.info("\u{1F517} Wrapped node in
    tag for:",g,p.url)})}catch(f){se.error("\u274C Error injecting clickable links:",f)}or.insertTitle(u,"statediagramTitleText",a?.titleTopMargin??25,n.db.getDiagramTitle()),Co(u,d,ex,a?.useMaxWidth??!0)},"draw"),VMe={getClasses:xRt,draw:bRt,getDir:OY}});var no,qMe,HMe,bL,$l,TL=F(()=>{"use strict";Wr();q0();Jt();St();er();Yr();$n();Xd();zMe();BY();yL();no={START_NODE:"[*]",START_TYPE:"start",END_NODE:"[*]",END_TYPE:"end",COLOR_KEYWORD:"color",FILL_KEYWORD:"fill",BG_FILL:"bgFill",STYLECLASS_SEP:","},qMe=s(()=>new Map,"newClassesList"),HMe=s(()=>({relations:[],states:new Map,documents:{}}),"newDoc"),bL=s(e=>JSON.parse(JSON.stringify(e)),"clone"),$l=class{constructor(t){this.version=t;this.nodes=[];this.edges=[];this.rootDoc=[];this.classes=qMe();this.documents={root:HMe()};this.currentDocument=this.documents.root;this.startEndCount=0;this.dividerCnt=0;this.links=new Map;this.funs=[];this.getAccTitle=Ir;this.setAccTitle=Lr;this.getAccDescription=Nr;this.setAccDescription=Mr;this.setDiagramTitle=Gr;this.getDiagramTitle=Pr;this.clear(),this.setRootDoc=this.setRootDoc.bind(this),this.getDividerId=this.getDividerId.bind(this),this.setDirection=this.setDirection.bind(this),this.trimColon=this.trimColon.bind(this),this.bindFunctions=this.bindFunctions.bind(this)}static{s(this,"StateDB")}static{this.relationType={AGGREGATION:0,EXTENSION:1,COMPOSITION:2,DEPENDENCY:3}}extract(t){this.clear(!0);for(let i of Array.isArray(t)?t:t.doc)switch(i.stmt){case Dp:this.addState(i.id.trim(),i.type,i.doc,i.description,i.note);break;case WS:this.addRelation(i.state1,i.state2,i.description);break;case wMe:this.addStyleClass(i.id.trim(),i.classes);break;case kMe:this.handleStyleDef(i);break;case EMe:this.setCssClass(i.id.trim(),i.styleClass);break;case"click":this.addLink(i.id,i.url,i.tooltip);break}let r=this.getStates(),n=Pe();GMe(),tx(void 0,this.getRootDocV2(),r,this.nodes,this.edges,!0,n.look,this.classes);for(let i of this.nodes)if(Array.isArray(i.label)){if(i.description=i.label.slice(1),i.isGroup&&i.description.length>0)throw new Error(`Group nodes can only have label. Remove the additional description for node [${i.id}]`);i.label=i.label[0]}}handleStyleDef(t){let r=t.id.trim().split(","),n=t.styleClass.split(",");for(let i of r){let a=this.getState(i);if(!a){let o=i.trim();this.addState(o),a=this.getState(o)}a&&(a.styles=n.map(o=>o.replace(/;/g,"")?.trim()))}}setRootDoc(t){se.info("Setting root doc",t),this.rootDoc=t,this.version===1?this.extract(t):this.extract(this.getRootDocV2())}docTranslator(t,r,n){if(r.stmt===WS){this.docTranslator(t,r.state1,!0),this.docTranslator(t,r.state2,!1);return}if(r.stmt===Dp&&(r.id===no.START_NODE?(r.id=t.id+(n?"_start":"_end"),r.start=n):r.id=r.id.trim()),r.stmt!==R0&&r.stmt!==Dp||!r.doc)return;let i=[],a=[];for(let o of r.doc)if(o.type===fL){let l=bL(o);l.doc=bL(a),i.push(l),a=[]}else a.push(o);if(i.length>0&&a.length>0){let o={stmt:Dp,id:YN(),type:"divider",doc:bL(a)};i.push(bL(o)),r.doc=i}r.doc.forEach(o=>this.docTranslator(r,o,!0))}getRootDocV2(){return this.docTranslator({id:R0,stmt:R0},{id:R0,stmt:R0,doc:this.rootDoc},!0),{id:R0,doc:this.rootDoc}}addState(t,r=_0,n=void 0,i=void 0,a=void 0,o=void 0,l=void 0,u=void 0){let h=t?.trim();if(!this.currentDocument.states.has(h))se.info("Adding state ",h,i),this.currentDocument.states.set(h,{stmt:Dp,id:h,descriptions:[],type:r,doc:n,note:a,classes:[],styles:[],textStyles:[]});else{let d=this.currentDocument.states.get(h);if(!d)throw new Error(`State not found: ${h}`);d.doc||(d.doc=n),d.type||(d.type=r)}if(i&&(se.info("Setting state description",h,i),(Array.isArray(i)?i:[i]).forEach(f=>this.addDescription(h,f.trim()))),a){let d=this.currentDocument.states.get(h);if(!d)throw new Error(`State not found: ${h}`);d.note=a,d.note.text=Tt.sanitizeText(d.note.text,Pe())}o&&(se.info("Setting state classes",h,o),(Array.isArray(o)?o:[o]).forEach(f=>this.setCssClass(h,f.trim()))),l&&(se.info("Setting state styles",h,l),(Array.isArray(l)?l:[l]).forEach(f=>this.setStyle(h,f.trim()))),u&&(se.info("Setting state styles",h,l),(Array.isArray(u)?u:[u]).forEach(f=>this.setTextStyle(h,f.trim())))}clear(t){this.nodes=[],this.edges=[],this.funs=[this.setupToolTips.bind(this)],this.documents={root:HMe()},this.currentDocument=this.documents.root,this.startEndCount=0,this.classes=qMe(),t||(this.links=new Map,Tr())}getState(t){return this.currentDocument.states.get(t)}getStates(){return this.currentDocument.states}logDocuments(){se.info("Documents = ",this.documents)}getRelations(){return this.currentDocument.relations}addLink(t,r,n){this.links.set(t,{url:r,tooltip:n}),se.warn("Adding link",t,r,n)}getLinks(){return this.links}startIdIfNeeded(t=""){return t===no.START_NODE?(this.startEndCount++,`${no.START_TYPE}${this.startEndCount}`):t}startTypeIfNeeded(t="",r=_0){return t===no.START_NODE?no.START_TYPE:r}endIdIfNeeded(t=""){return t===no.END_NODE?(this.startEndCount++,`${no.END_TYPE}${this.startEndCount}`):t}endTypeIfNeeded(t="",r=_0){return t===no.END_NODE?no.END_TYPE:r}addRelationObjs(t,r,n=""){let i=this.startIdIfNeeded(t.id.trim()),a=this.startTypeIfNeeded(t.id.trim(),t.type),o=this.startIdIfNeeded(r.id.trim()),l=this.startTypeIfNeeded(r.id.trim(),r.type);this.addState(i,a,t.doc,t.description,t.note,t.classes,t.styles,t.textStyles),this.addState(o,l,r.doc,r.description,r.note,r.classes,r.styles,r.textStyles),this.currentDocument.relations.push({id1:i,id2:o,relationTitle:Tt.sanitizeText(n,Pe())})}addRelation(t,r,n){if(typeof t=="object"&&typeof r=="object")this.addRelationObjs(t,r,n);else if(typeof t=="string"&&typeof r=="string"){let i=this.startIdIfNeeded(t.trim()),a=this.startTypeIfNeeded(t),o=this.endIdIfNeeded(r.trim()),l=this.endTypeIfNeeded(r);this.addState(i,a),this.addState(o,l),this.currentDocument.relations.push({id1:i,id2:o,relationTitle:n?Tt.sanitizeText(n,Pe()):void 0})}}addDescription(t,r){let n=this.currentDocument.states.get(t),i=r.startsWith(":")?r.replace(":","").trim():r;n?.descriptions?.push(Tt.sanitizeText(i,Pe()))}cleanupLabel(t){return t.startsWith(":")?t.slice(2).trim():t.trim()}getDividerId(){return this.dividerCnt++,`divider-id-${this.dividerCnt}`}addStyleClass(t,r=""){this.classes.has(t)||this.classes.set(t,{id:t,styles:[],textStyles:[]});let n=this.classes.get(t);r&&n&&r.split(no.STYLECLASS_SEP).forEach(i=>{let a=i.replace(/([^;]*);/,"$1").trim();if(RegExp(no.COLOR_KEYWORD).exec(i)){let l=a.replace(no.FILL_KEYWORD,no.BG_FILL).replace(no.COLOR_KEYWORD,no.FILL_KEYWORD);n.textStyles.push(l)}n.styles.push(a)})}getClasses(){return this.classes}setupToolTips(t){let r=_y();ut(t).select("svg").selectAll("g.node, g.rough-node").on("mouseover",a=>{let o=ut(a.currentTarget),l=o.attr("title");if(l===null)return;let u=a.currentTarget?.getBoundingClientRect();r.transition().duration(200).style("opacity",".9"),r.style("left",window.scrollX+u.left+(u.right-u.left)/2+"px").style("top",window.scrollY+u.bottom+"px"),r.html(ao.sanitize(l)),o.classed("hover",!0)}).on("mouseout",a=>{r.transition().duration(500).style("opacity",0),ut(a.currentTarget).classed("hover",!1)})}setCssClass(t,r){t.split(",").forEach(n=>{let i=this.getState(n);if(!i){let a=n.trim();this.addState(a),i=this.getState(a)}i?.classes?.push(r)})}setStyle(t,r){this.getState(t)?.styles?.push(r)}setTextStyle(t,r){this.getState(t)?.textStyles?.push(r)}bindFunctions(t){this.funs.forEach(r=>{r(t)})}getDirectionStatement(){return this.rootDoc.find(t=>t.stmt==="dir")}getDirection(){return this.getDirectionStatement()?.value??"TB"}setDirection(t){let r=this.getDirectionStatement();r?r.value=t:this.rootDoc.unshift({stmt:"dir",value:t})}trimColon(t){return t.startsWith(":")?t.slice(1).trim():t.trim()}getData(){let t=Pe();return{nodes:this.nodes,edges:this.edges,other:{},config:t,direction:OY(this.getRootDocV2())}}getConfig(){return Pe().state}}});var CRt,CL,$Y=F(()=>{"use strict";CRt=s(e=>` +defs [id$="-barbEnd"] { + fill: ${e.transitionColor}; + stroke: ${e.transitionColor}; + } +g.stateGroup text { + fill: ${e.nodeBorder}; + stroke: none; + font-size: 10px; +} +g.stateGroup text { + fill: ${e.textColor}; + stroke: none; + font-size: 10px; + +} +g.stateGroup .state-title { + font-weight: bolder; + fill: ${e.stateLabelColor}; +} + +g.stateGroup rect { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; +} + +g.stateGroup line { + stroke: ${e.lineColor}; + stroke-width: ${e.strokeWidth||1}; +} + +.transition { + stroke: ${e.transitionColor}; + stroke-width: ${e.strokeWidth||1}; + fill: none; +} + +.stateGroup .composit { + fill: ${e.background}; + border-bottom: 1px +} + +.stateGroup .alt-composit { + fill: #e0e0e0; + border-bottom: 1px +} + +.state-note { + stroke: ${e.noteBorderColor}; + fill: ${e.noteBkgColor}; + + text { + fill: ${e.noteTextColor}; + stroke: none; + font-size: 10px; + } +} + +.stateLabel .box { + stroke: none; + stroke-width: 0; + fill: ${e.mainBkg}; + opacity: 0.5; +} + +.edgeLabel .label rect { + fill: ${e.labelBackgroundColor}; + opacity: 0.5; +} +.edgeLabel { + background-color: ${e.edgeLabelBackground}; + p { + background-color: ${e.edgeLabelBackground}; + } + rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; +} +.edgeLabel .label text { + fill: ${e.transitionLabelColor||e.tertiaryTextColor}; +} +.label div .edgeLabel { + color: ${e.transitionLabelColor||e.tertiaryTextColor}; +} + +.stateLabel text { + fill: ${e.stateLabelColor}; + font-size: 10px; + font-weight: bold; +} + +.node circle.state-start { + fill: ${e.specialStateColor}; + stroke: ${e.specialStateColor}; +} + +.node .fork-join { + fill: ${e.specialStateColor}; + stroke: ${e.specialStateColor}; +} + +.node circle.state-end { + fill: ${e.innerEndBackground}; + stroke: ${e.background}; + stroke-width: 1.5 +} +.end-state-inner { + fill: ${e.compositeBackground||e.background}; + // stroke: ${e.background}; + stroke-width: 1.5 +} + +.node rect { + fill: ${e.stateBkg||e.mainBkg}; + stroke: ${e.stateBorder||e.nodeBorder}; + stroke-width: ${e.strokeWidth||1}px; +} +.node polygon { + fill: ${e.mainBkg}; + stroke: ${e.stateBorder||e.nodeBorder};; + stroke-width: ${e.strokeWidth||1}px; +} +[id$="-barbEnd"] { + fill: ${e.lineColor}; +} + +.statediagram-cluster rect { + fill: ${e.compositeTitleBackground}; + stroke: ${e.stateBorder||e.nodeBorder}; + stroke-width: ${e.strokeWidth||1}px; +} + +.cluster-label, .nodeLabel { + color: ${e.stateLabelColor}; + // line-height: 1; +} + +.statediagram-cluster rect.outer { + rx: 5px; + ry: 5px; +} +.statediagram-state .divider { + stroke: ${e.stateBorder||e.nodeBorder}; +} + +.statediagram-state .title-state { + rx: 5px; + ry: 5px; +} +.statediagram-cluster.statediagram-cluster .inner { + fill: ${e.compositeBackground||e.background}; +} +.statediagram-cluster.statediagram-cluster-alt .inner { + fill: ${e.altBackground?e.altBackground:"#efefef"}; +} + +.statediagram-cluster .inner { + rx:0; + ry:0; +} + +.statediagram-state rect.basic { + rx: 5px; + ry: 5px; +} +.statediagram-state rect.divider { + stroke-dasharray: 10,10; + fill: ${e.altBackground?e.altBackground:"#efefef"}; +} + +.note-edge { + stroke-dasharray: 5; +} + +.statediagram-note rect { + fill: ${e.noteBkgColor}; + stroke: ${e.noteBorderColor}; + stroke-width: 1px; + rx: 0; + ry: 0; +} +.statediagram-note rect { + fill: ${e.noteBkgColor}; + stroke: ${e.noteBorderColor}; + stroke-width: 1px; + rx: 0; + ry: 0; +} + +.statediagram-note text { + fill: ${e.noteTextColor}; +} + +.statediagram-note .nodeLabel { + color: ${e.noteTextColor}; +} +.statediagram .edgeLabel { + color: red; // ${e.noteTextColor}; +} + +[id$="-dependencyStart"], [id$="-dependencyEnd"] { + fill: ${e.lineColor}; + stroke: ${e.lineColor}; + stroke-width: ${e.strokeWidth||1}; +} + +.statediagramTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; +} + +[data-look="neo"].statediagram-cluster rect { + fill: ${e.mainBkg}; + stroke: ${e.useGradient?"url("+e.svgId+"-gradient)":e.stateBorder||e.nodeBorder}; + stroke-width: ${e.strokeWidth??1}; +} +[data-look="neo"].statediagram-cluster rect.outer { + rx: ${e.radius}px; + ry: ${e.radius}px; + filter: ${e.dropShadow?e.dropShadow.replace("url(#drop-shadow)",`url(${e.svgId}-drop-shadow)`):"none"} +} +`,"getStyles"),CL=CRt});var SRt,wRt,kRt,ERt,YMe,ARt,RRt,_Rt,LRt,FY,UMe,jMe,XMe=F(()=>{"use strict";Wr();TL();er();Yr();Jt();St();SRt=s(e=>e.append("circle").attr("class","start-state").attr("r",Pe().state.sizeUnit).attr("cx",Pe().state.padding+Pe().state.sizeUnit).attr("cy",Pe().state.padding+Pe().state.sizeUnit),"drawStartState"),wRt=s(e=>e.append("line").style("stroke","grey").style("stroke-dasharray","3").attr("x1",Pe().state.textHeight).attr("class","divider").attr("x2",Pe().state.textHeight*2).attr("y1",0).attr("y2",0),"drawDivider"),kRt=s((e,t)=>{let r=e.append("text").attr("x",2*Pe().state.padding).attr("y",Pe().state.textHeight+2*Pe().state.padding).attr("font-size",Pe().state.fontSize).attr("class","state-title").text(t.id),n=r.node().getBBox();return e.insert("rect",":first-child").attr("x",Pe().state.padding).attr("y",Pe().state.padding).attr("width",n.width+2*Pe().state.padding).attr("height",n.height+2*Pe().state.padding).attr("rx",Pe().state.radius),r},"drawSimpleState"),ERt=s((e,t)=>{let r=s(function(p,m,g){let y=p.append("tspan").attr("x",2*Pe().state.padding).text(m);g||y.attr("dy",Pe().state.textHeight)},"addTspan"),i=e.append("text").attr("x",2*Pe().state.padding).attr("y",Pe().state.textHeight+1.3*Pe().state.padding).attr("font-size",Pe().state.fontSize).attr("class","state-title").text(t.descriptions[0]).node().getBBox(),a=i.height,o=e.append("text").attr("x",Pe().state.padding).attr("y",a+Pe().state.padding*.4+Pe().state.dividerMargin+Pe().state.textHeight).attr("class","state-description"),l=!0,u=!0;t.descriptions.forEach(function(p){l||(r(o,p,u),u=!1),l=!1});let h=e.append("line").attr("x1",Pe().state.padding).attr("y1",Pe().state.padding+a+Pe().state.dividerMargin/2).attr("y2",Pe().state.padding+a+Pe().state.dividerMargin/2).attr("class","descr-divider"),d=o.node().getBBox(),f=Math.max(d.width,i.width);return h.attr("x2",f+3*Pe().state.padding),e.insert("rect",":first-child").attr("x",Pe().state.padding).attr("y",Pe().state.padding).attr("width",f+2*Pe().state.padding).attr("height",d.height+a+2*Pe().state.padding).attr("rx",Pe().state.radius),e},"drawDescrState"),YMe=s((e,t,r)=>{let n=Pe().state.padding,i=2*Pe().state.padding,a=e.node().getBBox(),o=a.width,l=a.x,u=e.append("text").attr("x",0).attr("y",Pe().state.titleShift).attr("font-size",Pe().state.fontSize).attr("class","state-title").text(t.id),d=u.node().getBBox().width+i,f=Math.max(d,o);f===o&&(f=f+i);let p,m=e.node().getBBox();t.doc,p=l-n,d>o&&(p=(o-f)/2+n),Math.abs(l-m.x)o&&(p=l-(d-o)/2);let g=1-Pe().state.textHeight;return e.insert("rect",":first-child").attr("x",p).attr("y",g).attr("class",r?"alt-composit":"composit").attr("width",f).attr("height",m.height+Pe().state.textHeight+Pe().state.titleShift+1).attr("rx","0"),u.attr("x",p+n),d<=o&&u.attr("x",l+(f-i)/2-d/2+n),e.insert("rect",":first-child").attr("x",p).attr("y",Pe().state.titleShift-Pe().state.textHeight-Pe().state.padding).attr("width",f).attr("height",Pe().state.textHeight*3).attr("rx",Pe().state.radius),e.insert("rect",":first-child").attr("x",p).attr("y",Pe().state.titleShift-Pe().state.textHeight-Pe().state.padding).attr("width",f).attr("height",m.height+3+2*Pe().state.textHeight).attr("rx",Pe().state.radius),e},"addTitleAndBox"),ARt=s(e=>(e.append("circle").attr("class","end-state-outer").attr("r",Pe().state.sizeUnit+Pe().state.miniPadding).attr("cx",Pe().state.padding+Pe().state.sizeUnit+Pe().state.miniPadding).attr("cy",Pe().state.padding+Pe().state.sizeUnit+Pe().state.miniPadding),e.append("circle").attr("class","end-state-inner").attr("r",Pe().state.sizeUnit).attr("cx",Pe().state.padding+Pe().state.sizeUnit+2).attr("cy",Pe().state.padding+Pe().state.sizeUnit+2)),"drawEndState"),RRt=s((e,t)=>{let r=Pe().state.forkWidth,n=Pe().state.forkHeight;if(t.parentId){let i=r;r=n,n=i}return e.append("rect").style("stroke","black").style("fill","black").attr("width",r).attr("height",n).attr("x",Pe().state.padding).attr("y",Pe().state.padding)},"drawForkJoinState"),_Rt=s((e,t,r,n)=>{let i=0,a=n.append("text");a.style("text-anchor","start"),a.attr("class","noteText");let o=e.replace(/\r\n/g,"
    ");o=o.replace(/\n/g,"
    ");let l=o.split(Tt.lineBreakRegex),u=1.25*Pe().state.noteMargin;for(let h of l){let d=h.trim();if(d.length>0){let f=a.append("tspan");if(f.text(d),u===0){let p=f.node().getBBox();u+=p.height}i+=u,f.attr("x",t+Pe().state.noteMargin),f.attr("y",r+i+1.25*Pe().state.noteMargin)}}return{textWidth:a.node().getBBox().width,textHeight:i}},"_drawLongText"),LRt=s((e,t)=>{t.attr("class","state-note");let r=t.append("rect").attr("x",0).attr("y",Pe().state.padding),n=t.append("g"),{textWidth:i,textHeight:a}=_Rt(e,0,0,n);return r.attr("height",a+2*Pe().state.noteMargin),r.attr("width",i+Pe().state.noteMargin*2),r},"drawNote"),FY=s(function(e,t){let r=t.id,n={id:r,label:t.id,width:0,height:0},i=e.append("g").attr("id",r).attr("class","stateGroup");t.type==="start"&&SRt(i),t.type==="end"&&ARt(i),(t.type==="fork"||t.type==="join")&&RRt(i,t),t.type==="note"&&LRt(t.note.text,i),t.type==="divider"&&wRt(i),t.type==="default"&&t.descriptions.length===0&&kRt(i,t),t.type==="default"&&t.descriptions.length>0&&ERt(i,t);let a=i.node().getBBox();return n.width=a.width+2*Pe().state.padding,n.height=a.height+2*Pe().state.padding,n},"drawState"),UMe=0,jMe=s(function(e,t,r){let n=s(function(u){switch(u){case $l.relationType.AGGREGATION:return"aggregation";case $l.relationType.EXTENSION:return"extension";case $l.relationType.COMPOSITION:return"composition";case $l.relationType.DEPENDENCY:return"dependency"}},"getRelationType");t.points=t.points.filter(u=>!Number.isNaN(u.y));let i=t.points,a=xh().x(function(u){return u.x}).y(function(u){return u.y}).curve(bh),o=e.append("path").attr("d",a(i)).attr("id","edge"+UMe).attr("class","transition"),l="";if(Pe().state.arrowMarkerAbsolute&&(l=eb(!0)),o.attr("marker-end","url("+l+"#"+n($l.relationType.DEPENDENCY)+"End)"),r.title!==void 0){let u=e.append("g").attr("class","stateLabel"),{x:h,y:d}=or.calcLabelPosition(t.points),f=Tt.getRows(r.title),p=0,m=[],g=0,y=0;for(let b=0;b<=f.length;b++){let T=u.append("text").attr("text-anchor","middle").text(f[b]).attr("x",h).attr("y",d+p),w=T.node().getBBox();g=Math.max(g,w.width),y=Math.min(y,w.x),se.info(w.x,h,d+p),p===0&&(p=T.node().getBBox().height,se.info("Title height",p,d)),m.push(T)}let v=p*f.length;if(f.length>1){let b=(f.length-1)*p*.5;m.forEach((T,w)=>T.attr("y",d+w*p-b)),v=p*f.length}let x=u.node().getBBox();u.insert("rect",":first-child").attr("class","box").attr("x",h-g/2-Pe().state.padding/2).attr("y",d-v/2-Pe().state.padding/2-3.5).attr("width",g+Pe().state.padding).attr("height",v+Pe().state.padding),se.info(x)}UMe++},"drawEdge")});var ol,GY,DRt,IRt,MRt,NRt,KMe,ZMe,QMe=F(()=>{"use strict";Wr();H9();Ko();St();Yr();XMe();Jt();Vn();GY={},DRt=s(function(){},"setConf"),IRt=s(function(e){e.append("defs").append("marker").attr("id","dependencyEnd").attr("refX",19).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 19,7 L9,13 L14,7 L9,1 Z")},"insertMarkers"),MRt=s(function(e,t,r,n){ol=Pe().state;let i=Pe().securityLevel,a;i==="sandbox"&&(a=ut("#i"+t));let o=i==="sandbox"?ut(a.nodes()[0].contentDocument.body):ut("body"),l=i==="sandbox"?a.nodes()[0].contentDocument:document;se.debug("Rendering diagram "+e);let u=o.select(`[id='${t}']`);IRt(u);let h=n.db.getRootDoc(),d=u.append("g").attr("id",t+"-root");KMe(h,d,void 0,!1,o,l,n);let f=ol.padding,p=u.node().getBBox(),m=p.width+f*2,g=p.height+f*2,y=m*1.75;jr(u,g,y,ol.useMaxWidth),u.attr("viewBox",`${p.x-ol.padding} ${p.y-ol.padding} `+m+" "+g)},"draw"),NRt=s(e=>e?e.length*ol.fontSizeFactor:1,"getLabelWidth"),KMe=s((e,t,r,n,i,a,o)=>{let l=new Ln({compound:!0,multigraph:!0}),u,h=!0;for(u=0;u{let C=w.parentElement,S=0,k=0;C&&(C.parentElement&&(S=C.parentElement.getBBox().width),k=parseInt(C.getAttribute("data-x-shift"),10),Number.isNaN(k)&&(k=0)),w.setAttribute("x1",0-k+8),w.setAttribute("x2",S-k-8)})):se.debug("No Node "+b+": "+JSON.stringify(l.node(b)))});let v=y.getBBox();l.edges().forEach(function(b){b!==void 0&&l.edge(b)!==void 0&&(se.debug("Edge "+b.v+" -> "+b.w+": "+JSON.stringify(l.edge(b))),jMe(t,l.edge(b),l.edge(b).relation))}),v=y.getBBox();let x={id:r||"root",label:r||"root",width:0,height:0};return x.width=v.width+2*ol.padding,x.height=v.height+2*ol.padding,se.debug("Doc rendered",x,l),x},"renderDoc"),ZMe={setConf:DRt,draw:MRt}});var JMe={};sr(JMe,{diagram:()=>PRt});var PRt,eNe=F(()=>{"use strict";SY();TL();$Y();QMe();PRt={parser:dL,get db(){return new $l(1)},renderer:ZMe,styles:CL,init:s(e=>{e.state||(e.state={}),e.state.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")}});var nNe={};sr(nNe,{diagram:()=>FRt});var FRt,iNe=F(()=>{"use strict";SY();TL();$Y();BY();FRt={parser:dL,get db(){return new $l(2)},renderer:VMe,styles:CL,init:s(e=>{e.state||(e.state={}),e.state.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")}});var zY,oNe,lNe=F(()=>{"use strict";zY=(function(){var e=s(function(f,p,m,g){for(m=m||{},g=f.length;g--;m[f[g]]=p);return m},"o"),t=[6,8,10,11,12,14,16,17,18],r=[1,9],n=[1,10],i=[1,11],a=[1,12],o=[1,13],l=[1,14],u={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,taskName:18,taskData:19,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",18:"taskName",19:"taskData"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,2]],performAction:s(function(p,m,g,y,v,x,b){var T=x.length-1;switch(v){case 1:return x[T-1];case 2:this.$=[];break;case 3:x[T-1].push(x[T]),this.$=x[T-1];break;case 4:case 5:this.$=x[T];break;case 6:case 7:this.$=[];break;case 8:y.setDiagramTitle(x[T].substr(6)),this.$=x[T].substr(6);break;case 9:this.$=x[T].trim(),y.setAccTitle(this.$);break;case 10:case 11:this.$=x[T].trim(),y.setAccDescription(this.$);break;case 12:y.addSection(x[T].substr(8)),this.$=x[T].substr(8);break;case 13:y.addTask(x[T-1],x[T]),this.$="task";break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},e(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:r,12:n,14:i,16:a,17:o,18:l},e(t,[2,7],{1:[2,1]}),e(t,[2,3]),{9:15,11:r,12:n,14:i,16:a,17:o,18:l},e(t,[2,5]),e(t,[2,6]),e(t,[2,8]),{13:[1,16]},{15:[1,17]},e(t,[2,11]),e(t,[2,12]),{19:[1,18]},e(t,[2,4]),e(t,[2,9]),e(t,[2,10]),e(t,[2,13])],defaultActions:{},parseError:s(function(p,m){if(m.recoverable)this.trace(p);else{var g=new Error(p);throw g.hash=m,g}},"parseError"),parse:s(function(p){var m=this,g=[0],y=[],v=[null],x=[],b=this.table,T="",w=0,C=0,S=0,k=2,R=1,M=x.slice.call(arguments,1),P=Object.create(this.lexer),N={yy:{}};for(var _ in this.yy)Object.prototype.hasOwnProperty.call(this.yy,_)&&(N.yy[_]=this.yy[_]);P.setInput(p,N.yy),N.yy.lexer=P,N.yy.parser=this,typeof P.yylloc>"u"&&(P.yylloc={});var A=P.yylloc;x.push(A);var I=P.options&&P.options.ranges;typeof N.yy.parseError=="function"?this.parseError=N.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function D(te){g.length=g.length-2*te,v.length=v.length-te,x.length=x.length-te}s(D,"popStack");function L(){var te;return te=y.pop()||P.lex()||R,typeof te!="number"&&(te instanceof Array&&(y=te,te=y.pop()),te=m.symbols_[te]||te),te}s(L,"lex");for(var B,O,$,W,V,z,H={},K,J,ne,Z;;){if($=g[g.length-1],this.defaultActions[$]?W=this.defaultActions[$]:((B===null||typeof B>"u")&&(B=L()),W=b[$]&&b[$][B]),typeof W>"u"||!W.length||!W[0]){var pe="";Z=[];for(K in b[$])this.terminals_[K]&&K>k&&Z.push("'"+this.terminals_[K]+"'");P.showPosition?pe="Parse error on line "+(w+1)+`: +`+P.showPosition()+` +Expecting `+Z.join(", ")+", got '"+(this.terminals_[B]||B)+"'":pe="Parse error on line "+(w+1)+": Unexpected "+(B==R?"end of input":"'"+(this.terminals_[B]||B)+"'"),this.parseError(pe,{text:P.match,token:this.terminals_[B]||B,line:P.yylineno,loc:A,expected:Z})}if(W[0]instanceof Array&&W.length>1)throw new Error("Parse Error: multiple actions possible at state: "+$+", token: "+B);switch(W[0]){case 1:g.push(B),v.push(P.yytext),x.push(P.yylloc),g.push(W[1]),B=null,O?(B=O,O=null):(C=P.yyleng,T=P.yytext,w=P.yylineno,A=P.yylloc,S>0&&S--);break;case 2:if(J=this.productions_[W[1]][1],H.$=v[v.length-J],H._$={first_line:x[x.length-(J||1)].first_line,last_line:x[x.length-1].last_line,first_column:x[x.length-(J||1)].first_column,last_column:x[x.length-1].last_column},I&&(H._$.range=[x[x.length-(J||1)].range[0],x[x.length-1].range[1]]),z=this.performAction.apply(H,[T,C,w,N.yy,W[1],v,x].concat(M)),typeof z<"u")return z;J&&(g=g.slice(0,-1*J*2),v=v.slice(0,-1*J),x=x.slice(0,-1*J)),g.push(this.productions_[W[1]][0]),v.push(H.$),x.push(H._$),ne=b[g[g.length-2]][g[g.length-1]],g.push(ne);break;case 3:return!0}}return!0},"parse")},h=(function(){var f={EOF:1,parseError:s(function(m,g){if(this.yy.parser)this.yy.parser.parseError(m,g);else throw new Error(m)},"parseError"),setInput:s(function(p,m){return this.yy=m||this.yy||{},this._input=p,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var p=this._input[0];this.yytext+=p,this.yyleng++,this.offset++,this.match+=p,this.matched+=p;var m=p.match(/(?:\r\n?|\n).*/g);return m?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),p},"input"),unput:s(function(p){var m=p.length,g=p.split(/(?:\r\n?|\n)/g);this._input=p+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-m),this.offset-=m;var y=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),g.length-1&&(this.yylineno-=g.length-1);var v=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:g?(g.length===y.length?this.yylloc.first_column:0)+y[y.length-g.length].length-g[0].length:this.yylloc.first_column-m},this.options.ranges&&(this.yylloc.range=[v[0],v[0]+this.yyleng-m]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(p){this.unput(this.match.slice(p))},"less"),pastInput:s(function(){var p=this.matched.substr(0,this.matched.length-this.match.length);return(p.length>20?"...":"")+p.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var p=this.match;return p.length<20&&(p+=this._input.substr(0,20-p.length)),(p.substr(0,20)+(p.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var p=this.pastInput(),m=new Array(p.length+1).join("-");return p+this.upcomingInput()+` +`+m+"^"},"showPosition"),test_match:s(function(p,m){var g,y,v;if(this.options.backtrack_lexer&&(v={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(v.yylloc.range=this.yylloc.range.slice(0))),y=p[0].match(/(?:\r\n?|\n).*/g),y&&(this.yylineno+=y.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:y?y[y.length-1].length-y[y.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+p[0].length},this.yytext+=p[0],this.match+=p[0],this.matches=p,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(p[0].length),this.matched+=p[0],g=this.performAction.call(this,this.yy,this,m,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),g)return g;if(this._backtrack){for(var x in v)this[x]=v[x];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var p,m,g,y;this._more||(this.yytext="",this.match="");for(var v=this._currentRules(),x=0;xm[0].length)){if(m=g,y=x,this.options.backtrack_lexer){if(p=this.test_match(g,v[x]),p!==!1)return p;if(this._backtrack){m=!1;continue}else return!1}else if(!this.options.flex)break}return m?(p=this.test_match(m,v[y]),p!==!1?p:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var m=this.next();return m||this.lex()},"lex"),begin:s(function(m){this.conditionStack.push(m)},"begin"),popState:s(function(){var m=this.conditionStack.length-1;return m>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(m){return m=this.conditionStack.length-1-Math.abs(m||0),m>=0?this.conditionStack[m]:"INITIAL"},"topState"),pushState:s(function(m){this.begin(m)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(m,g,y,v){var x=v;switch(y){case 0:break;case 1:break;case 2:return 10;case 3:break;case 4:break;case 5:return 4;case 6:return 11;case 7:return this.begin("acc_title"),12;break;case 8:return this.popState(),"acc_title_value";break;case 9:return this.begin("acc_descr"),14;break;case 10:return this.popState(),"acc_descr_value";break;case 11:this.begin("acc_descr_multiline");break;case 12:this.popState();break;case 13:return"acc_descr_multiline_value";case 14:return 17;case 15:return 18;case 16:return 19;case 17:return":";case 18:return 6;case 19:return"INVALID"}},"anonymous"),rules:[/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:journey\b)/i,/^(?:title\s[^#\n;]+)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:section\s[^#:\n;]+)/i,/^(?:[^#:\n;]+)/i,/^(?::[^#\n;]+)/i,/^(?::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{acc_descr_multiline:{rules:[12,13],inclusive:!1},acc_descr:{rules:[10],inclusive:!1},acc_title:{rules:[8],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,9,11,14,15,16,17,18,19],inclusive:!0}}};return f})();u.lexer=h;function d(){this.yy={}}return s(d,"Parser"),d.prototype=u,u.Parser=d,new d})();zY.parser=zY;oNe=zY});var rx,VY,qS,HS,WRt,qRt,HRt,URt,YRt,jRt,XRt,cNe,KRt,WY,uNe=F(()=>{"use strict";Jt();$n();rx="",VY=[],qS=[],HS=[],WRt=s(function(){VY.length=0,qS.length=0,rx="",HS.length=0,Tr()},"clear"),qRt=s(function(e){rx=e,VY.push(e)},"addSection"),HRt=s(function(){return VY},"getSections"),URt=s(function(){let e=cNe(),t=100,r=0;for(;!e&&r{r.people&&e.push(...r.people)}),[...new Set(e)].sort()},"updateActors"),jRt=s(function(e,t){let r=t.substr(1).split(":"),n=0,i=[];r.length===1?(n=Number(r[0]),i=[]):(n=Number(r[0]),i=r[1].split(","));let a=i.map(l=>l.trim()),o={section:rx,type:rx,people:a,task:e,score:n};HS.push(o)},"addTask"),XRt=s(function(e){let t={section:rx,type:rx,description:e,task:e,classes:[]};qS.push(t)},"addTaskOrg"),cNe=s(function(){let e=s(function(r){return HS[r].processed},"compileTask"),t=!0;for(let[r,n]of HS.entries())e(r),t=t&&n.processed;return t},"compileTasks"),KRt=s(function(){return YRt()},"getActors"),WY={getConfig:s(()=>Pe().journey,"getConfig"),clear:WRt,setDiagramTitle:Gr,getDiagramTitle:Pr,setAccTitle:Lr,getAccTitle:Ir,setAccDescription:Mr,getAccDescription:Nr,addSection:qRt,getSections:HRt,getTasks:URt,addTask:jRt,addTaskOrg:XRt,getActors:KRt}});var ZRt,hNe,dNe=F(()=>{"use strict";U1();ZRt=s(e=>`.label { + font-family: ${e.fontFamily}; + color: ${e.textColor}; + } + .mouth { + stroke: #666; + } + + line { + stroke: ${e.textColor} + } + + .legend { + fill: ${e.textColor}; + font-family: ${e.fontFamily}; + } + + .label text { + fill: #333; + } + .label { + color: ${e.textColor} + } + + .face { + ${e.faceColor?`fill: ${e.faceColor}`:"fill: #FFF8DC"}; + stroke: #999; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: 1px; + } + + .node .label { + text-align: center; + } + .node.clickable { + cursor: pointer; + } + + .arrowheadPath { + fill: ${e.arrowheadColor}; + } + + .edgePaths .path { + stroke: ${e.lineColor}; + stroke-width: 1.5px; + } + + .flowchart-link { + stroke: ${e.lineColor}; + fill: none; + } + + .edgeLabel { + background-color: ${e.edgeLabelBackground}; + rect { + opacity: 0.5; + } + text-align: center; + } + + .cluster rect { + } + + .cluster text { + fill: ${e.titleColor}; + } + + div.mermaidTooltip { + position: absolute; + text-align: center; + max-width: 200px; + padding: 2px; + font-family: ${e.fontFamily}; + font-size: 12px; + background: ${e.tertiaryColor}; + border: 1px solid ${e.border2}; + border-radius: 2px; + pointer-events: none; + z-index: 100; + } + + .task-type-0, .section-type-0 { + ${e.fillType0?`fill: ${e.fillType0}`:""}; + } + .task-type-1, .section-type-1 { + ${e.fillType0?`fill: ${e.fillType1}`:""}; + } + .task-type-2, .section-type-2 { + ${e.fillType0?`fill: ${e.fillType2}`:""}; + } + .task-type-3, .section-type-3 { + ${e.fillType0?`fill: ${e.fillType3}`:""}; + } + .task-type-4, .section-type-4 { + ${e.fillType0?`fill: ${e.fillType4}`:""}; + } + .task-type-5, .section-type-5 { + ${e.fillType0?`fill: ${e.fillType5}`:""}; + } + .task-type-6, .section-type-6 { + ${e.fillType0?`fill: ${e.fillType6}`:""}; + } + .task-type-7, .section-type-7 { + ${e.fillType0?`fill: ${e.fillType7}`:""}; + } + + .actor-0 { + ${e.actor0?`fill: ${e.actor0}`:""}; + } + .actor-1 { + ${e.actor1?`fill: ${e.actor1}`:""}; + } + .actor-2 { + ${e.actor2?`fill: ${e.actor2}`:""}; + } + .actor-3 { + ${e.actor3?`fill: ${e.actor3}`:""}; + } + .actor-4 { + ${e.actor4?`fill: ${e.actor4}`:""}; + } + .actor-5 { + ${e.actor5?`fill: ${e.actor5}`:""}; + } + ${Ru()} +`,"getStyles"),hNe=ZRt});var HY,QRt,fNe,pNe,JRt,e6t,qY,t6t,r6t,mNe,n6t,nx,gNe=F(()=>{"use strict";Wr();Xd();HY=s(function(e,t){return gm(e,t)},"drawRect"),QRt=s(function(e,t){let n=e.append("circle").attr("cx",t.cx).attr("cy",t.cy).attr("class","face").attr("r",15).attr("stroke-width",2).attr("overflow","visible"),i=e.append("g");i.append("circle").attr("cx",t.cx-15/3).attr("cy",t.cy-15/3).attr("r",1.5).attr("stroke-width",2).attr("fill","#666").attr("stroke","#666"),i.append("circle").attr("cx",t.cx+15/3).attr("cy",t.cy-15/3).attr("r",1.5).attr("stroke-width",2).attr("fill","#666").attr("stroke","#666");function a(u){let h=nc().startAngle(Math.PI/2).endAngle(3*(Math.PI/2)).innerRadius(7.5).outerRadius(6.8181818181818175);u.append("path").attr("class","mouth").attr("d",h).attr("transform","translate("+t.cx+","+(t.cy+2)+")")}s(a,"smile");function o(u){let h=nc().startAngle(3*Math.PI/2).endAngle(5*(Math.PI/2)).innerRadius(7.5).outerRadius(6.8181818181818175);u.append("path").attr("class","mouth").attr("d",h).attr("transform","translate("+t.cx+","+(t.cy+7)+")")}s(o,"sad");function l(u){u.append("line").attr("class","mouth").attr("stroke",2).attr("x1",t.cx-5).attr("y1",t.cy+7).attr("x2",t.cx+5).attr("y2",t.cy+7).attr("class","mouth").attr("stroke-width","1px").attr("stroke","#666")}return s(l,"ambivalent"),t.score>3?a(i):t.score<3?o(i):l(i),n},"drawFace"),fNe=s(function(e,t){let r=e.append("circle");return r.attr("cx",t.cx),r.attr("cy",t.cy),r.attr("class","actor-"+t.pos),r.attr("fill",t.fill),r.attr("stroke",t.stroke),r.attr("r",t.r),r.class!==void 0&&r.attr("class",r.class),t.title!==void 0&&r.append("title").text(t.title),r},"drawCircle"),pNe=s(function(e,t){return Pie(e,t)},"drawText"),JRt=s(function(e,t){function r(i,a,o,l,u){return i+","+a+" "+(i+o)+","+a+" "+(i+o)+","+(a+l-u)+" "+(i+o-u*1.2)+","+(a+l)+" "+i+","+(a+l)}s(r,"genPoints");let n=e.append("polygon");n.attr("points",r(t.x,t.y,50,20,7)),n.attr("class","labelBox"),t.y=t.y+t.labelMargin,t.x=t.x+.5*t.labelMargin,pNe(e,t)},"drawLabel"),e6t=s(function(e,t,r){let n=e.append("g"),i=Os();i.x=t.x,i.y=t.y,i.fill=t.fill,i.width=r.width*t.taskCount+r.diagramMarginX*(t.taskCount-1),i.height=r.height,i.class="journey-section section-type-"+t.num,i.rx=3,i.ry=3,HY(n,i),mNe(r)(t.text,n,i.x,i.y,i.width,i.height,{class:"journey-section section-type-"+t.num},r,t.colour)},"drawSection"),qY=-1,t6t=s(function(e,t,r,n){let i=t.x+r.width/2,a=e.append("g");qY++,a.append("line").attr("id",n+"-task"+qY).attr("x1",i).attr("y1",t.y).attr("x2",i).attr("y2",450).attr("class","task-line").attr("stroke-width","1px").attr("stroke-dasharray","4 2").attr("stroke","#666"),QRt(a,{cx:i,cy:300+(5-t.score)*30,score:t.score});let l=Os();l.x=t.x,l.y=t.y,l.fill=t.fill,l.width=r.width,l.height=r.height,l.class="task task-type-"+t.num,l.rx=3,l.ry=3,HY(a,l);let u=t.x+14;t.people.forEach(h=>{let d=t.actors[h].color,f={cx:u,cy:t.y,r:7,fill:d,stroke:"#000",title:h,pos:t.actors[h].position};fNe(a,f),u+=10}),mNe(r)(t.task,a,l.x,l.y,l.width,l.height,{class:"task"},r,t.colour)},"drawTask"),r6t=s(function(e,t){ME(e,t)},"drawBackgroundRect"),mNe=(function(){function e(i,a,o,l,u,h,d,f){let p=a.append("text").attr("x",o+u/2).attr("y",l+h/2+5).style("font-color",f).style("text-anchor","middle").text(i);n(p,d)}s(e,"byText");function t(i,a,o,l,u,h,d,f,p){let{taskFontSize:m,taskFontFamily:g}=f,y=i.split(//gi);for(let v=0;v{let a=pd[i].color,o={cx:20,cy:n,r:7,fill:a,stroke:"#000",pos:pd[i].position};nx.drawCircle(e,o);let l=e.append("text").attr("visibility","hidden").text(i),u=l.node().getBoundingClientRect().width;l.remove();let h=[];if(u<=r)h=[i];else{let d=i.split(" "),f="";l=e.append("text").attr("visibility","hidden"),d.forEach(p=>{let m=f?`${f} ${p}`:p;if(l.text(m),l.node().getBoundingClientRect().width>r){if(f&&h.push(f),f=p,l.text(p),l.node().getBoundingClientRect().width>r){let y="";for(let v of p)y+=v,l.text(y+"-"),l.node().getBoundingClientRect().width>r&&(h.push(y.slice(0,-1)+"-"),y=v);f=y}}else f=m}),f&&h.push(f),l.remove()}h.forEach((d,f)=>{let p={x:40,y:n+7+f*20,fill:"#666",text:d,textMargin:t.boxTextMargin??5},g=nx.drawText(e,p).node().getBoundingClientRect().width;g>SL&&g>t.leftMargin-g&&(SL=g)}),n+=Math.max(20,h.length*20)})}var i6t,pd,SL,_c,Mp,s6t,Fl,UY,yNe,o6t,YY,vNe=F(()=>{"use strict";Wr();gNe();Jt();Vn();i6t=s(function(e){Object.keys(e).forEach(function(r){_c[r]=e[r]})},"setConf"),pd={},SL=0;s(a6t,"drawActorLegend");_c=Pe().journey,Mp=0,s6t=s(function(e,t,r,n){let i=Pe(),a=i.journey.titleColor,o=i.journey.titleFontSize,l=i.journey.titleFontFamily,u=i.securityLevel,h;u==="sandbox"&&(h=ut("#i"+t));let d=u==="sandbox"?ut(h.nodes()[0].contentDocument.body):ut("body");Fl.init();let f=d.select("#"+t);nx.initGraphics(f,t);let p=n.db.getTasks(),m=n.db.getDiagramTitle(),g=n.db.getActors();for(let w in pd)delete pd[w];let y=0;g.forEach(w=>{pd[w]={color:_c.actorColours[y%_c.actorColours.length],position:y},y++}),a6t(f),Mp=_c.leftMargin+SL,Fl.insert(0,0,Mp,Object.keys(pd).length*50),o6t(f,p,0,t);let v=Fl.getBounds();m&&f.append("text").text(m).attr("x",Mp).attr("font-size",o).attr("font-weight","bold").attr("y",25).attr("fill",a).attr("font-family",l);let x=v.stopy-v.starty+2*_c.diagramMarginY,b=Mp+v.stopx+2*_c.diagramMarginX;jr(f,x,b,_c.useMaxWidth),f.append("line").attr("x1",Mp).attr("y1",_c.height*4).attr("x2",b-Mp-4).attr("y2",_c.height*4).attr("stroke-width",4).attr("stroke","black").attr("marker-end","url(#"+t+"-arrowhead)");let T=m?70:0;f.attr("viewBox",`${v.startx} -25 ${b} ${x+T}`),f.attr("preserveAspectRatio","xMinYMin meet"),f.attr("height",x+T+25)},"draw"),Fl={data:{startx:void 0,stopx:void 0,starty:void 0,stopy:void 0},verticalPos:0,sequenceItems:[],init:s(function(){this.sequenceItems=[],this.data={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0},this.verticalPos=0},"init"),updateVal:s(function(e,t,r,n){e[t]===void 0?e[t]=r:e[t]=n(r,e[t])},"updateVal"),updateBounds:s(function(e,t,r,n){let i=Pe().journey,a=this,o=0;function l(u){return s(function(d){o++;let f=a.sequenceItems.length-o+1;a.updateVal(d,"starty",t-f*i.boxMargin,Math.min),a.updateVal(d,"stopy",n+f*i.boxMargin,Math.max),a.updateVal(Fl.data,"startx",e-f*i.boxMargin,Math.min),a.updateVal(Fl.data,"stopx",r+f*i.boxMargin,Math.max),u!=="activation"&&(a.updateVal(d,"startx",e-f*i.boxMargin,Math.min),a.updateVal(d,"stopx",r+f*i.boxMargin,Math.max),a.updateVal(Fl.data,"starty",t-f*i.boxMargin,Math.min),a.updateVal(Fl.data,"stopy",n+f*i.boxMargin,Math.max))},"updateItemBounds")}s(l,"updateFn"),this.sequenceItems.forEach(l())},"updateBounds"),insert:s(function(e,t,r,n){let i=Math.min(e,r),a=Math.max(e,r),o=Math.min(t,n),l=Math.max(t,n);this.updateVal(Fl.data,"startx",i,Math.min),this.updateVal(Fl.data,"starty",o,Math.min),this.updateVal(Fl.data,"stopx",a,Math.max),this.updateVal(Fl.data,"stopy",l,Math.max),this.updateBounds(i,o,a,l)},"insert"),bumpVerticalPos:s(function(e){this.verticalPos=this.verticalPos+e,this.data.stopy=this.verticalPos},"bumpVerticalPos"),getVerticalPos:s(function(){return this.verticalPos},"getVerticalPos"),getBounds:s(function(){return this.data},"getBounds")},UY=_c.sectionFills,yNe=_c.sectionColours,o6t=s(function(e,t,r,n){let i=Pe().journey,a="",o=i.height*2+i.diagramMarginY,l=r+o,u=0,h="#CCC",d="black",f=0;for(let[p,m]of t.entries()){if(a!==m.section){h=UY[u%UY.length],f=u%UY.length,d=yNe[u%yNe.length];let y=0,v=m.section;for(let b=p;b(pd[v]&&(y[v]=pd[v]),y),{});m.x=p*i.taskMargin+p*i.width+Mp,m.y=l,m.width=i.diagramMarginX,m.height=i.diagramMarginY,m.colour=d,m.fill=h,m.num=f,m.actors=g,nx.drawTask(e,m,i,n),Fl.insert(m.x,m.y,m.x+m.width+i.taskMargin,450)}},"drawTasks"),YY={setConf:i6t,draw:s6t}});var xNe={};sr(xNe,{diagram:()=>l6t});var l6t,bNe=F(()=>{"use strict";lNe();uNe();dNe();vNe();l6t={parser:oNe,db:WY,renderer:YY,styles:hNe,init:s(e=>{YY.setConf(e.journey),WY.clear()},"init")}});var XY,ANe,RNe=F(()=>{"use strict";XY=(function(){var e=s(function(p,m,g,y){for(g=g||{},y=p.length;y--;g[p[y]]=m);return g},"o"),t=[6,11,13,14,15,17,19,20,23,24],r=[1,12],n=[1,13],i=[1,14],a=[1,15],o=[1,16],l=[1,19],u=[1,20],h={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,timeline_header:4,document:5,EOF:6,timeline:7,timeline_lr:8,timeline_td:9,line:10,SPACE:11,statement:12,NEWLINE:13,title:14,acc_title:15,acc_title_value:16,acc_descr:17,acc_descr_value:18,acc_descr_multiline_value:19,section:20,period_statement:21,event_statement:22,period:23,event:24,$accept:0,$end:1},terminals_:{2:"error",6:"EOF",7:"timeline",8:"timeline_lr",9:"timeline_td",11:"SPACE",13:"NEWLINE",14:"title",15:"acc_title",16:"acc_title_value",17:"acc_descr",18:"acc_descr_value",19:"acc_descr_multiline_value",20:"section",23:"period",24:"event"},productions_:[0,[3,3],[4,1],[4,1],[4,1],[5,0],[5,2],[10,2],[10,1],[10,1],[10,1],[12,1],[12,2],[12,2],[12,1],[12,1],[12,1],[12,1],[21,1],[22,1]],performAction:s(function(m,g,y,v,x,b,T){var w=b.length-1;switch(x){case 1:return b[w-1];case 3:v.setDirection("LR");break;case 4:v.setDirection("TD");break;case 5:this.$=[];break;case 6:b[w-1].push(b[w]),this.$=b[w-1];break;case 7:case 8:this.$=b[w];break;case 9:case 10:this.$=[];break;case 11:v.getCommonDb().setDiagramTitle(b[w].substr(6)),this.$=b[w].substr(6);break;case 12:this.$=b[w].trim(),v.getCommonDb().setAccTitle(this.$);break;case 13:case 14:this.$=b[w].trim(),v.getCommonDb().setAccDescription(this.$);break;case 15:v.addSection(b[w].substr(8)),this.$=b[w].substr(8);break;case 18:v.addTask(b[w],0,""),this.$=b[w];break;case 19:v.addEvent(b[w].substr(2)),this.$=b[w];break}},"anonymous"),table:[{3:1,4:2,7:[1,3],8:[1,4],9:[1,5]},{1:[3]},e(t,[2,5],{5:6}),e(t,[2,2]),e(t,[2,3]),e(t,[2,4]),{6:[1,7],10:8,11:[1,9],12:10,13:[1,11],14:r,15:n,17:i,19:a,20:o,21:17,22:18,23:l,24:u},e(t,[2,10],{1:[2,1]}),e(t,[2,6]),{12:21,14:r,15:n,17:i,19:a,20:o,21:17,22:18,23:l,24:u},e(t,[2,8]),e(t,[2,9]),e(t,[2,11]),{16:[1,22]},{18:[1,23]},e(t,[2,14]),e(t,[2,15]),e(t,[2,16]),e(t,[2,17]),e(t,[2,18]),e(t,[2,19]),e(t,[2,7]),e(t,[2,12]),e(t,[2,13])],defaultActions:{},parseError:s(function(m,g){if(g.recoverable)this.trace(m);else{var y=new Error(m);throw y.hash=g,y}},"parseError"),parse:s(function(m){var g=this,y=[0],v=[],x=[null],b=[],T=this.table,w="",C=0,S=0,k=0,R=2,M=1,P=b.slice.call(arguments,1),N=Object.create(this.lexer),_={yy:{}};for(var A in this.yy)Object.prototype.hasOwnProperty.call(this.yy,A)&&(_.yy[A]=this.yy[A]);N.setInput(m,_.yy),_.yy.lexer=N,_.yy.parser=this,typeof N.yylloc>"u"&&(N.yylloc={});var I=N.yylloc;b.push(I);var D=N.options&&N.options.ranges;typeof _.yy.parseError=="function"?this.parseError=_.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function L(fe){y.length=y.length-2*fe,x.length=x.length-fe,b.length=b.length-fe}s(L,"popStack");function B(){var fe;return fe=v.pop()||N.lex()||M,typeof fe!="number"&&(fe instanceof Array&&(v=fe,fe=v.pop()),fe=g.symbols_[fe]||fe),fe}s(B,"lex");for(var O,$,W,V,z,H,K={},J,ne,Z,pe;;){if(W=y[y.length-1],this.defaultActions[W]?V=this.defaultActions[W]:((O===null||typeof O>"u")&&(O=B()),V=T[W]&&T[W][O]),typeof V>"u"||!V.length||!V[0]){var te="";pe=[];for(J in T[W])this.terminals_[J]&&J>R&&pe.push("'"+this.terminals_[J]+"'");N.showPosition?te="Parse error on line "+(C+1)+`: +`+N.showPosition()+` +Expecting `+pe.join(", ")+", got '"+(this.terminals_[O]||O)+"'":te="Parse error on line "+(C+1)+": Unexpected "+(O==M?"end of input":"'"+(this.terminals_[O]||O)+"'"),this.parseError(te,{text:N.match,token:this.terminals_[O]||O,line:N.yylineno,loc:I,expected:pe})}if(V[0]instanceof Array&&V.length>1)throw new Error("Parse Error: multiple actions possible at state: "+W+", token: "+O);switch(V[0]){case 1:y.push(O),x.push(N.yytext),b.push(N.yylloc),y.push(V[1]),O=null,$?(O=$,$=null):(S=N.yyleng,w=N.yytext,C=N.yylineno,I=N.yylloc,k>0&&k--);break;case 2:if(ne=this.productions_[V[1]][1],K.$=x[x.length-ne],K._$={first_line:b[b.length-(ne||1)].first_line,last_line:b[b.length-1].last_line,first_column:b[b.length-(ne||1)].first_column,last_column:b[b.length-1].last_column},D&&(K._$.range=[b[b.length-(ne||1)].range[0],b[b.length-1].range[1]]),H=this.performAction.apply(K,[w,S,C,_.yy,V[1],x,b].concat(P)),typeof H<"u")return H;ne&&(y=y.slice(0,-1*ne*2),x=x.slice(0,-1*ne),b=b.slice(0,-1*ne)),y.push(this.productions_[V[1]][0]),x.push(K.$),b.push(K._$),Z=T[y[y.length-2]][y[y.length-1]],y.push(Z);break;case 3:return!0}}return!0},"parse")},d=(function(){var p={EOF:1,parseError:s(function(g,y){if(this.yy.parser)this.yy.parser.parseError(g,y);else throw new Error(g)},"parseError"),setInput:s(function(m,g){return this.yy=g||this.yy||{},this._input=m,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var m=this._input[0];this.yytext+=m,this.yyleng++,this.offset++,this.match+=m,this.matched+=m;var g=m.match(/(?:\r\n?|\n).*/g);return g?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),m},"input"),unput:s(function(m){var g=m.length,y=m.split(/(?:\r\n?|\n)/g);this._input=m+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-g),this.offset-=g;var v=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),y.length-1&&(this.yylineno-=y.length-1);var x=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:y?(y.length===v.length?this.yylloc.first_column:0)+v[v.length-y.length].length-y[0].length:this.yylloc.first_column-g},this.options.ranges&&(this.yylloc.range=[x[0],x[0]+this.yyleng-g]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(m){this.unput(this.match.slice(m))},"less"),pastInput:s(function(){var m=this.matched.substr(0,this.matched.length-this.match.length);return(m.length>20?"...":"")+m.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var m=this.match;return m.length<20&&(m+=this._input.substr(0,20-m.length)),(m.substr(0,20)+(m.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var m=this.pastInput(),g=new Array(m.length+1).join("-");return m+this.upcomingInput()+` +`+g+"^"},"showPosition"),test_match:s(function(m,g){var y,v,x;if(this.options.backtrack_lexer&&(x={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(x.yylloc.range=this.yylloc.range.slice(0))),v=m[0].match(/(?:\r\n?|\n).*/g),v&&(this.yylineno+=v.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:v?v[v.length-1].length-v[v.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+m[0].length},this.yytext+=m[0],this.match+=m[0],this.matches=m,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(m[0].length),this.matched+=m[0],y=this.performAction.call(this,this.yy,this,g,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),y)return y;if(this._backtrack){for(var b in x)this[b]=x[b];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var m,g,y,v;this._more||(this.yytext="",this.match="");for(var x=this._currentRules(),b=0;bg[0].length)){if(g=y,v=b,this.options.backtrack_lexer){if(m=this.test_match(y,x[b]),m!==!1)return m;if(this._backtrack){g=!1;continue}else return!1}else if(!this.options.flex)break}return g?(m=this.test_match(g,x[v]),m!==!1?m:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var g=this.next();return g||this.lex()},"lex"),begin:s(function(g){this.conditionStack.push(g)},"begin"),popState:s(function(){var g=this.conditionStack.length-1;return g>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(g){return g=this.conditionStack.length-1-Math.abs(g||0),g>=0?this.conditionStack[g]:"INITIAL"},"topState"),pushState:s(function(g){this.begin(g)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(g,y,v,x){var b=x;switch(v){case 0:break;case 1:break;case 2:return 13;case 3:break;case 4:break;case 5:return 8;case 6:return 9;case 7:return 7;case 8:return 14;case 9:return this.begin("acc_title"),15;break;case 10:return this.popState(),"acc_title_value";break;case 11:return this.begin("acc_descr"),17;break;case 12:return this.popState(),"acc_descr_value";break;case 13:this.begin("acc_descr_multiline");break;case 14:this.popState();break;case 15:return"acc_descr_multiline_value";case 16:return 20;case 17:return 24;case 18:return 23;case 19:return 6;case 20:return"INVALID"}},"anonymous"),rules:[/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:timeline[ \t]+LR\b)/i,/^(?:timeline[ \t]+TD\b)/i,/^(?:timeline\b)/i,/^(?:title\s[^\n]+)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:section\s[^:\n]+)/i,/^(?::\s(?:[^:\n]|:(?!\s))+)/i,/^(?:[^#:\n]+)/i,/^(?:$)/i,/^(?:.)/i],conditions:{acc_descr_multiline:{rules:[14,15],inclusive:!1},acc_descr:{rules:[12],inclusive:!1},acc_title:{rules:[10],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,11,13,16,17,18,19,20],inclusive:!0}}};return p})();h.lexer=d;function f(){this.yy={}}return s(f,"Parser"),f.prototype=h,h.Parser=f,new f})();XY.parser=XY;ANe=XY});var QY={};sr(QY,{addEvent:()=>FNe,addSection:()=>PNe,addTask:()=>$Ne,addTaskOrg:()=>GNe,clear:()=>INe,default:()=>y6t,getCommonDb:()=>DNe,getDirection:()=>NNe,getSections:()=>ONe,getTasks:()=>BNe,setDirection:()=>MNe});var ix,LNe,KY,ZY,wL,ax,DNe,INe,MNe,NNe,PNe,ONe,BNe,$Ne,FNe,GNe,_Ne,y6t,zNe=F(()=>{"use strict";$n();ix="",LNe=0,KY="LR",ZY=[],wL=[],ax=[],DNe=s(()=>nb,"getCommonDb"),INe=s(function(){ZY.length=0,wL.length=0,ix="",ax.length=0,KY="LR",Tr()},"clear"),MNe=s(function(e){KY=e},"setDirection"),NNe=s(function(){return KY},"getDirection"),PNe=s(function(e){ix=e,ZY.push(e)},"addSection"),ONe=s(function(){return ZY},"getSections"),BNe=s(function(){let e=_Ne(),t=100,r=0;for(;!e&&rr.id===LNe-1).events.push(e)},"addEvent"),GNe=s(function(e){let t={section:ix,type:ix,description:e,task:e,classes:[]};wL.push(t)},"addTaskOrg"),_Ne=s(function(){let e=s(function(r){return ax[r].processed},"compileTask"),t=!0;for(let[r,n]of ax.entries())e(r),t=t&&n.processed;return t},"compileTasks"),y6t={clear:INe,getCommonDb:DNe,getDirection:NNe,setDirection:MNe,addSection:PNe,getSections:ONe,getTasks:BNe,addTask:$Ne,addTaskOrg:GNe,addEvent:FNe}});function HNe(e,t){e.each(function(){var r=ut(this),n=r.text().split(/(\s+|
    )/).reverse(),i,a=[],o=1.1,l=r.attr("y"),u=parseFloat(r.attr("dy")),h=r.text(null).append("tspan").attr("x",0).attr("y",l).attr("dy",u+"em");for(let d=0;dt||i==="
    ")&&(a.pop(),h.text(a.join(" ").trim()),i==="
    "?a=[""]:a=[i],h=r.append("tspan").attr("x",0).attr("y",l).attr("dy",o+"em").text(i))})}var VNe,kL,v6t,x6t,WNe,b6t,T6t,JY,C6t,S6t,w6t,ej,qNe,k6t,E6t,A6t,R6t,ws,tj=F(()=>{"use strict";Wr();VNe=0,kL=s(function(e,t){let r=e.append("rect");return r.attr("x",t.x),r.attr("y",t.y),r.attr("fill",t.fill),r.attr("stroke",t.stroke),r.attr("width",t.width),r.attr("height",t.height),r.attr("rx",t.rx),r.attr("ry",t.ry),t.class!==void 0&&r.attr("class",t.class),r},"drawRect"),v6t=s(function(e,t){let n=e.append("circle").attr("cx",t.cx).attr("cy",t.cy).attr("class","face").attr("r",15).attr("stroke-width",2).attr("overflow","visible"),i=e.append("g");i.append("circle").attr("cx",t.cx-15/3).attr("cy",t.cy-15/3).attr("r",1.5).attr("stroke-width",2).attr("fill","#666").attr("stroke","#666"),i.append("circle").attr("cx",t.cx+15/3).attr("cy",t.cy-15/3).attr("r",1.5).attr("stroke-width",2).attr("fill","#666").attr("stroke","#666");function a(u){let h=nc().startAngle(Math.PI/2).endAngle(3*(Math.PI/2)).innerRadius(7.5).outerRadius(6.8181818181818175);u.append("path").attr("class","mouth").attr("d",h).attr("transform","translate("+t.cx+","+(t.cy+2)+")")}s(a,"smile");function o(u){let h=nc().startAngle(3*Math.PI/2).endAngle(5*(Math.PI/2)).innerRadius(7.5).outerRadius(6.8181818181818175);u.append("path").attr("class","mouth").attr("d",h).attr("transform","translate("+t.cx+","+(t.cy+7)+")")}s(o,"sad");function l(u){u.append("line").attr("class","mouth").attr("stroke",2).attr("x1",t.cx-5).attr("y1",t.cy+7).attr("x2",t.cx+5).attr("y2",t.cy+7).attr("class","mouth").attr("stroke-width","1px").attr("stroke","#666")}return s(l,"ambivalent"),t.score>3?a(i):t.score<3?o(i):l(i),n},"drawFace"),x6t=s(function(e,t){let r=e.append("circle");return r.attr("cx",t.cx),r.attr("cy",t.cy),r.attr("class","actor-"+t.pos),r.attr("fill",t.fill),r.attr("stroke",t.stroke),r.attr("r",t.r),r.class!==void 0&&r.attr("class",r.class),t.title!==void 0&&r.append("title").text(t.title),r},"drawCircle"),WNe=s(function(e,t){let r=t.text.replace(//gi," "),n=e.append("text");n.attr("x",t.x),n.attr("y",t.y),n.attr("class","legend"),n.style("text-anchor",t.anchor),t.class!==void 0&&n.attr("class",t.class);let i=n.append("tspan");return i.attr("x",t.x+t.textMargin*2),i.text(r),n},"drawText"),b6t=s(function(e,t){function r(i,a,o,l,u){return i+","+a+" "+(i+o)+","+a+" "+(i+o)+","+(a+l-u)+" "+(i+o-u*1.2)+","+(a+l)+" "+i+","+(a+l)}s(r,"genPoints");let n=e.append("polygon");n.attr("points",r(t.x,t.y,50,20,7)),n.attr("class","labelBox"),t.y=t.y+t.labelMargin,t.x=t.x+.5*t.labelMargin,WNe(e,t)},"drawLabel"),T6t=s(function(e,t,r){let n=e.append("g"),i=ej();i.x=t.x,i.y=t.y,i.fill=t.fill,i.width=r.width,i.height=r.height,i.class="journey-section section-type-"+t.num,i.rx=3,i.ry=3,kL(n,i),qNe(r)(t.text,n,i.x,i.y,i.width,i.height,{class:"journey-section section-type-"+t.num},r,t.colour)},"drawSection"),JY=-1,C6t=s(function(e,t,r,n){let i=t.x+r.width/2,a=e.append("g");JY++,a.append("line").attr("id",n+"-task"+JY).attr("x1",i).attr("y1",t.y).attr("x2",i).attr("y2",450).attr("class","task-line").attr("stroke-width","1px").attr("stroke-dasharray","4 2").attr("stroke","#666"),v6t(a,{cx:i,cy:300+(5-t.score)*30,score:t.score});let l=ej();l.x=t.x,l.y=t.y,l.fill=t.fill,l.width=r.width,l.height=r.height,l.class="task task-type-"+t.num,l.rx=3,l.ry=3,kL(a,l),qNe(r)(t.task,a,l.x,l.y,l.width,l.height,{class:"task"},r,t.colour)},"drawTask"),S6t=s(function(e,t){kL(e,{x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,class:"rect"}).lower()},"drawBackgroundRect"),w6t=s(function(){return{x:0,y:0,fill:void 0,"text-anchor":"start",width:100,height:100,textMargin:0,rx:0,ry:0}},"getTextObj"),ej=s(function(){return{x:0,y:0,width:100,anchor:"start",height:100,rx:0,ry:0}},"getNoteRect"),qNe=(function(){function e(i,a,o,l,u,h,d,f){let p=a.append("text").attr("x",o+u/2).attr("y",l+h/2+5).style("font-color",f).style("text-anchor","middle").text(i);n(p,d)}s(e,"byText");function t(i,a,o,l,u,h,d,f,p){let{taskFontSize:m,taskFontFamily:g}=f,y=i.split(//gi);for(let v=0;v0?`M0 ${t.height-l} v${-t.height+2*l} q0,-${o},${o},-${o} h${t.width-2*l} q${o},0,${o},${o} v${t.height-l} H0 Z`:`M0 ${t.height-l} v${-(t.height-l)} h${t.width} v${t.height} H0 Z`;e.append("path").attr("id",n+"-node-"+VNe++).attr("class","node-bkg node-"+t.type).attr("d",u),a?.includes("redux")||e.append("line").attr("class","node-line-"+r).attr("x1",0).attr("y1",t.height).attr("x2",t.width).attr("y2",t.height)},"defaultBkg"),ws={drawRect:kL,drawCircle:x6t,drawSection:T6t,drawText:WNe,drawLabel:b6t,drawTask:C6t,drawBackgroundRect:S6t,getTextObj:w6t,getNoteRect:ej,initGraphics:k6t,drawNode:E6t,getVirtualNodeHeight:A6t}});var _6t,UNe,L6t,YNe,jNe=F(()=>{"use strict";Wr();tj();St();Jt();Vn();_6t=s(function(e,t,r,n){let i=Pe(),{look:a,theme:o,themeVariables:l}=i,{useGradient:u,gradientStart:h,gradientStop:d}=l,f=i.timeline?.leftMargin??50;se.debug("timeline",n.db);let p=i.securityLevel,m;p==="sandbox"&&(m=ut("#i"+t));let y=(p==="sandbox"?ut(m.nodes()[0].contentDocument.body):ut("body")).select("#"+t);y.append("g");let v=n.db.getTasks(),x=n.db.getCommonDb().getDiagramTitle();se.debug("task",v),ws.initGraphics(y,t);let b=n.db.getSections();se.debug("sections",b);let T=0,w=0,C=0,S=0,k=50+f,R=50;S=50;let M=0,P=!0;b.forEach(function(D){let L={number:M,descr:D,section:M,width:150,padding:20,maxHeight:T},B=ws.getVirtualNodeHeight(y,L,i);se.debug("sectionHeight before draw",B),T=Math.max(T,B+20)});let N=0,_=0;se.debug("tasks.length",v.length);for(let[D,L]of v.entries()){let B={number:D,descr:L,section:L.section,width:150,padding:20,maxHeight:w},O=ws.getVirtualNodeHeight(y,B,i);se.debug("taskHeight before draw",O),w=Math.max(w,O+20),N=Math.max(N,L.events.length);let $=0;for(let W of L.events){let V={descr:W,section:L.section,number:L.section,width:150,padding:20,maxHeight:50};$+=ws.getVirtualNodeHeight(y,V,i)}L.events.length>0&&($+=(L.events.length-1)*10),_=Math.max(_,$)}se.debug("maxSectionHeight before draw",T),se.debug("maxTaskHeight before draw",w),b&&b.length>0?b.forEach(D=>{let L=v.filter(W=>W.section===D),B={number:M,descr:D,section:M,width:200*Math.max(L.length,1)-50,padding:20,maxHeight:T};se.debug("sectionNode",B);let O=y.append("g"),$=ws.drawNode(O,B,M,i,t);se.debug("sectionNode output",$),O.attr("transform",`translate(${k}, ${S})`),R+=T+50,L.length>0&&UNe(y,L,M,k,R,w,i,N,_,T,!1,t),k+=200*Math.max(L.length,1),R=S,M++}):(P=!1,UNe(y,v,M,k,R,w,i,N,_,T,!0,t));let A=y.node().getBBox();if(se.debug("bounds",A),x&&y.append("text").text(x).attr("x",a==="neo"?A.x*2+f:A.width/2-f).attr("font-size","4ex").attr("font-weight","bold").attr("y",20),C=P?T+w+150:w+100,y.append("g").attr("class","lineWrapper").append("line").attr("x1",f).attr("y1",C).attr("x2",A.width+3*f).attr("y2",C).attr("stroke-width",4).attr("stroke","black").attr("marker-end",`url(#${t}-arrowhead)`),a==="neo"&&u&&o!=="neutral"){let D=y.select("defs"),B=(D.empty()?y.append("defs"):D).append("linearGradient").attr("id",y.attr("id")+"-gradient").attr("gradientUnits","objectBoundingBox").attr("x1","0%").attr("y1","0%").attr("x2","100%").attr("y2","0%");B.append("stop").attr("offset","0%").attr("stop-color",h).attr("stop-opacity",1),B.append("stop").attr("offset","100%").attr("stop-color",d).attr("stop-opacity",1)}ml(void 0,y,i.timeline?.padding??50,i.timeline?.useMaxWidth??!1)},"draw"),UNe=s(function(e,t,r,n,i,a,o,l,u,h,d,f){for(let p of t){let m={descr:p.task,section:r,number:r,width:150,padding:20,maxHeight:a};se.debug("taskNode",m);let g=e.append("g").attr("class","taskWrapper"),v=ws.drawNode(g,m,r,o,f).height;if(se.debug("taskHeight after draw",v),g.attr("transform",`translate(${n}, ${i})`),a=Math.max(a,v),p.events){let x=e.append("g").attr("class","lineWrapper"),b=a;i+=100,b=b+L6t(e,p.events,r,n,i,o,f),i-=100,x.append("line").attr("x1",n+190/2).attr("y1",i+a).attr("x2",n+190/2).attr("y2",i+a+100+u+100).attr("stroke-width",2).attr("stroke","black").attr("marker-end",`url(#${f}-arrowhead)`).attr("stroke-dasharray","5,5")}n=n+200,d&&!o.timeline?.disableMulticolor&&r++}i=i-10},"drawTasks"),L6t=s(function(e,t,r,n,i,a,o){let l=0,u=i;i=i+100;for(let h of t){let d={descr:h,section:r,number:r,width:150,padding:20,maxHeight:50};se.debug("eventNode",d);let f=e.append("g").attr("class","eventWrapper"),m=ws.drawNode(f,d,r,a,o,!0).height;l=l+m,f.attr("transform",`translate(${n}, ${i})`),i=i+10+m}return i=u,l},"drawEvents"),YNe={setConf:s(()=>{},"setConf"),draw:_6t}});var EL,md,D6t,rj,I6t,QNe,M6t,XNe,JNe,KNe,ePe,N6t,ZNe,P6t,tPe,rPe=F(()=>{"use strict";tj();St();Jt();Vn();cs();er();EL=200,md=5,D6t=EL+md*2,rj=EL+100,I6t=rj+md*2,QNe=10,M6t=0,XNe=20,JNe=20,KNe=30,ePe=50,N6t=s(function(e,t,r,n){let i=Pe(),a=i.timeline?.leftMargin??50;se.debug("timeline",n.db);let o=wn(t);o.append("g");let l=n.db.getTasks(),u=n.db.getCommonDb().getDiagramTitle();se.debug("task",l),ws.initGraphics(o);let h=n.db.getSections();se.debug("sections",h);let d=0,f=0,p=50+a,m=50,g=m,y=p,v=D6t+JNe,x=I6t+ePe,b=y+v,T=0,w=h&&h.length>0,C=w?b:p+v,S=Math.max(50,v+x-md*2);h.forEach(function(D){let L={number:T,descr:D,section:T,width:S,padding:md,maxHeight:d},B=ws.getVirtualNodeHeight(o,L,i);se.debug("sectionHeight before draw",B),d=Math.max(d,B)});let k=0;se.debug("tasks.length",l.length);for(let[D,L]of l.entries()){let B={number:D,descr:L,section:L.section,width:EL,padding:md,maxHeight:f},O=ws.getVirtualNodeHeight(o,B,i);se.debug("taskHeight before draw",O),f=Math.max(f,O);let $=0;for(let W of L.events){let V={descr:W,section:L.section,number:L.section,width:rj,padding:md,maxHeight:50};$+=ws.getVirtualNodeHeight(o,V,i)}L.events.length>0&&($+=(L.events.length-1)*QNe),k=Math.max(k,$)+M6t}se.debug("maxSectionHeight before draw",d),se.debug("maxTaskHeight before draw",f);let M=Math.max(f,k)+KNe;w?h.forEach(D=>{let L=l.filter(K=>K.section===D),B={number:T,descr:D,section:T,width:S,padding:md,maxHeight:d};se.debug("sectionNode",B);let O=o.append("g"),$=ws.drawNode(O,B,T,i);se.debug("sectionNode output",$);let W=C-v;O.attr("transform",`translate(${W}, ${m})`);let V=m+$.height+XNe;L.length>0&&ZNe(o,L,T,C,V,f,i,M,!1);let z=L.length,H=$.height+XNe+M*Math.max(z,1)-(z>0?KNe*2:0);m+=H,T++}):ZNe(o,l,T,C,m,f,i,M,!0);let P=o.node()?.getBBox();if(!P)throw new Error("bbox not found");if(se.debug("bounds",P),u){if(o.append("text").text(u).attr("x",P.width/2-a).attr("font-size","4ex").attr("font-weight","bold").attr("y",20),P=o.node()?.getBBox(),!P)throw new Error("bbox not found");se.debug("bounds after title",P)}let[N]=Bs(i.fontSize),_=(N??16)*2,A=(N??16)*.5+20,I=o.append("g").attr("class","lineWrapper");I.append("line").attr("x1",C).attr("y1",g-_).attr("x2",C).attr("y2",P.y+P.height+A).attr("stroke-width",4).attr("stroke","black").attr("marker-end","url(#arrowhead)"),I.lower(),ml(void 0,o,i.timeline?.padding??50,i.timeline?.useMaxWidth??!1)},"draw"),ZNe=s(function(e,t,r,n,i,a,o,l,u){for(let h of t){let d={descr:h.task,section:r,number:r,width:EL,padding:md,maxHeight:a};se.debug("taskNode",d);let f=e.append("g").attr("class","taskWrapper"),p=ws.drawNode(f,d,r,o),m=p.height;se.debug("taskHeight after draw",m);let g=n-JNe-p.width;if(f.attr("transform",`translate(${g}, ${i})`),a=Math.max(a,m),h.events&&h.events.length>0){let y=i,v=n+ePe;P6t(e,h.events,r,n,v,y,o)}i=i+l,u&&!o.timeline?.disableMulticolor&&r++}},"drawTasks"),P6t=s(function(e,t,r,n,i,a,o){let l=a;for(let u of t){let h={descr:u,section:r,number:r,width:rj,padding:md,maxHeight:0};se.debug("eventNode",h);let d=e.append("g").attr("class","eventWrapper"),p=ws.drawNode(d,h,r,o).height;d.attr("transform",`translate(${i}, ${l})`);let m=e.append("g").attr("class","lineWrapper"),g=l+p/2;m.append("line").attr("x1",n).attr("y1",g).attr("x2",i).attr("y2",g).attr("stroke-width",2).attr("stroke","black").attr("marker-end","url(#arrowhead)").attr("stroke-dasharray","5,5"),l=l+p+QNe}return l-a},"drawEvents"),tPe={setConf:s(()=>{},"setConf"),draw:N6t}});var O6t,B6t,$6t,nPe,iPe=F(()=>{"use strict";Xi();br();O6t=s(e=>{let{theme:t}=Lt(),r=t?.includes("dark"),n=t?.includes("color"),i=e.svgId?.replace(/^#/,"")??"",a=i?`url(#${i}-drop-shadow)`:e.dropShadow??"none",o="";for(let l=0;l{let t="";for(let r=0;r{let{theme:t}=Lt(),r=t?.includes("redux"),n=t==="neutral",i=e.svgId?.replace(/^#/,"")??"",a="";if(e.useGradient&&i&&e.THEME_COLOR_LIMIT&&!n)for(let o=0;oG6t});var F6t,G6t,sPe=F(()=>{"use strict";RNe();zNe();jNe();rPe();iPe();F6t={setConf:s(()=>{},"setConf"),draw:s((e,t,r,n)=>(n?.db?.getDirection?.()??"LR")==="TD"?tPe.draw(e,t,r,n):YNe.draw(e,t,r,n),"draw")},G6t={db:QY,renderer:F6t,parser:ANe,styles:nPe}});var nj,cPe,uPe=F(()=>{"use strict";nj=(function(){var e=s(function(w,C,S,k){for(S=S||{},k=w.length;k--;S[w[k]]=C);return S},"o"),t=[1,4],r=[1,13],n=[1,12],i=[1,15],a=[1,16],o=[1,20],l=[1,19],u=[6,7,8],h=[1,26],d=[1,24],f=[1,25],p=[6,7,11],m=[1,6,13,15,16,19,22],g=[1,33],y=[1,34],v=[1,6,7,11,13,15,16,19,22],x={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,MINDMAP:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,ICON:15,CLASS:16,nodeWithId:17,nodeWithoutId:18,NODE_DSTART:19,NODE_DESCR:20,NODE_DEND:21,NODE_ID:22,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"MINDMAP",11:"EOF",13:"SPACELIST",15:"ICON",16:"CLASS",19:"NODE_DSTART",20:"NODE_DESCR",21:"NODE_DEND",22:"NODE_ID"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,2],[12,2],[12,2],[12,1],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[18,3],[17,1],[17,4]],performAction:s(function(C,S,k,R,M,P,N){var _=P.length-1;switch(M){case 6:case 7:return R;case 8:R.getLogger().trace("Stop NL ");break;case 9:R.getLogger().trace("Stop EOF ");break;case 11:R.getLogger().trace("Stop NL2 ");break;case 12:R.getLogger().trace("Stop EOF2 ");break;case 15:R.getLogger().info("Node: ",P[_].id),R.addNode(P[_-1].length,P[_].id,P[_].descr,P[_].type);break;case 16:R.getLogger().trace("Icon: ",P[_]),R.decorateNode({icon:P[_]});break;case 17:case 21:R.decorateNode({class:P[_]});break;case 18:R.getLogger().trace("SPACELIST");break;case 19:R.getLogger().trace("Node: ",P[_].id),R.addNode(0,P[_].id,P[_].descr,P[_].type);break;case 20:R.decorateNode({icon:P[_]});break;case 25:R.getLogger().trace("node found ..",P[_-2]),this.$={id:P[_-1],descr:P[_-1],type:R.getType(P[_-2],P[_])};break;case 26:this.$={id:P[_],descr:P[_],type:R.nodeType.DEFAULT};break;case 27:R.getLogger().trace("node found ..",P[_-3]),this.$={id:P[_-3],descr:P[_-1],type:R.getType(P[_-2],P[_])};break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:t},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:t},{6:r,7:[1,10],9:9,12:11,13:n,14:14,15:i,16:a,17:17,18:18,19:o,22:l},e(u,[2,3]),{1:[2,2]},e(u,[2,4]),e(u,[2,5]),{1:[2,6],6:r,12:21,13:n,14:14,15:i,16:a,17:17,18:18,19:o,22:l},{6:r,9:22,12:11,13:n,14:14,15:i,16:a,17:17,18:18,19:o,22:l},{6:h,7:d,10:23,11:f},e(p,[2,22],{17:17,18:18,14:27,15:[1,28],16:[1,29],19:o,22:l}),e(p,[2,18]),e(p,[2,19]),e(p,[2,20]),e(p,[2,21]),e(p,[2,23]),e(p,[2,24]),e(p,[2,26],{19:[1,30]}),{20:[1,31]},{6:h,7:d,10:32,11:f},{1:[2,7],6:r,12:21,13:n,14:14,15:i,16:a,17:17,18:18,19:o,22:l},e(m,[2,14],{7:g,11:y}),e(v,[2,8]),e(v,[2,9]),e(v,[2,10]),e(p,[2,15]),e(p,[2,16]),e(p,[2,17]),{20:[1,35]},{21:[1,36]},e(m,[2,13],{7:g,11:y}),e(v,[2,11]),e(v,[2,12]),{21:[1,37]},e(p,[2,25]),e(p,[2,27])],defaultActions:{2:[2,1],6:[2,2]},parseError:s(function(C,S){if(S.recoverable)this.trace(C);else{var k=new Error(C);throw k.hash=S,k}},"parseError"),parse:s(function(C){var S=this,k=[0],R=[],M=[null],P=[],N=this.table,_="",A=0,I=0,D=0,L=2,B=1,O=P.slice.call(arguments,1),$=Object.create(this.lexer),W={yy:{}};for(var V in this.yy)Object.prototype.hasOwnProperty.call(this.yy,V)&&(W.yy[V]=this.yy[V]);$.setInput(C,W.yy),W.yy.lexer=$,W.yy.parser=this,typeof $.yylloc>"u"&&($.yylloc={});var z=$.yylloc;P.push(z);var H=$.options&&$.options.ranges;typeof W.yy.parseError=="function"?this.parseError=W.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function K(ye){k.length=k.length-2*ye,M.length=M.length-ye,P.length=P.length-ye}s(K,"popStack");function J(){var ye;return ye=R.pop()||$.lex()||B,typeof ye!="number"&&(ye instanceof Array&&(R=ye,ye=R.pop()),ye=S.symbols_[ye]||ye),ye}s(J,"lex");for(var ne,Z,pe,te,fe,ce,de={},Ee,we,Fe,Ye;;){if(pe=k[k.length-1],this.defaultActions[pe]?te=this.defaultActions[pe]:((ne===null||typeof ne>"u")&&(ne=J()),te=N[pe]&&N[pe][ne]),typeof te>"u"||!te.length||!te[0]){var He="";Ye=[];for(Ee in N[pe])this.terminals_[Ee]&&Ee>L&&Ye.push("'"+this.terminals_[Ee]+"'");$.showPosition?He="Parse error on line "+(A+1)+`: +`+$.showPosition()+` +Expecting `+Ye.join(", ")+", got '"+(this.terminals_[ne]||ne)+"'":He="Parse error on line "+(A+1)+": Unexpected "+(ne==B?"end of input":"'"+(this.terminals_[ne]||ne)+"'"),this.parseError(He,{text:$.match,token:this.terminals_[ne]||ne,line:$.yylineno,loc:z,expected:Ye})}if(te[0]instanceof Array&&te.length>1)throw new Error("Parse Error: multiple actions possible at state: "+pe+", token: "+ne);switch(te[0]){case 1:k.push(ne),M.push($.yytext),P.push($.yylloc),k.push(te[1]),ne=null,Z?(ne=Z,Z=null):(I=$.yyleng,_=$.yytext,A=$.yylineno,z=$.yylloc,D>0&&D--);break;case 2:if(we=this.productions_[te[1]][1],de.$=M[M.length-we],de._$={first_line:P[P.length-(we||1)].first_line,last_line:P[P.length-1].last_line,first_column:P[P.length-(we||1)].first_column,last_column:P[P.length-1].last_column},H&&(de._$.range=[P[P.length-(we||1)].range[0],P[P.length-1].range[1]]),ce=this.performAction.apply(de,[_,I,A,W.yy,te[1],M,P].concat(O)),typeof ce<"u")return ce;we&&(k=k.slice(0,-1*we*2),M=M.slice(0,-1*we),P=P.slice(0,-1*we)),k.push(this.productions_[te[1]][0]),M.push(de.$),P.push(de._$),Fe=N[k[k.length-2]][k[k.length-1]],k.push(Fe);break;case 3:return!0}}return!0},"parse")},b=(function(){var w={EOF:1,parseError:s(function(S,k){if(this.yy.parser)this.yy.parser.parseError(S,k);else throw new Error(S)},"parseError"),setInput:s(function(C,S){return this.yy=S||this.yy||{},this._input=C,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var C=this._input[0];this.yytext+=C,this.yyleng++,this.offset++,this.match+=C,this.matched+=C;var S=C.match(/(?:\r\n?|\n).*/g);return S?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),C},"input"),unput:s(function(C){var S=C.length,k=C.split(/(?:\r\n?|\n)/g);this._input=C+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-S),this.offset-=S;var R=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),k.length-1&&(this.yylineno-=k.length-1);var M=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:k?(k.length===R.length?this.yylloc.first_column:0)+R[R.length-k.length].length-k[0].length:this.yylloc.first_column-S},this.options.ranges&&(this.yylloc.range=[M[0],M[0]+this.yyleng-S]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(C){this.unput(this.match.slice(C))},"less"),pastInput:s(function(){var C=this.matched.substr(0,this.matched.length-this.match.length);return(C.length>20?"...":"")+C.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var C=this.match;return C.length<20&&(C+=this._input.substr(0,20-C.length)),(C.substr(0,20)+(C.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var C=this.pastInput(),S=new Array(C.length+1).join("-");return C+this.upcomingInput()+` +`+S+"^"},"showPosition"),test_match:s(function(C,S){var k,R,M;if(this.options.backtrack_lexer&&(M={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(M.yylloc.range=this.yylloc.range.slice(0))),R=C[0].match(/(?:\r\n?|\n).*/g),R&&(this.yylineno+=R.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:R?R[R.length-1].length-R[R.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+C[0].length},this.yytext+=C[0],this.match+=C[0],this.matches=C,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(C[0].length),this.matched+=C[0],k=this.performAction.call(this,this.yy,this,S,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),k)return k;if(this._backtrack){for(var P in M)this[P]=M[P];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var C,S,k,R;this._more||(this.yytext="",this.match="");for(var M=this._currentRules(),P=0;PS[0].length)){if(S=k,R=P,this.options.backtrack_lexer){if(C=this.test_match(k,M[P]),C!==!1)return C;if(this._backtrack){S=!1;continue}else return!1}else if(!this.options.flex)break}return S?(C=this.test_match(S,M[R]),C!==!1?C:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var S=this.next();return S||this.lex()},"lex"),begin:s(function(S){this.conditionStack.push(S)},"begin"),popState:s(function(){var S=this.conditionStack.length-1;return S>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(S){return S=this.conditionStack.length-1-Math.abs(S||0),S>=0?this.conditionStack[S]:"INITIAL"},"topState"),pushState:s(function(S){this.begin(S)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(S,k,R,M){var P=M;switch(R){case 0:return S.getLogger().trace("Found comment",k.yytext),6;break;case 1:return 8;case 2:this.begin("CLASS");break;case 3:return this.popState(),16;break;case 4:this.popState();break;case 5:S.getLogger().trace("Begin icon"),this.begin("ICON");break;case 6:return S.getLogger().trace("SPACELINE"),6;break;case 7:return 7;case 8:return 15;case 9:S.getLogger().trace("end icon"),this.popState();break;case 10:return S.getLogger().trace("Exploding node"),this.begin("NODE"),19;break;case 11:return S.getLogger().trace("Cloud"),this.begin("NODE"),19;break;case 12:return S.getLogger().trace("Explosion Bang"),this.begin("NODE"),19;break;case 13:return S.getLogger().trace("Cloud Bang"),this.begin("NODE"),19;break;case 14:return this.begin("NODE"),19;break;case 15:return this.begin("NODE"),19;break;case 16:return this.begin("NODE"),19;break;case 17:return this.begin("NODE"),19;break;case 18:return 13;case 19:return 22;case 20:return 11;case 21:this.begin("NSTR2");break;case 22:return"NODE_DESCR";case 23:this.popState();break;case 24:S.getLogger().trace("Starting NSTR"),this.begin("NSTR");break;case 25:return S.getLogger().trace("description:",k.yytext),"NODE_DESCR";break;case 26:this.popState();break;case 27:return this.popState(),S.getLogger().trace("node end ))"),"NODE_DEND";break;case 28:return this.popState(),S.getLogger().trace("node end )"),"NODE_DEND";break;case 29:return this.popState(),S.getLogger().trace("node end ...",k.yytext),"NODE_DEND";break;case 30:return this.popState(),S.getLogger().trace("node end (("),"NODE_DEND";break;case 31:return this.popState(),S.getLogger().trace("node end (-"),"NODE_DEND";break;case 32:return this.popState(),S.getLogger().trace("node end (-"),"NODE_DEND";break;case 33:return this.popState(),S.getLogger().trace("node end (("),"NODE_DEND";break;case 34:return this.popState(),S.getLogger().trace("node end (("),"NODE_DEND";break;case 35:return S.getLogger().trace("Long description:",k.yytext),20;break;case 36:return S.getLogger().trace("Long description:",k.yytext),20;break}},"anonymous"),rules:[/^(?:\s*%%.*)/i,/^(?:mindmap\b)/i,/^(?::::)/i,/^(?:.+)/i,/^(?:\n)/i,/^(?:::icon\()/i,/^(?:[\s]+[\n])/i,/^(?:[\n]+)/i,/^(?:[^\)]+)/i,/^(?:\))/i,/^(?:-\))/i,/^(?:\(-)/i,/^(?:\)\))/i,/^(?:\))/i,/^(?:\(\()/i,/^(?:\{\{)/i,/^(?:\()/i,/^(?:\[)/i,/^(?:[\s]+)/i,/^(?:[^\(\[\n\)\{\}]+)/i,/^(?:$)/i,/^(?:["][`])/i,/^(?:[^`"]+)/i,/^(?:[`]["])/i,/^(?:["])/i,/^(?:[^"]+)/i,/^(?:["])/i,/^(?:[\)]\))/i,/^(?:[\)])/i,/^(?:[\]])/i,/^(?:\}\})/i,/^(?:\(-)/i,/^(?:-\))/i,/^(?:\(\()/i,/^(?:\()/i,/^(?:[^\)\]\(\}]+)/i,/^(?:.+(?!\(\())/i],conditions:{CLASS:{rules:[3,4],inclusive:!1},ICON:{rules:[8,9],inclusive:!1},NSTR2:{rules:[22,23],inclusive:!1},NSTR:{rules:[25,26],inclusive:!1},NODE:{rules:[21,24,27,28,29,30,31,32,33,34,35,36],inclusive:!1},INITIAL:{rules:[0,1,2,5,6,7,10,11,12,13,14,15,16,17,18,19,20],inclusive:!0}}};return w})();x.lexer=b;function T(){this.yy={}}return s(T,"Parser"),T.prototype=x,x.Parser=T,new T})();nj.parser=nj;cPe=nj});function hPe(e,t=0){return(hs[e[t+0]]+hs[e[t+1]]+hs[e[t+2]]+hs[e[t+3]]+"-"+hs[e[t+4]]+hs[e[t+5]]+"-"+hs[e[t+6]]+hs[e[t+7]]+"-"+hs[e[t+8]]+hs[e[t+9]]+"-"+hs[e[t+10]]+hs[e[t+11]]+hs[e[t+12]]+hs[e[t+13]]+hs[e[t+14]]+hs[e[t+15]]).toLowerCase()}var hs,dPe=F(()=>{"use strict";hs=[];for(let e=0;e<256;++e)hs.push((e+256).toString(16).slice(1));s(hPe,"unsafeStringify")});function ij(){return crypto.getRandomValues(q6t)}var q6t,fPe=F(()=>{"use strict";q6t=new Uint8Array(16);s(ij,"rng")});function H6t(e,t,r){return!t&&!e&&crypto.randomUUID?crypto.randomUUID():U6t(e,t,r)}function U6t(e,t,r){e=e||{};let n=e.random??e.rng?.()??ij();if(n.length<16)throw new Error("Random bytes length must be >= 16");if(n[6]=n[6]&15|64,n[8]=n[8]&63|128,t){if(r=r||0,r<0||r+16>t.length)throw new RangeError(`UUID byte range ${r}:${r+15} is out of buffer bounds`);for(let i=0;i<16;++i)t[r+i]=n[i];return t}return hPe(n)}var aj,pPe=F(()=>{"use strict";fPe();dPe();s(H6t,"v4");s(U6t,"_v4");aj=H6t});var mPe=F(()=>{"use strict";pPe()});var gPe,yPe=F(()=>{"use strict";qo();er();gPe=12});var gd,AL,vPe=F(()=>{"use strict";Jt();mPe();Yr();St();Qi();br();yPe();gd={DEFAULT:0,NO_BORDER:0,ROUNDED_RECT:1,RECT:2,CIRCLE:3,CLOUD:4,BANG:5,HEXAGON:6},AL=class{constructor(){this.nodes=[];this.count=0;this.elements={};this.getLogger=this.getLogger.bind(this),this.nodeType=gd,this.clear(),this.getType=this.getType.bind(this),this.getElementById=this.getElementById.bind(this),this.getParent=this.getParent.bind(this),this.getMindmap=this.getMindmap.bind(this),this.addNode=this.addNode.bind(this),this.decorateNode=this.decorateNode.bind(this)}static{s(this,"MindmapDB")}clear(){this.nodes=[],this.count=0,this.elements={},this.baseLevel=void 0}getParent(t){for(let r=this.nodes.length-1;r>=0;r--)if(this.nodes[r].level0?this.nodes[0]:null}addNode(t,r,n,i){se.info("addNode",t,r,n,i);let a=!1;this.nodes.length===0?(this.baseLevel=t,t=0,a=!0):this.baseLevel!==void 0&&(t=t-this.baseLevel,a=!1);let o=Pe(),l=o.mindmap?.padding??mr.mindmap.padding;switch(i){case this.nodeType.ROUNDED_RECT:case this.nodeType.RECT:case this.nodeType.HEXAGON:l*=2;break}let u={id:this.count++,nodeId:xr(r,o),level:t,descr:xr(n,o),type:i,children:[],width:o.mindmap?.maxNodeWidth??mr.mindmap.maxNodeWidth,padding:l,isRoot:a},h=this.getParent(t);if(h)h.children.push(u),this.nodes.push(u);else if(a)this.nodes.push(u);else throw new Error(`There can be only one root. No parent could be found for ("${u.descr}")`)}getType(t,r){switch(se.debug("In get type",t,r),t){case"[":return this.nodeType.RECT;case"(":return r===")"?this.nodeType.ROUNDED_RECT:this.nodeType.CLOUD;case"((":return this.nodeType.CIRCLE;case")":return this.nodeType.CLOUD;case"))":return this.nodeType.BANG;case"{{":return this.nodeType.HEXAGON;default:return this.nodeType.DEFAULT}}setElementForId(t,r){this.elements[t]=r}getElementById(t){return this.elements[t]}decorateNode(t){if(!t)return;let r=Pe(),n=this.nodes[this.nodes.length-1];t.icon&&(n.icon=xr(t.icon,r)),t.class&&(n.class=xr(t.class,r))}type2Str(t){switch(t){case this.nodeType.DEFAULT:return"no-border";case this.nodeType.RECT:return"rect";case this.nodeType.ROUNDED_RECT:return"rounded-rect";case this.nodeType.CIRCLE:return"circle";case this.nodeType.CLOUD:return"cloud";case this.nodeType.BANG:return"bang";case this.nodeType.HEXAGON:return"hexgon";default:return"no-border"}}assignSections(t,r){if(t.level===0?t.section=void 0:t.section=r,t.children)for(let[n,i]of t.children.entries()){let a=t.level===0?n%(gPe-1):r;this.assignSections(i,a)}}flattenNodes(t,r){let n=Pe(),i=["mindmap-node"];t.isRoot===!0?i.push("section-root","section--1"):t.section!==void 0&&i.push(`section-${t.section}`),t.class&&i.push(t.class);let a=i.join(" "),o=s(u=>{let d=(n.theme?.toLowerCase()??"").includes("redux");switch(u){case gd.CIRCLE:return"mindmapCircle";case gd.RECT:return"rect";case gd.ROUNDED_RECT:return"rounded";case gd.CLOUD:return"cloud";case gd.BANG:return"bang";case gd.HEXAGON:return"hexagon";case gd.DEFAULT:return d?"rounded":"defaultMindmapNode";case gd.NO_BORDER:default:return"rect"}},"getShapeFromType"),l={id:t.id.toString(),domId:"node_"+t.id.toString(),label:t.descr,labelType:"markdown",isGroup:!1,shape:o(t.type),width:t.width,height:t.height??0,padding:t.padding,cssClasses:a,cssStyles:[],look:n.look,icon:t.icon,x:t.x,y:t.y,level:t.level,nodeId:t.nodeId,type:t.type,section:t.section};if(r.push(l),t.children)for(let u of t.children)this.flattenNodes(u,r)}generateEdges(t,r){if(!t.children)return;let n=Pe();for(let i of t.children){let a="edge";i.section!==void 0&&(a+=` section-edge-${i.section}`);let o=t.level+1;a+=` edge-depth-${o}`;let l={id:`edge_${t.id}_${i.id}`,start:t.id.toString(),end:i.id.toString(),type:"normal",curve:"basis",thickness:"normal",look:n.look,classes:a,depth:t.level,section:i.section};r.push(l),this.generateEdges(i,r)}}getData(){let t=this.getMindmap(),r=Pe(),i=Ew().layout!==void 0,a=r;if(i||(a.layout="cose-bilkent"),!t)return{nodes:[],edges:[],config:a};se.debug("getData: mindmapRoot",t,r),this.assignSections(t);let o=[],l=[];this.flattenNodes(t,o),this.generateEdges(t,l),se.debug(`getData: processed ${o.length} nodes and ${l.length} edges`);let u=new Map;for(let h of o)u.set(h.id,{shape:h.shape,width:h.width,height:h.height,padding:h.padding});return{nodes:o,edges:l,config:a,rootNode:t,markers:["point"],direction:"TB",nodeSpacing:50,rankSpacing:50,shapes:Object.fromEntries(u),type:"mindmap",diagramId:"mindmap-"+aj()}}getLogger(){return se}}});var Y6t,xPe,bPe=F(()=>{"use strict";St();Rm();ep();tp();Qi();br();Y6t=s(async(e,t,r,n)=>{se.debug(`Rendering mindmap diagram +`+e);let i=n.db,a=i.getData(),o=xl(t,a.config.securityLevel);if(a.type=n.type,a.layoutAlgorithm=Au(a.config.layout,{fallback:"cose-bilkent"}),a.diagramId=t,!i.getMindmap())return;a.nodes.forEach(p=>{p.shape==="rounded"?(p.radius=15,p.taper=15,p.stroke="none",p.width=0,p.padding=15):p.shape==="circle"?p.padding=10:p.shape==="rect"?(p.width=0,p.padding=10):p.shape==="hexagon"&&(p.width=0,p.height=0)}),await Dl(a,o);let{themeVariables:u}=Lt(),{useGradient:h,gradientStart:d,gradientStop:f}=u;if(h&&d&&f){let p=o.attr("id"),m=o.append("defs").append("linearGradient").attr("id",`${p}-gradient`).attr("gradientUnits","objectBoundingBox").attr("x1","0%").attr("y1","0%").attr("x2","100%").attr("y2","0%");m.append("stop").attr("offset","0%").attr("stop-color",d).attr("stop-opacity",1),m.append("stop").attr("offset","100%").attr("stop-color",f).attr("stop-opacity",1)}Co(o,a.config.mindmap?.padding??mr.mindmap.padding,"mindmapDiagram",a.config.mindmap?.useMaxWidth??mr.mindmap.useMaxWidth)},"draw"),xPe={draw:Y6t}});var j6t,X6t,K6t,TPe,CPe=F(()=>{"use strict";Xi();j6t=s(e=>{let{theme:t,look:r}=e,n="";for(let i=0;i{let n="";for(let i=0;i{let{theme:t}=e,r=e.svgId,n=e.dropShadow?e.dropShadow.replace("url(#drop-shadow)",`url(${r}-drop-shadow)`):"none";return` + .edge { + stroke-width: 3; + } + ${j6t(e)} + .section-root rect, .section-root path, .section-root circle, .section-root polygon { + fill: ${e.git0}; + } + .section-root text { + fill: ${e.gitBranchLabel0}; + } + .section-root span { + color: ${t?.includes("redux")?e.nodeBorder:e.gitBranchLabel0}; + } + .icon-container { + height:100%; + display: flex; + justify-content: center; + align-items: center; + } + .edge { + fill: none; + } + .mindmap-node-label { + dy: 1em; + alignment-baseline: middle; + text-anchor: middle; + dominant-baseline: middle; + text-align: center; + } + [data-look="neo"].mindmap-node { + filter: ${n}; + } + [data-look="neo"].mindmap-node.section-root rect, [data-look="neo"].mindmap-node.section-root path, [data-look="neo"].mindmap-node.section-root circle, [data-look="neo"].mindmap-node.section-root polygon { + fill: ${t?.includes("redux")?e.mainBkg:e.git0}; + } + [data-look="neo"].mindmap-node.section-root .text-inner-tspan { + fill: ${t?.includes("redux")?e.nodeBorder:e["cScaleLabel"+(t==="neutral"?1:0)]}; + } + ${e.useGradient&&r&&e.mainBkg?X6t(e.THEME_COLOR_LIMIT,r,e.mainBkg):""} +`},"getStyles"),TPe=K6t});var SPe={};sr(SPe,{diagram:()=>Z6t});var Z6t,wPe=F(()=>{"use strict";uPe();vPe();bPe();CPe();Z6t={get db(){return new AL},renderer:xPe,parser:cPe,styles:TPe}});var sj,APe,RPe=F(()=>{"use strict";sj=(function(){var e=s(function(k,R,M,P){for(M=M||{},P=k.length;P--;M[k[P]]=R);return M},"o"),t=[1,4],r=[1,13],n=[1,12],i=[1,15],a=[1,16],o=[1,20],l=[1,19],u=[6,7,8],h=[1,26],d=[1,24],f=[1,25],p=[6,7,11],m=[1,31],g=[6,7,11,24],y=[1,6,13,16,17,20,23],v=[1,35],x=[1,36],b=[1,6,7,11,13,16,17,20,23],T=[1,38],w={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,KANBAN:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,shapeData:15,ICON:16,CLASS:17,nodeWithId:18,nodeWithoutId:19,NODE_DSTART:20,NODE_DESCR:21,NODE_DEND:22,NODE_ID:23,SHAPE_DATA:24,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"KANBAN",11:"EOF",13:"SPACELIST",16:"ICON",17:"CLASS",20:"NODE_DSTART",21:"NODE_DESCR",22:"NODE_DEND",23:"NODE_ID",24:"SHAPE_DATA"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,3],[12,2],[12,2],[12,2],[12,1],[12,2],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[19,3],[18,1],[18,4],[15,2],[15,1]],performAction:s(function(R,M,P,N,_,A,I){var D=A.length-1;switch(_){case 6:case 7:return N;case 8:N.getLogger().trace("Stop NL ");break;case 9:N.getLogger().trace("Stop EOF ");break;case 11:N.getLogger().trace("Stop NL2 ");break;case 12:N.getLogger().trace("Stop EOF2 ");break;case 15:N.getLogger().info("Node: ",A[D-1].id),N.addNode(A[D-2].length,A[D-1].id,A[D-1].descr,A[D-1].type,A[D]);break;case 16:N.getLogger().info("Node: ",A[D].id),N.addNode(A[D-1].length,A[D].id,A[D].descr,A[D].type);break;case 17:N.getLogger().trace("Icon: ",A[D]),N.decorateNode({icon:A[D]});break;case 18:case 23:N.decorateNode({class:A[D]});break;case 19:N.getLogger().trace("SPACELIST");break;case 20:N.getLogger().trace("Node: ",A[D-1].id),N.addNode(0,A[D-1].id,A[D-1].descr,A[D-1].type,A[D]);break;case 21:N.getLogger().trace("Node: ",A[D].id),N.addNode(0,A[D].id,A[D].descr,A[D].type);break;case 22:N.decorateNode({icon:A[D]});break;case 27:N.getLogger().trace("node found ..",A[D-2]),this.$={id:A[D-1],descr:A[D-1],type:N.getType(A[D-2],A[D])};break;case 28:this.$={id:A[D],descr:A[D],type:0};break;case 29:N.getLogger().trace("node found ..",A[D-3]),this.$={id:A[D-3],descr:A[D-1],type:N.getType(A[D-2],A[D])};break;case 30:this.$=A[D-1]+A[D];break;case 31:this.$=A[D];break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:t},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:t},{6:r,7:[1,10],9:9,12:11,13:n,14:14,16:i,17:a,18:17,19:18,20:o,23:l},e(u,[2,3]),{1:[2,2]},e(u,[2,4]),e(u,[2,5]),{1:[2,6],6:r,12:21,13:n,14:14,16:i,17:a,18:17,19:18,20:o,23:l},{6:r,9:22,12:11,13:n,14:14,16:i,17:a,18:17,19:18,20:o,23:l},{6:h,7:d,10:23,11:f},e(p,[2,24],{18:17,19:18,14:27,16:[1,28],17:[1,29],20:o,23:l}),e(p,[2,19]),e(p,[2,21],{15:30,24:m}),e(p,[2,22]),e(p,[2,23]),e(g,[2,25]),e(g,[2,26]),e(g,[2,28],{20:[1,32]}),{21:[1,33]},{6:h,7:d,10:34,11:f},{1:[2,7],6:r,12:21,13:n,14:14,16:i,17:a,18:17,19:18,20:o,23:l},e(y,[2,14],{7:v,11:x}),e(b,[2,8]),e(b,[2,9]),e(b,[2,10]),e(p,[2,16],{15:37,24:m}),e(p,[2,17]),e(p,[2,18]),e(p,[2,20],{24:T}),e(g,[2,31]),{21:[1,39]},{22:[1,40]},e(y,[2,13],{7:v,11:x}),e(b,[2,11]),e(b,[2,12]),e(p,[2,15],{24:T}),e(g,[2,30]),{22:[1,41]},e(g,[2,27]),e(g,[2,29])],defaultActions:{2:[2,1],6:[2,2]},parseError:s(function(R,M){if(M.recoverable)this.trace(R);else{var P=new Error(R);throw P.hash=M,P}},"parseError"),parse:s(function(R){var M=this,P=[0],N=[],_=[null],A=[],I=this.table,D="",L=0,B=0,O=0,$=2,W=1,V=A.slice.call(arguments,1),z=Object.create(this.lexer),H={yy:{}};for(var K in this.yy)Object.prototype.hasOwnProperty.call(this.yy,K)&&(H.yy[K]=this.yy[K]);z.setInput(R,H.yy),H.yy.lexer=z,H.yy.parser=this,typeof z.yylloc>"u"&&(z.yylloc={});var J=z.yylloc;A.push(J);var ne=z.options&&z.options.ranges;typeof H.yy.parseError=="function"?this.parseError=H.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Z(Ne){P.length=P.length-2*Ne,_.length=_.length-Ne,A.length=A.length-Ne}s(Z,"popStack");function pe(){var Ne;return Ne=N.pop()||z.lex()||W,typeof Ne!="number"&&(Ne instanceof Array&&(N=Ne,Ne=N.pop()),Ne=M.symbols_[Ne]||Ne),Ne}s(pe,"lex");for(var te,fe,ce,de,Ee,we,Fe={},Ye,He,ye,Be;;){if(ce=P[P.length-1],this.defaultActions[ce]?de=this.defaultActions[ce]:((te===null||typeof te>"u")&&(te=pe()),de=I[ce]&&I[ce][te]),typeof de>"u"||!de.length||!de[0]){var Ie="";Be=[];for(Ye in I[ce])this.terminals_[Ye]&&Ye>$&&Be.push("'"+this.terminals_[Ye]+"'");z.showPosition?Ie="Parse error on line "+(L+1)+`: +`+z.showPosition()+` +Expecting `+Be.join(", ")+", got '"+(this.terminals_[te]||te)+"'":Ie="Parse error on line "+(L+1)+": Unexpected "+(te==W?"end of input":"'"+(this.terminals_[te]||te)+"'"),this.parseError(Ie,{text:z.match,token:this.terminals_[te]||te,line:z.yylineno,loc:J,expected:Be})}if(de[0]instanceof Array&&de.length>1)throw new Error("Parse Error: multiple actions possible at state: "+ce+", token: "+te);switch(de[0]){case 1:P.push(te),_.push(z.yytext),A.push(z.yylloc),P.push(de[1]),te=null,fe?(te=fe,fe=null):(B=z.yyleng,D=z.yytext,L=z.yylineno,J=z.yylloc,O>0&&O--);break;case 2:if(He=this.productions_[de[1]][1],Fe.$=_[_.length-He],Fe._$={first_line:A[A.length-(He||1)].first_line,last_line:A[A.length-1].last_line,first_column:A[A.length-(He||1)].first_column,last_column:A[A.length-1].last_column},ne&&(Fe._$.range=[A[A.length-(He||1)].range[0],A[A.length-1].range[1]]),we=this.performAction.apply(Fe,[D,B,L,H.yy,de[1],_,A].concat(V)),typeof we<"u")return we;He&&(P=P.slice(0,-1*He*2),_=_.slice(0,-1*He),A=A.slice(0,-1*He)),P.push(this.productions_[de[1]][0]),_.push(Fe.$),A.push(Fe._$),ye=I[P[P.length-2]][P[P.length-1]],P.push(ye);break;case 3:return!0}}return!0},"parse")},C=(function(){var k={EOF:1,parseError:s(function(M,P){if(this.yy.parser)this.yy.parser.parseError(M,P);else throw new Error(M)},"parseError"),setInput:s(function(R,M){return this.yy=M||this.yy||{},this._input=R,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var R=this._input[0];this.yytext+=R,this.yyleng++,this.offset++,this.match+=R,this.matched+=R;var M=R.match(/(?:\r\n?|\n).*/g);return M?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),R},"input"),unput:s(function(R){var M=R.length,P=R.split(/(?:\r\n?|\n)/g);this._input=R+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-M),this.offset-=M;var N=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),P.length-1&&(this.yylineno-=P.length-1);var _=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:P?(P.length===N.length?this.yylloc.first_column:0)+N[N.length-P.length].length-P[0].length:this.yylloc.first_column-M},this.options.ranges&&(this.yylloc.range=[_[0],_[0]+this.yyleng-M]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(R){this.unput(this.match.slice(R))},"less"),pastInput:s(function(){var R=this.matched.substr(0,this.matched.length-this.match.length);return(R.length>20?"...":"")+R.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var R=this.match;return R.length<20&&(R+=this._input.substr(0,20-R.length)),(R.substr(0,20)+(R.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var R=this.pastInput(),M=new Array(R.length+1).join("-");return R+this.upcomingInput()+` +`+M+"^"},"showPosition"),test_match:s(function(R,M){var P,N,_;if(this.options.backtrack_lexer&&(_={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(_.yylloc.range=this.yylloc.range.slice(0))),N=R[0].match(/(?:\r\n?|\n).*/g),N&&(this.yylineno+=N.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:N?N[N.length-1].length-N[N.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+R[0].length},this.yytext+=R[0],this.match+=R[0],this.matches=R,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(R[0].length),this.matched+=R[0],P=this.performAction.call(this,this.yy,this,M,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),P)return P;if(this._backtrack){for(var A in _)this[A]=_[A];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var R,M,P,N;this._more||(this.yytext="",this.match="");for(var _=this._currentRules(),A=0;A<_.length;A++)if(P=this._input.match(this.rules[_[A]]),P&&(!M||P[0].length>M[0].length)){if(M=P,N=A,this.options.backtrack_lexer){if(R=this.test_match(P,_[A]),R!==!1)return R;if(this._backtrack){M=!1;continue}else return!1}else if(!this.options.flex)break}return M?(R=this.test_match(M,_[N]),R!==!1?R:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var M=this.next();return M||this.lex()},"lex"),begin:s(function(M){this.conditionStack.push(M)},"begin"),popState:s(function(){var M=this.conditionStack.length-1;return M>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(M){return M=this.conditionStack.length-1-Math.abs(M||0),M>=0?this.conditionStack[M]:"INITIAL"},"topState"),pushState:s(function(M){this.begin(M)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(M,P,N,_){var A=_;switch(N){case 0:return this.pushState("shapeData"),P.yytext="",24;break;case 1:return this.pushState("shapeDataStr"),24;break;case 2:return this.popState(),24;break;case 3:let I=/\n\s*/g;return P.yytext=P.yytext.replace(I,"
    "),24;break;case 4:return 24;case 5:this.popState();break;case 6:return M.getLogger().trace("Found comment",P.yytext),6;break;case 7:return 8;case 8:this.begin("CLASS");break;case 9:return this.popState(),17;break;case 10:this.popState();break;case 11:M.getLogger().trace("Begin icon"),this.begin("ICON");break;case 12:return M.getLogger().trace("SPACELINE"),6;break;case 13:return 7;case 14:return 16;case 15:M.getLogger().trace("end icon"),this.popState();break;case 16:return M.getLogger().trace("Exploding node"),this.begin("NODE"),20;break;case 17:return M.getLogger().trace("Cloud"),this.begin("NODE"),20;break;case 18:return M.getLogger().trace("Explosion Bang"),this.begin("NODE"),20;break;case 19:return M.getLogger().trace("Cloud Bang"),this.begin("NODE"),20;break;case 20:return this.begin("NODE"),20;break;case 21:return this.begin("NODE"),20;break;case 22:return this.begin("NODE"),20;break;case 23:return this.begin("NODE"),20;break;case 24:return 13;case 25:return 23;case 26:return 11;case 27:this.begin("NSTR2");break;case 28:return"NODE_DESCR";case 29:this.popState();break;case 30:M.getLogger().trace("Starting NSTR"),this.begin("NSTR");break;case 31:return M.getLogger().trace("description:",P.yytext),"NODE_DESCR";break;case 32:this.popState();break;case 33:return this.popState(),M.getLogger().trace("node end ))"),"NODE_DEND";break;case 34:return this.popState(),M.getLogger().trace("node end )"),"NODE_DEND";break;case 35:return this.popState(),M.getLogger().trace("node end ...",P.yytext),"NODE_DEND";break;case 36:return this.popState(),M.getLogger().trace("node end (("),"NODE_DEND";break;case 37:return this.popState(),M.getLogger().trace("node end (-"),"NODE_DEND";break;case 38:return this.popState(),M.getLogger().trace("node end (-"),"NODE_DEND";break;case 39:return this.popState(),M.getLogger().trace("node end (("),"NODE_DEND";break;case 40:return this.popState(),M.getLogger().trace("node end (("),"NODE_DEND";break;case 41:return M.getLogger().trace("Long description:",P.yytext),21;break;case 42:return M.getLogger().trace("Long description:",P.yytext),21;break}},"anonymous"),rules:[/^(?:@\{)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^\"]+)/i,/^(?:[^}^"]+)/i,/^(?:\})/i,/^(?:\s*%%.*)/i,/^(?:kanban\b)/i,/^(?::::)/i,/^(?:.+)/i,/^(?:\n)/i,/^(?:::icon\()/i,/^(?:[\s]+[\n])/i,/^(?:[\n]+)/i,/^(?:[^\)]+)/i,/^(?:\))/i,/^(?:-\))/i,/^(?:\(-)/i,/^(?:\)\))/i,/^(?:\))/i,/^(?:\(\()/i,/^(?:\{\{)/i,/^(?:\()/i,/^(?:\[)/i,/^(?:[\s]+)/i,/^(?:[^\(\[\n\)\{\}@]+)/i,/^(?:$)/i,/^(?:["][`])/i,/^(?:[^`"]+)/i,/^(?:[`]["])/i,/^(?:["])/i,/^(?:[^"]+)/i,/^(?:["])/i,/^(?:[\)]\))/i,/^(?:[\)])/i,/^(?:[\]])/i,/^(?:\}\})/i,/^(?:\(-)/i,/^(?:-\))/i,/^(?:\(\()/i,/^(?:\()/i,/^(?:[^\)\]\(\}]+)/i,/^(?:.+(?!\(\())/i],conditions:{shapeDataEndBracket:{rules:[],inclusive:!1},shapeDataStr:{rules:[2,3],inclusive:!1},shapeData:{rules:[1,4,5],inclusive:!1},CLASS:{rules:[9,10],inclusive:!1},ICON:{rules:[14,15],inclusive:!1},NSTR2:{rules:[28,29],inclusive:!1},NSTR:{rules:[31,32],inclusive:!1},NODE:{rules:[27,30,33,34,35,36,37,38,39,40,41,42],inclusive:!1},INITIAL:{rules:[0,6,7,8,11,12,13,16,17,18,19,20,21,22,23,24,25,26],inclusive:!0}}};return k})();w.lexer=C;function S(){this.yy={}}return s(S,"Parser"),S.prototype=w,w.Parser=S,new S})();sj.parser=sj;APe=sj});var Gl,lj,oj,cj,t_t,r_t,_Pe,n_t,i_t,Ca,a_t,s_t,o_t,l_t,c_t,u_t,h_t,LPe,DPe=F(()=>{"use strict";Jt();Yr();St();Qi();P2();Gl=[],lj=[],oj=0,cj={},t_t=s(()=>{Gl=[],lj=[],oj=0,cj={}},"clear"),r_t=s(e=>{if(Gl.length===0)return null;let t=Gl[0].level,r=null;for(let n=Gl.length-1;n>=0;n--)if(Gl[n].level===t&&!r&&(r=Gl[n]),Gl[n].levell.parentId===i.id);for(let l of o){let u={id:l.id,parentId:i.id,label:xr(l.label??"",n),labelType:"markdown",isGroup:!1,ticket:l?.ticket,priority:l?.priority,assigned:l?.assigned,icon:l?.icon,shape:"kanbanItem",level:l.level,rx:5,ry:5,cssStyles:["text-align: left"]};t.push(u)}}return{nodes:t,edges:e,other:{},config:Pe()}},"getData"),i_t=s((e,t,r,n,i)=>{let a=Pe(),o=a.mindmap?.padding??mr.mindmap.padding;switch(n){case Ca.ROUNDED_RECT:case Ca.RECT:case Ca.HEXAGON:o*=2}let l={id:xr(t,a)||"kbn"+oj++,level:e,label:xr(r,a),width:a.mindmap?.maxNodeWidth??mr.mindmap.maxNodeWidth,padding:o,isGroup:!1};if(i!==void 0){let h;i.includes(` +`)?h=i+` +`:h=`{ +`+i+` +}`;let d=af(h,{schema:nf});if(d.shape&&(d.shape!==d.shape.toLowerCase()||d.shape.includes("_")))throw new Error(`No such shape: ${d.shape}. Shape names should be lowercase.`);d?.shape&&d.shape==="kanbanItem"&&(l.shape=d?.shape),d?.label&&(l.label=d?.label),d?.icon&&(l.icon=d?.icon.toString()),d?.assigned&&(l.assigned=d?.assigned.toString()),d?.ticket&&(l.ticket=d?.ticket.toString()),d?.priority&&(l.priority=d?.priority)}let u=r_t(e);u?l.parentId=u.id||"kbn"+oj++:lj.push(l),Gl.push(l)},"addNode"),Ca={DEFAULT:0,NO_BORDER:0,ROUNDED_RECT:1,RECT:2,CIRCLE:3,CLOUD:4,BANG:5,HEXAGON:6},a_t=s((e,t)=>{switch(se.debug("In get type",e,t),e){case"[":return Ca.RECT;case"(":return t===")"?Ca.ROUNDED_RECT:Ca.CLOUD;case"((":return Ca.CIRCLE;case")":return Ca.CLOUD;case"))":return Ca.BANG;case"{{":return Ca.HEXAGON;default:return Ca.DEFAULT}},"getType"),s_t=s((e,t)=>{cj[e]=t},"setElementForId"),o_t=s(e=>{if(!e)return;let t=Pe(),r=Gl[Gl.length-1];e.icon&&(r.icon=xr(e.icon,t)),e.class&&(r.cssClasses=xr(e.class,t))},"decorateNode"),l_t=s(e=>{switch(e){case Ca.DEFAULT:return"no-border";case Ca.RECT:return"rect";case Ca.ROUNDED_RECT:return"rounded-rect";case Ca.CIRCLE:return"circle";case Ca.CLOUD:return"cloud";case Ca.BANG:return"bang";case Ca.HEXAGON:return"hexgon";default:return"no-border"}},"type2Str"),c_t=s(()=>se,"getLogger"),u_t=s(e=>cj[e],"getElementById"),h_t={clear:t_t,addNode:i_t,getSections:_Pe,getData:n_t,nodeType:Ca,getType:a_t,setElementForId:s_t,decorateNode:o_t,type2Str:l_t,getLogger:c_t,getElementById:u_t},LPe=h_t});var d_t,IPe,MPe=F(()=>{"use strict";Jt();St();cs();Vn();Qi();B2();Lm();d_t=s(async(e,t,r,n)=>{se.debug(`Rendering kanban diagram +`+e);let a=n.db.getData(),o=Pe();o.htmlLabels=!1;let l=wn(t);for(let v of a.nodes)v.domId=`${t}-${v.id}`;let u=l.append("g");u.attr("class","sections");let h=l.append("g");h.attr("class","items");let d=a.nodes.filter(v=>v.isGroup),f=0,p=10,m=[],g=25;for(let v of d){let x=o?.kanban?.sectionWidth||200;f=f+1,v.x=x*f+(f-1)*p/2,v.width=x,v.y=0,v.height=x*3,v.rx=5,v.ry=5,v.cssClasses=v.cssClasses+" section-"+f;let b=await sf(u,v);g=Math.max(g,b?.labelBBox?.height),m.push(b)}let y=0;for(let v of d){let x=m[y];y=y+1;let b=o?.kanban?.sectionWidth||200,T=-b*3/2+g,w=T,C=a.nodes.filter(R=>R.parentId===v.id);for(let R of C){if(R.isGroup)throw new Error("Groups within groups are not allowed in Kanban diagrams");R.x=v.x,R.width=b-1.5*p;let P=(await Eh(h,R,{config:o})).node().getBBox();R.y=w+P.height/2,await au(R),w=R.y+P.height/2+p/2}let S=x.cluster.select("rect"),k=Math.max(w-T+3*p,50)+(g-25);S.attr("height",k)}ml(void 0,l,o.mindmap?.padding??mr.kanban.padding,o.mindmap?.useMaxWidth??mr.kanban.useMaxWidth)},"draw"),IPe={draw:d_t}});var f_t,p_t,NPe,PPe=F(()=>{"use strict";Xi();U1();f_t=s(e=>{let t="";for(let n=0;ne.darkMode?Je(n,i):Qe(n,i),"adjuster");for(let n=0;n` + .edge { + stroke-width: 3; + } + ${f_t(e)} + .section-root rect, .section-root path, .section-root circle, .section-root polygon { + fill: ${e.git0}; + } + .section-root text { + fill: ${e.gitBranchLabel0}; + } + .icon-container { + height:100%; + display: flex; + justify-content: center; + align-items: center; + } + .edge { + fill: none; + } + .cluster-label, .label { + color: ${e.textColor}; + fill: ${e.textColor}; + } + .kanban-label { + dy: 1em; + alignment-baseline: middle; + text-anchor: middle; + dominant-baseline: middle; + text-align: center; + } + ${Ru()} +`,"getStyles"),NPe=p_t});var OPe={};sr(OPe,{diagram:()=>m_t});var m_t,BPe=F(()=>{"use strict";RPe();DPe();MPe();PPe();m_t={db:LPe,renderer:IPe,parser:APe,styles:NPe}});var uj,US,GPe=F(()=>{"use strict";uj=(function(){var e=s(function(l,u,h,d){for(h=h||{},d=l.length;d--;h[l[d]]=u);return h},"o"),t=[1,9],r=[1,10],n=[1,5,10,12],i={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,SANKEY:4,NEWLINE:5,csv:6,opt_eof:7,record:8,csv_tail:9,EOF:10,"field[source]":11,COMMA:12,"field[target]":13,"field[value]":14,field:15,escaped:16,non_escaped:17,DQUOTE:18,ESCAPED_TEXT:19,NON_ESCAPED_TEXT:20,$accept:0,$end:1},terminals_:{2:"error",4:"SANKEY",5:"NEWLINE",10:"EOF",11:"field[source]",12:"COMMA",13:"field[target]",14:"field[value]",18:"DQUOTE",19:"ESCAPED_TEXT",20:"NON_ESCAPED_TEXT"},productions_:[0,[3,4],[6,2],[9,2],[9,0],[7,1],[7,0],[8,5],[15,1],[15,1],[16,3],[17,1]],performAction:s(function(u,h,d,f,p,m,g){var y=m.length-1;switch(p){case 7:let v=f.findOrCreateNode(m[y-4].trim().replaceAll('""','"')),x=f.findOrCreateNode(m[y-2].trim().replaceAll('""','"')),b=parseFloat(m[y].trim());f.addLink(v,x,b);break;case 8:case 9:case 11:this.$=m[y];break;case 10:this.$=m[y-1];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},{5:[1,3]},{6:4,8:5,15:6,16:7,17:8,18:t,20:r},{1:[2,6],7:11,10:[1,12]},e(r,[2,4],{9:13,5:[1,14]}),{12:[1,15]},e(n,[2,8]),e(n,[2,9]),{19:[1,16]},e(n,[2,11]),{1:[2,1]},{1:[2,5]},e(r,[2,2]),{6:17,8:5,15:6,16:7,17:8,18:t,20:r},{15:18,16:7,17:8,18:t,20:r},{18:[1,19]},e(r,[2,3]),{12:[1,20]},e(n,[2,10]),{15:21,16:7,17:8,18:t,20:r},e([1,5,10],[2,7])],defaultActions:{11:[2,1],12:[2,5]},parseError:s(function(u,h){if(h.recoverable)this.trace(u);else{var d=new Error(u);throw d.hash=h,d}},"parseError"),parse:s(function(u){var h=this,d=[0],f=[],p=[null],m=[],g=this.table,y="",v=0,x=0,b=0,T=2,w=1,C=m.slice.call(arguments,1),S=Object.create(this.lexer),k={yy:{}};for(var R in this.yy)Object.prototype.hasOwnProperty.call(this.yy,R)&&(k.yy[R]=this.yy[R]);S.setInput(u,k.yy),k.yy.lexer=S,k.yy.parser=this,typeof S.yylloc>"u"&&(S.yylloc={});var M=S.yylloc;m.push(M);var P=S.options&&S.options.ranges;typeof k.yy.parseError=="function"?this.parseError=k.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function N(J){d.length=d.length-2*J,p.length=p.length-J,m.length=m.length-J}s(N,"popStack");function _(){var J;return J=f.pop()||S.lex()||w,typeof J!="number"&&(J instanceof Array&&(f=J,J=f.pop()),J=h.symbols_[J]||J),J}s(_,"lex");for(var A,I,D,L,B,O,$={},W,V,z,H;;){if(D=d[d.length-1],this.defaultActions[D]?L=this.defaultActions[D]:((A===null||typeof A>"u")&&(A=_()),L=g[D]&&g[D][A]),typeof L>"u"||!L.length||!L[0]){var K="";H=[];for(W in g[D])this.terminals_[W]&&W>T&&H.push("'"+this.terminals_[W]+"'");S.showPosition?K="Parse error on line "+(v+1)+`: +`+S.showPosition()+` +Expecting `+H.join(", ")+", got '"+(this.terminals_[A]||A)+"'":K="Parse error on line "+(v+1)+": Unexpected "+(A==w?"end of input":"'"+(this.terminals_[A]||A)+"'"),this.parseError(K,{text:S.match,token:this.terminals_[A]||A,line:S.yylineno,loc:M,expected:H})}if(L[0]instanceof Array&&L.length>1)throw new Error("Parse Error: multiple actions possible at state: "+D+", token: "+A);switch(L[0]){case 1:d.push(A),p.push(S.yytext),m.push(S.yylloc),d.push(L[1]),A=null,I?(A=I,I=null):(x=S.yyleng,y=S.yytext,v=S.yylineno,M=S.yylloc,b>0&&b--);break;case 2:if(V=this.productions_[L[1]][1],$.$=p[p.length-V],$._$={first_line:m[m.length-(V||1)].first_line,last_line:m[m.length-1].last_line,first_column:m[m.length-(V||1)].first_column,last_column:m[m.length-1].last_column},P&&($._$.range=[m[m.length-(V||1)].range[0],m[m.length-1].range[1]]),O=this.performAction.apply($,[y,x,v,k.yy,L[1],p,m].concat(C)),typeof O<"u")return O;V&&(d=d.slice(0,-1*V*2),p=p.slice(0,-1*V),m=m.slice(0,-1*V)),d.push(this.productions_[L[1]][0]),p.push($.$),m.push($._$),z=g[d[d.length-2]][d[d.length-1]],d.push(z);break;case 3:return!0}}return!0},"parse")},a=(function(){var l={EOF:1,parseError:s(function(h,d){if(this.yy.parser)this.yy.parser.parseError(h,d);else throw new Error(h)},"parseError"),setInput:s(function(u,h){return this.yy=h||this.yy||{},this._input=u,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var u=this._input[0];this.yytext+=u,this.yyleng++,this.offset++,this.match+=u,this.matched+=u;var h=u.match(/(?:\r\n?|\n).*/g);return h?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),u},"input"),unput:s(function(u){var h=u.length,d=u.split(/(?:\r\n?|\n)/g);this._input=u+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-h),this.offset-=h;var f=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),d.length-1&&(this.yylineno-=d.length-1);var p=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:d?(d.length===f.length?this.yylloc.first_column:0)+f[f.length-d.length].length-d[0].length:this.yylloc.first_column-h},this.options.ranges&&(this.yylloc.range=[p[0],p[0]+this.yyleng-h]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(u){this.unput(this.match.slice(u))},"less"),pastInput:s(function(){var u=this.matched.substr(0,this.matched.length-this.match.length);return(u.length>20?"...":"")+u.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var u=this.match;return u.length<20&&(u+=this._input.substr(0,20-u.length)),(u.substr(0,20)+(u.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var u=this.pastInput(),h=new Array(u.length+1).join("-");return u+this.upcomingInput()+` +`+h+"^"},"showPosition"),test_match:s(function(u,h){var d,f,p;if(this.options.backtrack_lexer&&(p={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(p.yylloc.range=this.yylloc.range.slice(0))),f=u[0].match(/(?:\r\n?|\n).*/g),f&&(this.yylineno+=f.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:f?f[f.length-1].length-f[f.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+u[0].length},this.yytext+=u[0],this.match+=u[0],this.matches=u,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(u[0].length),this.matched+=u[0],d=this.performAction.call(this,this.yy,this,h,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),d)return d;if(this._backtrack){for(var m in p)this[m]=p[m];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var u,h,d,f;this._more||(this.yytext="",this.match="");for(var p=this._currentRules(),m=0;mh[0].length)){if(h=d,f=m,this.options.backtrack_lexer){if(u=this.test_match(d,p[m]),u!==!1)return u;if(this._backtrack){h=!1;continue}else return!1}else if(!this.options.flex)break}return h?(u=this.test_match(h,p[f]),u!==!1?u:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var h=this.next();return h||this.lex()},"lex"),begin:s(function(h){this.conditionStack.push(h)},"begin"),popState:s(function(){var h=this.conditionStack.length-1;return h>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(h){return h=this.conditionStack.length-1-Math.abs(h||0),h>=0?this.conditionStack[h]:"INITIAL"},"topState"),pushState:s(function(h){this.begin(h)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(h,d,f,p){var m=p;switch(f){case 0:return this.pushState("csv"),4;break;case 1:return this.pushState("csv"),4;break;case 2:return 10;case 3:return 5;case 4:return 12;case 5:return this.pushState("escaped_text"),18;break;case 6:return 20;case 7:return this.popState("escaped_text"),18;break;case 8:return 19}},"anonymous"),rules:[/^(?:sankey-beta\b)/i,/^(?:sankey\b)/i,/^(?:$)/i,/^(?:((\u000D\u000A)|(\u000A)))/i,/^(?:(\u002C))/i,/^(?:(\u0022))/i,/^(?:([\u0020-\u0021\u0023-\u002B\u002D-\u007E])*)/i,/^(?:(\u0022)(?!(\u0022)))/i,/^(?:(([\u0020-\u0021\u0023-\u002B\u002D-\u007E])|(\u002C)|(\u000D)|(\u000A)|(\u0022)(\u0022))*)/i],conditions:{csv:{rules:[2,3,4,5,6,7,8],inclusive:!1},escaped_text:{rules:[7,8],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8],inclusive:!0}}};return l})();i.lexer=a;function o(){this.yy={}}return s(o,"Parser"),o.prototype=i,i.Parser=o,new o})();uj.parser=uj;US=uj});var _L,LL,RL,x_t,hj,b_t,dj,T_t,C_t,S_t,w_t,zPe,VPe=F(()=>{"use strict";Jt();Yr();$n();_L=[],LL=[],RL=new Map,x_t=s(()=>{_L=[],LL=[],RL=new Map,Tr()},"clear"),hj=class{constructor(t,r,n=0){this.source=t;this.target=r;this.value=n}static{s(this,"SankeyLink")}},b_t=s((e,t,r)=>{_L.push(new hj(e,t,r))},"addLink"),dj=class{constructor(t){this.ID=t}static{s(this,"SankeyNode")}},T_t=s(e=>{e=Tt.sanitizeText(e,Pe());let t=RL.get(e);return t===void 0&&(t=new dj(e),RL.set(e,t),LL.push(t)),t},"findOrCreateNode"),C_t=s(()=>LL,"getNodes"),S_t=s(()=>_L,"getLinks"),w_t=s(()=>({nodes:LL.map(e=>({id:e.ID})),links:_L.map(e=>({source:e.source.ID,target:e.target.ID,value:e.value}))}),"getGraph"),zPe={nodesMap:RL,getConfig:s(()=>Pe().sankey,"getConfig"),getNodes:C_t,getLinks:S_t,getGraph:w_t,addLink:b_t,findOrCreateNode:T_t,getAccTitle:Ir,setAccTitle:Lr,getAccDescription:Nr,setAccDescription:Mr,getDiagramTitle:Pr,setDiagramTitle:Gr,clear:x_t}});function YS(e,t){let r;if(t===void 0)for(let n of e)n!=null&&(r=n)&&(r=n);else{let n=-1;for(let i of e)(i=t(i,++n,e))!=null&&(r=i)&&(r=i)}return r}var WPe=F(()=>{"use strict";s(YS,"max")});function sx(e,t){let r;if(t===void 0)for(let n of e)n!=null&&(r>n||r===void 0&&n>=n)&&(r=n);else{let n=-1;for(let i of e)(i=t(i,++n,e))!=null&&(r>i||r===void 0&&i>=i)&&(r=i)}return r}var qPe=F(()=>{"use strict";s(sx,"min")});function ox(e,t){let r=0;if(t===void 0)for(let n of e)(n=+n)&&(r+=n);else{let n=-1;for(let i of e)(i=+t(i,++n,e))&&(r+=i)}return r}var HPe=F(()=>{"use strict";s(ox,"sum")});var fj=F(()=>{"use strict";WPe();qPe();HPe()});function k_t(e){return e.target.depth}function pj(e){return e.depth}function mj(e,t){return t-1-e.height}function jS(e,t){return e.sourceLinks.length?e.depth:t-1}function gj(e){return e.targetLinks.length?e.depth:e.sourceLinks.length?sx(e.sourceLinks,k_t)-1:0}var yj=F(()=>{"use strict";fj();s(k_t,"targetDepth");s(pj,"left");s(mj,"right");s(jS,"justify");s(gj,"center")});function lx(e){return function(){return e}}var UPe=F(()=>{"use strict";s(lx,"constant")});function YPe(e,t){return DL(e.source,t.source)||e.index-t.index}function jPe(e,t){return DL(e.target,t.target)||e.index-t.index}function DL(e,t){return e.y0-t.y0}function vj(e){return e.value}function E_t(e){return e.index}function A_t(e){return e.nodes}function R_t(e){return e.links}function XPe(e,t){let r=e.get(t);if(!r)throw new Error("missing: "+t);return r}function KPe({nodes:e}){for(let t of e){let r=t.y0,n=r;for(let i of t.sourceLinks)i.y0=r+i.width/2,r+=i.width;for(let i of t.targetLinks)i.y1=n+i.width/2,n+=i.width}}function IL(){let e=0,t=0,r=1,n=1,i=24,a=8,o,l=E_t,u=jS,h,d,f=A_t,p=R_t,m=6;function g(){let D={nodes:f.apply(null,arguments),links:p.apply(null,arguments)};return y(D),v(D),x(D),b(D),C(D),KPe(D),D}s(g,"sankey"),g.update=function(D){return KPe(D),D},g.nodeId=function(D){return arguments.length?(l=typeof D=="function"?D:lx(D),g):l},g.nodeAlign=function(D){return arguments.length?(u=typeof D=="function"?D:lx(D),g):u},g.nodeSort=function(D){return arguments.length?(h=D,g):h},g.nodeWidth=function(D){return arguments.length?(i=+D,g):i},g.nodePadding=function(D){return arguments.length?(a=o=+D,g):a},g.nodes=function(D){return arguments.length?(f=typeof D=="function"?D:lx(D),g):f},g.links=function(D){return arguments.length?(p=typeof D=="function"?D:lx(D),g):p},g.linkSort=function(D){return arguments.length?(d=D,g):d},g.size=function(D){return arguments.length?(e=t=0,r=+D[0],n=+D[1],g):[r-e,n-t]},g.extent=function(D){return arguments.length?(e=+D[0][0],r=+D[1][0],t=+D[0][1],n=+D[1][1],g):[[e,t],[r,n]]},g.iterations=function(D){return arguments.length?(m=+D,g):m};function y({nodes:D,links:L}){for(let[O,$]of D.entries())$.index=O,$.sourceLinks=[],$.targetLinks=[];let B=new Map(D.map((O,$)=>[l(O,$,D),O]));for(let[O,$]of L.entries()){$.index=O;let{source:W,target:V}=$;typeof W!="object"&&(W=$.source=XPe(B,W)),typeof V!="object"&&(V=$.target=XPe(B,V)),W.sourceLinks.push($),V.targetLinks.push($)}if(d!=null)for(let{sourceLinks:O,targetLinks:$}of D)O.sort(d),$.sort(d)}s(y,"computeNodeLinks");function v({nodes:D}){for(let L of D)L.value=L.fixedValue===void 0?Math.max(ox(L.sourceLinks,vj),ox(L.targetLinks,vj)):L.fixedValue}s(v,"computeNodeValues");function x({nodes:D}){let L=D.length,B=new Set(D),O=new Set,$=0;for(;B.size;){for(let W of B){W.depth=$;for(let{target:V}of W.sourceLinks)O.add(V)}if(++$>L)throw new Error("circular link");B=O,O=new Set}}s(x,"computeNodeDepths");function b({nodes:D}){let L=D.length,B=new Set(D),O=new Set,$=0;for(;B.size;){for(let W of B){W.height=$;for(let{source:V}of W.targetLinks)O.add(V)}if(++$>L)throw new Error("circular link");B=O,O=new Set}}s(b,"computeNodeHeights");function T({nodes:D}){let L=YS(D,$=>$.depth)+1,B=(r-e-i)/(L-1),O=new Array(L);for(let $ of D){let W=Math.max(0,Math.min(L-1,Math.floor(u.call(null,$,L))));$.layer=W,$.x0=e+W*B,$.x1=$.x0+i,O[W]?O[W].push($):O[W]=[$]}if(h)for(let $ of O)$.sort(h);return O}s(T,"computeNodeLayers");function w(D){let L=sx(D,B=>(n-t-(B.length-1)*o)/ox(B,vj));for(let B of D){let O=t;for(let $ of B){$.y0=O,$.y1=O+$.value*L,O=$.y1+o;for(let W of $.sourceLinks)W.width=W.value*L}O=(n-O+o)/(B.length+1);for(let $=0;$B.length)-1)),w(L);for(let B=0;B0))continue;let K=(z/H-V.y0)*L;V.y0+=K,V.y1+=K,N(V)}h===void 0&&W.sort(DL),R(W,B)}}s(S,"relaxLeftToRight");function k(D,L,B){for(let O=D.length,$=O-2;$>=0;--$){let W=D[$];for(let V of W){let z=0,H=0;for(let{target:J,value:ne}of V.sourceLinks){let Z=ne*(J.layer-V.layer);z+=I(V,J)*Z,H+=Z}if(!(H>0))continue;let K=(z/H-V.y0)*L;V.y0+=K,V.y1+=K,N(V)}h===void 0&&W.sort(DL),R(W,B)}}s(k,"relaxRightToLeft");function R(D,L){let B=D.length>>1,O=D[B];P(D,O.y0-o,B-1,L),M(D,O.y1+o,B+1,L),P(D,n,D.length-1,L),M(D,t,0,L)}s(R,"resolveCollisions");function M(D,L,B,O){for(;B1e-6&&($.y0+=W,$.y1+=W),L=$.y1+o}}s(M,"resolveCollisionsTopToBottom");function P(D,L,B,O){for(;B>=0;--B){let $=D[B],W=($.y1-L)*O;W>1e-6&&($.y0-=W,$.y1-=W),L=$.y0-o}}s(P,"resolveCollisionsBottomToTop");function N({sourceLinks:D,targetLinks:L}){if(d===void 0){for(let{source:{sourceLinks:B}}of L)B.sort(jPe);for(let{target:{targetLinks:B}}of D)B.sort(YPe)}}s(N,"reorderNodeLinks");function _(D){if(d===void 0)for(let{sourceLinks:L,targetLinks:B}of D)L.sort(jPe),B.sort(YPe)}s(_,"reorderLinks");function A(D,L){let B=D.y0-(D.sourceLinks.length-1)*o/2;for(let{target:O,width:$}of D.sourceLinks){if(O===L)break;B+=$+o}for(let{source:O,width:$}of L.targetLinks){if(O===D)break;B-=$}return B}s(A,"targetTop");function I(D,L){let B=L.y0-(L.targetLinks.length-1)*o/2;for(let{source:O,width:$}of L.targetLinks){if(O===D)break;B+=$+o}for(let{target:O,width:$}of D.sourceLinks){if(O===L)break;B-=$}return B}return s(I,"sourceTop"),g}var ZPe=F(()=>{"use strict";fj();yj();UPe();s(YPe,"ascendingSourceBreadth");s(jPe,"ascendingTargetBreadth");s(DL,"ascendingBreadth");s(vj,"value");s(E_t,"defaultId");s(A_t,"defaultNodes");s(R_t,"defaultLinks");s(XPe,"find");s(KPe,"computeLinkBreadths");s(IL,"Sankey")});function Tj(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function QPe(){return new Tj}var xj,bj,L0,__t,Cj,JPe=F(()=>{"use strict";xj=Math.PI,bj=2*xj,L0=1e-6,__t=bj-L0;s(Tj,"Path");s(QPe,"path");Tj.prototype=QPe.prototype={constructor:Tj,moveTo:s(function(e,t){this._+="M"+(this._x0=this._x1=+e)+","+(this._y0=this._y1=+t)},"moveTo"),closePath:s(function(){this._x1!==null&&(this._x1=this._x0,this._y1=this._y0,this._+="Z")},"closePath"),lineTo:s(function(e,t){this._+="L"+(this._x1=+e)+","+(this._y1=+t)},"lineTo"),quadraticCurveTo:s(function(e,t,r,n){this._+="Q"+ +e+","+ +t+","+(this._x1=+r)+","+(this._y1=+n)},"quadraticCurveTo"),bezierCurveTo:s(function(e,t,r,n,i,a){this._+="C"+ +e+","+ +t+","+ +r+","+ +n+","+(this._x1=+i)+","+(this._y1=+a)},"bezierCurveTo"),arcTo:s(function(e,t,r,n,i){e=+e,t=+t,r=+r,n=+n,i=+i;var a=this._x1,o=this._y1,l=r-e,u=n-t,h=a-e,d=o-t,f=h*h+d*d;if(i<0)throw new Error("negative radius: "+i);if(this._x1===null)this._+="M"+(this._x1=e)+","+(this._y1=t);else if(f>L0)if(!(Math.abs(d*l-u*h)>L0)||!i)this._+="L"+(this._x1=e)+","+(this._y1=t);else{var p=r-a,m=n-o,g=l*l+u*u,y=p*p+m*m,v=Math.sqrt(g),x=Math.sqrt(f),b=i*Math.tan((xj-Math.acos((g+f-y)/(2*v*x)))/2),T=b/x,w=b/v;Math.abs(T-1)>L0&&(this._+="L"+(e+T*h)+","+(t+T*d)),this._+="A"+i+","+i+",0,0,"+ +(d*p>h*m)+","+(this._x1=e+w*l)+","+(this._y1=t+w*u)}},"arcTo"),arc:s(function(e,t,r,n,i,a){e=+e,t=+t,r=+r,a=!!a;var o=r*Math.cos(n),l=r*Math.sin(n),u=e+o,h=t+l,d=1^a,f=a?n-i:i-n;if(r<0)throw new Error("negative radius: "+r);this._x1===null?this._+="M"+u+","+h:(Math.abs(this._x1-u)>L0||Math.abs(this._y1-h)>L0)&&(this._+="L"+u+","+h),r&&(f<0&&(f=f%bj+bj),f>__t?this._+="A"+r+","+r+",0,1,"+d+","+(e-o)+","+(t-l)+"A"+r+","+r+",0,1,"+d+","+(this._x1=u)+","+(this._y1=h):f>L0&&(this._+="A"+r+","+r+",0,"+ +(f>=xj)+","+d+","+(this._x1=e+r*Math.cos(i))+","+(this._y1=t+r*Math.sin(i))))},"arc"),rect:s(function(e,t,r,n){this._+="M"+(this._x0=this._x1=+e)+","+(this._y0=this._y1=+t)+"h"+ +r+"v"+ +n+"h"+-r+"Z"},"rect"),toString:s(function(){return this._},"toString")};Cj=QPe});var eOe=F(()=>{"use strict";JPe()});function ML(e){return s(function(){return e},"constant")}var tOe=F(()=>{"use strict";s(ML,"default")});function rOe(e){return e[0]}function nOe(e){return e[1]}var iOe=F(()=>{"use strict";s(rOe,"x");s(nOe,"y")});var aOe,sOe=F(()=>{"use strict";aOe=Array.prototype.slice});function L_t(e){return e.source}function D_t(e){return e.target}function I_t(e){var t=L_t,r=D_t,n=rOe,i=nOe,a=null;function o(){var l,u=aOe.call(arguments),h=t.apply(this,u),d=r.apply(this,u);if(a||(a=l=Cj()),e(a,+n.apply(this,(u[0]=h,u)),+i.apply(this,u),+n.apply(this,(u[0]=d,u)),+i.apply(this,u)),l)return a=null,l+""||null}return s(o,"link"),o.source=function(l){return arguments.length?(t=l,o):t},o.target=function(l){return arguments.length?(r=l,o):r},o.x=function(l){return arguments.length?(n=typeof l=="function"?l:ML(+l),o):n},o.y=function(l){return arguments.length?(i=typeof l=="function"?l:ML(+l),o):i},o.context=function(l){return arguments.length?(a=l??null,o):a},o}function M_t(e,t,r,n,i){e.moveTo(t,r),e.bezierCurveTo(t=(t+n)/2,r,t,i,n,i)}function Sj(){return I_t(M_t)}var oOe=F(()=>{"use strict";eOe();sOe();tOe();iOe();s(L_t,"linkSource");s(D_t,"linkTarget");s(I_t,"link");s(M_t,"curveHorizontal");s(Sj,"linkHorizontal")});var lOe=F(()=>{"use strict";oOe()});function N_t(e){return[e.source.x1,e.y0]}function P_t(e){return[e.target.x0,e.y1]}function NL(){return Sj().source(N_t).target(P_t)}var cOe=F(()=>{"use strict";lOe();s(N_t,"horizontalSource");s(P_t,"horizontalTarget");s(NL,"default")});var uOe=F(()=>{"use strict";ZPe();yj();cOe()});var XS,hOe=F(()=>{"use strict";XS=class e{static{s(this,"Uid")}static{this.count=0}static next(t){return new e(t+ ++e.count)}constructor(t){this.id=t,this.href=`#${t}`}toString(){return"url("+this.href+")"}}});var O_t,B_t,$_t,dOe,fOe=F(()=>{"use strict";Jt();Wr();uOe();Vn();hOe();O_t={left:pj,right:mj,center:gj,justify:jS},B_t=s(e=>{let t=0,r=0;for(let n of e){let i=n.value??0;i>t&&(t=i,r=n.layer??0)}return r},"findCentralNodeLayer"),$_t=s(function(e,t,r,n){let{securityLevel:i,sankey:a}=Pe(),o=pk.sankey,l;i==="sandbox"&&(l=ut("#i"+t));let u=i==="sandbox"?ut(l.nodes()[0].contentDocument.body):ut("body"),h=i==="sandbox"?u.select(`[id="${t}"]`):ut(`[id="${t}"]`),d=a?.width??o.width,f=a?.height??o.width,p=a?.useMaxWidth??o.useMaxWidth,m=a?.nodeAlignment??o.nodeAlignment,g=a?.prefix??o.prefix,y=a?.suffix??o.suffix,v=a?.showValues??o.showValues,x=a?.nodeWidth??o.nodeWidth??10,b=a?.nodePadding??o.nodePadding??12,T=a?.labelStyle??o.labelStyle??"legacy",w=a?.nodeColors??{},C=n.db.getGraph(),S=O_t[m];IL().nodeId(O=>O.id).nodeWidth(x).nodePadding(b+(v?15:0)).nodeAlign(S).extent([[0,0],[d,f]])(C);let R=B_t(C.nodes),M=Go(pN),P=s(O=>w[O]??M(O),"getNodeColor");h.append("g").attr("class","nodes").selectAll(".node").data(C.nodes).join("g").attr("class","node").attr("id",O=>(O.uid=XS.next("node-")).id).attr("transform",function(O){return"translate("+O.x0+","+O.y0+")"}).attr("x",O=>O.x0).attr("y",O=>O.y0).append("rect").attr("height",O=>O.y1-O.y0).attr("width",O=>O.x1-O.x0).attr("fill",O=>P(O.id));let N=s(({id:O,value:$})=>v?`${O} +${g}${Math.round($*100)/100}${y}`:O,"getText"),_=s(O=>T==="outlined"?(O.layer??0)A.selectAll(O?`.${O}`:"text").data(C.nodes).join("text").attr("class",O??null).attr("x",$=>_($).x).attr("y",$=>($.y1+$.y0)/2).attr("dy",`${v?"0":"0.35"}em`).attr("text-anchor",$=>_($).anchor).text(N),"appendLabel");T==="outlined"?(I("sankey-label-bg"),I("sankey-label-fg")):I();let D=h.append("g").attr("class","links").attr("fill","none").attr("stroke-opacity",.5).selectAll(".link").data(C.links).join("g").attr("class","link").style("mix-blend-mode","multiply"),L=a?.linkColor??"gradient";if(L==="gradient"){let O=D.append("linearGradient").attr("id",$=>($.uid=XS.next("linearGradient-")).id).attr("gradientUnits","userSpaceOnUse").attr("x1",$=>$.source.x1).attr("x2",$=>$.target.x0);O.append("stop").attr("offset","0%").attr("stop-color",$=>P($.source.id)),O.append("stop").attr("offset","100%").attr("stop-color",$=>P($.target.id))}let B;switch(L){case"gradient":B=s(O=>O.uid,"coloring");break;case"source":B=s(O=>P(O.source.id),"coloring");break;case"target":B=s(O=>P(O.target.id),"coloring");break;default:B=L}D.append("path").attr("d",NL()).attr("stroke",B).attr("stroke-width",O=>Math.max(1,O.width)),ml(void 0,h,0,p)},"draw"),dOe={draw:$_t}});var pOe,mOe=F(()=>{"use strict";pOe=s(e=>e.replaceAll(/^[^\S\n\r]+|[^\S\n\r]+$/g,"").replaceAll(/([\n\r])+/g,` +`).trim(),"prepareTextForParsing")});var F_t,gOe,yOe=F(()=>{"use strict";F_t=s(e=>`.label { + font-family: ${e.fontFamily}; + } + + .node-labels { + font-family: ${e.fontFamily}; + } + + /* Outlined label style - background stroke for better readability */ + .sankey-label-bg { + stroke: ${e.mainBkg||e.background||"#fff"}; + stroke-width: 4px; + stroke-linejoin: round; + paint-order: stroke; + } + + /* Foreground label text */ + .sankey-label-fg { + fill: ${e.textColor}; + } + + /* Node styling */ + .node rect { + shape-rendering: crispEdges; + } + + /* Link styling */ + .link { + fill: none; + stroke-opacity: 0.5; + mix-blend-mode: multiply; + } +`,"getStyles"),gOe=F_t});var vOe={};sr(vOe,{diagram:()=>z_t});var G_t,z_t,xOe=F(()=>{"use strict";GPe();VPe();fOe();mOe();yOe();G_t=US.parse.bind(US);US.parse=e=>G_t(pOe(e));z_t={styles:gOe,parser:US,db:zPe,renderer:dOe}});var H_t,cx,wj=F(()=>{"use strict";br();Qi();er();$n();H_t=mr.packet,cx=class{constructor(){this.packet=[];this.setAccTitle=Lr;this.getAccTitle=Ir;this.setDiagramTitle=Gr;this.getDiagramTitle=Pr;this.getAccDescription=Nr;this.setAccDescription=Mr}static{s(this,"PacketDB")}getConfig(){let t=Xr({...H_t,...Lt().packet});return t.showBits&&(t.paddingY+=10),t}getPacket(){return this.packet}pushWord(t){t.length>0&&this.packet.push(t)}clear(){Tr(),this.packet=[]}}});var U_t,Y_t,j_t,kj,COe=F(()=>{"use strict";ls();St();to();wj();U_t=1e4,Y_t=s((e,t)=>{Hn(e,t);let r=-1,n=[],i=1,{bitsPerRow:a}=t.getConfig();for(let{start:o,end:l,bits:u,label:h}of e.blocks){if(o!==void 0&&l!==void 0&&l{if(e.start===void 0)throw new Error("start should have been set during first phase");if(e.end===void 0)throw new Error("end should have been set during first phase");if(e.start>e.end)throw new Error(`Block start ${e.start} is greater than block end ${e.end}.`);if(e.end+1<=t*r)return[e,void 0];let n=t*r-1,i=t*r;return[{start:e.start,end:n,label:e.label,bits:n-e.start},{start:i,end:e.end,label:e.label,bits:e.end-i}]},"getNextFittingBlock"),kj={parser:{yy:void 0},parse:s(async e=>{let t=await Li("packet",e),r=kj.parser?.yy;if(!(r instanceof cx))throw new Error("parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");se.debug(t),Y_t(t,r)},"parse")}});var X_t,K_t,SOe,wOe=F(()=>{"use strict";cs();Vn();X_t=s((e,t,r,n)=>{let i=n.db,a=i.getConfig(),{rowHeight:o,paddingY:l,bitWidth:u,bitsPerRow:h}=a,d=i.getPacket(),f=i.getDiagramTitle(),p=o+l,m=p*(d.length+1)-(f?0:o),g=u*h+2,y=wn(t);y.attr("viewBox",`0 0 ${g} ${m}`),jr(y,m,g,a.useMaxWidth);for(let[v,x]of d.entries())K_t(y,x,v,a);y.append("text").text(f).attr("x",g/2).attr("y",m-p/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),K_t=s((e,t,r,{rowHeight:n,paddingX:i,paddingY:a,bitWidth:o,bitsPerRow:l,showBits:u})=>{let h=e.append("g"),d=r*(n+a)+a;for(let f of t){let p=f.start%l*o+1,m=(f.end-f.start+1)*o-i;if(h.append("rect").attr("x",p).attr("y",d).attr("width",m).attr("height",n).attr("class","packetBlock"),h.append("text").attr("x",p+m/2).attr("y",d+n/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(f.label),!u)continue;let g=f.end===f.start,y=d-2;h.append("text").attr("x",p+(g?m/2:0)).attr("y",y).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",g?"middle":"start").text(f.start),g||h.append("text").attr("x",p+m).attr("y",y).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(f.end)}},"drawWord"),SOe={draw:X_t}});var Z_t,kOe,EOe=F(()=>{"use strict";er();Z_t={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},kOe=s(({packet:e}={})=>{let t=Xr(Z_t,e);return` + .packetByte { + font-size: ${t.byteFontSize}; + } + .packetByte.start { + fill: ${t.startByteColor}; + } + .packetByte.end { + fill: ${t.endByteColor}; + } + .packetLabel { + fill: ${t.labelColor}; + font-size: ${t.labelFontSize}; + } + .packetTitle { + fill: ${t.titleColor}; + font-size: ${t.titleFontSize}; + } + .packetBlock { + stroke: ${t.blockStrokeColor}; + stroke-width: ${t.blockStrokeWidth}; + fill: ${t.blockFillColor}; + } + `},"styles")});var AOe={};sr(AOe,{diagram:()=>Q_t});var Q_t,ROe=F(()=>{"use strict";wj();COe();wOe();EOe();Q_t={parser:kj,get db(){return new cx},renderer:SOe,styles:kOe}});var ux,PL,DOe,ju,tLt,rLt,IOe,nLt,iLt,aLt,sLt,oLt,lLt,cLt,D0,Ej=F(()=>{"use strict";br();Qi();er();$n();St();ux={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},PL=32,DOe={axes:[],curves:[],options:ux},ju=structuredClone(DOe),tLt=mr.radar,rLt=s(()=>Xr({...tLt,...Lt().radar}),"getConfig"),IOe=s(()=>ju.axes,"getAxes"),nLt=s(()=>ju.curves,"getCurves"),iLt=s(()=>ju.options,"getOptions"),aLt=s(e=>{ju.axes=e.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),sLt=s(e=>{ju.curves=e.map(t=>({name:t.name,label:t.label??t.name,entries:oLt(t.entries)}))},"setCurves"),oLt=s(e=>{if(e[0].axis==null)return e.map(r=>r.value);let t=IOe();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(r=>{let n=e.find(i=>i.axis?.$refText===r.name);if(n===void 0)throw new Error("Missing entry for axis "+r.label);return n.value})},"computeCurveEntries"),lLt=s(e=>{let t=e.reduce((r,n)=>(r[n.name]=n,r),{});ju.options={showLegend:t.showLegend?.value??ux.showLegend,ticks:t.ticks?.value??ux.ticks,max:t.max?.value??ux.max,min:t.min?.value??ux.min,graticule:t.graticule?.value??ux.graticule},ju.options.ticks>PL&&(se.warn(`Radar diagram ticks (${ju.options.ticks}) exceeds maximum allowed (${PL}). Using ${PL} instead.`),ju.options.ticks=PL)},"setOptions"),cLt=s(()=>{Tr(),ju=structuredClone(DOe)},"clear"),D0={getAxes:IOe,getCurves:nLt,getOptions:iLt,setAxes:aLt,setCurves:sLt,setOptions:lLt,getConfig:rLt,clear:cLt,setAccTitle:Lr,getAccTitle:Ir,setDiagramTitle:Gr,getDiagramTitle:Pr,getAccDescription:Nr,setAccDescription:Mr}});var uLt,MOe,NOe=F(()=>{"use strict";ls();St();to();Ej();uLt=s(e=>{Hn(e,D0);let{axes:t,curves:r,options:n}=e;D0.setAxes(t),D0.setCurves(r),D0.setOptions(n)},"populate"),MOe={parse:s(async e=>{let t=await Li("radar",e);se.debug(t),uLt(t)},"parse")}});function mLt(e,t,r,n,i,a,o){let l=t.length,u=Math.min(o.width,o.height)/2;r.forEach((h,d)=>{if(h.entries.length!==l)return;let f=h.entries.map((p,m)=>{let g=2*Math.PI*m/l-Math.PI/2,y=gLt(p,n,i,u),v=y*Math.cos(g),x=y*Math.sin(g);return{x:v,y:x}});a==="circle"?e.append("path").attr("d",yLt(f,o.curveTension)).attr("class",`radarCurve-${d}`):a==="polygon"&&e.append("polygon").attr("points",f.map(p=>`${p.x},${p.y}`).join(" ")).attr("class",`radarCurve-${d}`)})}function gLt(e,t,r,n){let i=Math.min(Math.max(e,t),r);return n*(i-t)/(r-t)}function yLt(e,t){let r=e.length,n=`M${e[0].x},${e[0].y}`;for(let i=0;i{let h=e.append("g").attr("transform",`translate(${i}, ${a+u*o})`);h.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${u}`),h.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(l.label)})}var hLt,dLt,fLt,pLt,POe,OOe=F(()=>{"use strict";cs();Vn();hLt=s((e,t,r,n)=>{let i=n.db,a=i.getAxes(),o=i.getCurves(),l=i.getOptions(),u=i.getConfig(),h=i.getDiagramTitle(),d=wn(t),f=dLt(d,u),p=l.max??Math.max(...o.map(y=>Math.max(...y.entries))),m=l.min,g=Math.min(u.width,u.height)/2;fLt(f,a,g,l.ticks,l.graticule),pLt(f,a,g,u),mLt(f,a,o,m,p,l.graticule,u),vLt(f,o,l.showLegend,u),f.append("text").attr("class","radarTitle").text(h).attr("x",0).attr("y",-u.height/2-u.marginTop)},"draw"),dLt=s((e,t)=>{let r=t.width+t.marginLeft+t.marginRight,n=t.height+t.marginTop+t.marginBottom,i={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return jr(e,n,r,t.useMaxWidth??!0),e.attr("viewBox",`0 0 ${r} ${n}`).attr("overflow","visible"),e.append("g").attr("transform",`translate(${i.x}, ${i.y})`)},"drawFrame"),fLt=s((e,t,r,n,i)=>{if(i==="circle")for(let a=0;a{let f=2*d*Math.PI/a-Math.PI/2,p=l*Math.cos(f),m=l*Math.sin(f);return`${p},${m}`}).join(" ");e.append("polygon").attr("points",u).attr("class","radarGraticule")}}},"drawGraticule"),pLt=s((e,t,r,n)=>{let i=t.length;for(let a=0;a.01?"start":u<-.01?"end":"middle",f=h>.01?"hanging":h<-.01?"auto":"central",p=4;e.append("text").text(o).attr("x",r*n.axisLabelFactor*u+p*u).attr("y",r*n.axisLabelFactor*h+p*h).attr("text-anchor",d).attr("dominant-baseline",f).attr("class","radarAxisLabel")}},"drawAxes");s(mLt,"drawCurves");s(gLt,"relativeRadius");s(yLt,"closedRoundCurve");s(vLt,"drawLegend");POe={draw:hLt}});var xLt,bLt,BOe,$Oe=F(()=>{"use strict";er();Mc();br();xLt=s((e,t)=>{let r="";for(let n=0;n{let t=ka(),r=Lt(),n=Xr(t,r.themeVariables),i=Xr(n.radar,e);return{themeVariables:n,radarOptions:i}},"buildRadarStyleOptions"),BOe=s(({radar:e}={})=>{let{themeVariables:t,radarOptions:r}=bLt(e);return` + .radarTitle { + font-size: ${t.fontSize}; + color: ${t.titleColor}; + dominant-baseline: hanging; + text-anchor: middle; + } + .radarAxisLine { + stroke: ${r.axisColor}; + stroke-width: ${r.axisStrokeWidth}; + } + .radarAxisLabel { + font-size: ${r.axisLabelFontSize}px; + color: ${r.axisColor}; + } + .radarGraticule { + fill: ${r.graticuleColor}; + fill-opacity: ${r.graticuleOpacity}; + stroke: ${r.graticuleColor}; + stroke-width: ${r.graticuleStrokeWidth}; + } + .radarLegendText { + text-anchor: start; + font-size: ${r.legendFontSize}px; + dominant-baseline: hanging; + } + ${xLt(t,r)} + `},"styles")});var FOe={};sr(FOe,{diagram:()=>TLt});var TLt,GOe=F(()=>{"use strict";Ej();NOe();OOe();$Oe();TLt={parser:MOe,db:D0,renderer:POe,styles:BOe}});var Aj,WOe,qOe=F(()=>{"use strict";Aj=(function(){var e=s(function(T,w,C,S){for(C=C||{},S=T.length;S--;C[T[S]]=w);return C},"o"),t=[1,15],r=[1,7],n=[1,13],i=[1,14],a=[1,19],o=[1,16],l=[1,17],u=[1,18],h=[8,30],d=[8,10,21,28,29,30,31,39,43,46],f=[1,23],p=[1,24],m=[8,10,15,16,21,28,29,30,31,39,43,46],g=[8,10,15,16,21,27,28,29,30,31,39,43,46],y=[1,49],v={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,spaceLines:3,SPACELINE:4,NL:5,separator:6,SPACE:7,EOF:8,start:9,BLOCK_DIAGRAM_KEY:10,document:11,stop:12,statement:13,link:14,LINK:15,START_LINK:16,LINK_LABEL:17,STR:18,nodeStatement:19,columnsStatement:20,SPACE_BLOCK:21,blockStatement:22,classDefStatement:23,cssClassStatement:24,styleStatement:25,node:26,SIZE:27,COLUMNS:28,"id-block":29,end:30,NODE_ID:31,nodeShapeNLabel:32,dirList:33,DIR:34,NODE_DSTART:35,NODE_DEND:36,BLOCK_ARROW_START:37,BLOCK_ARROW_END:38,classDef:39,CLASSDEF_ID:40,CLASSDEF_STYLEOPTS:41,DEFAULT:42,class:43,CLASSENTITY_IDS:44,STYLECLASS:45,style:46,STYLE_ENTITY_IDS:47,STYLE_DEFINITION_DATA:48,$accept:0,$end:1},terminals_:{2:"error",4:"SPACELINE",5:"NL",7:"SPACE",8:"EOF",10:"BLOCK_DIAGRAM_KEY",15:"LINK",16:"START_LINK",17:"LINK_LABEL",18:"STR",21:"SPACE_BLOCK",27:"SIZE",28:"COLUMNS",29:"id-block",30:"end",31:"NODE_ID",34:"DIR",35:"NODE_DSTART",36:"NODE_DEND",37:"BLOCK_ARROW_START",38:"BLOCK_ARROW_END",39:"classDef",40:"CLASSDEF_ID",41:"CLASSDEF_STYLEOPTS",42:"DEFAULT",43:"class",44:"CLASSENTITY_IDS",45:"STYLECLASS",46:"style",47:"STYLE_ENTITY_IDS",48:"STYLE_DEFINITION_DATA"},productions_:[0,[3,1],[3,2],[3,2],[6,1],[6,1],[6,1],[9,3],[12,1],[12,1],[12,2],[12,2],[11,1],[11,2],[14,1],[14,4],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[19,3],[19,2],[19,1],[20,1],[22,4],[22,3],[26,1],[26,2],[33,1],[33,2],[32,3],[32,4],[23,3],[23,3],[24,3],[25,3]],performAction:s(function(w,C,S,k,R,M,P){var N=M.length-1;switch(R){case 4:k.getLogger().debug("Rule: separator (NL) ");break;case 5:k.getLogger().debug("Rule: separator (Space) ");break;case 6:k.getLogger().debug("Rule: separator (EOF) ");break;case 7:k.getLogger().debug("Rule: hierarchy: ",M[N-1]),k.setHierarchy(M[N-1]);break;case 8:k.getLogger().debug("Stop NL ");break;case 9:k.getLogger().debug("Stop EOF ");break;case 10:k.getLogger().debug("Stop NL2 ");break;case 11:k.getLogger().debug("Stop EOF2 ");break;case 12:k.getLogger().debug("Rule: statement: ",M[N]),typeof M[N].length=="number"?this.$=M[N]:this.$=[M[N]];break;case 13:k.getLogger().debug("Rule: statement #2: ",M[N-1]),this.$=[M[N-1]].concat(M[N]);break;case 14:k.getLogger().debug("Rule: link: ",M[N],w),this.$={edgeTypeStr:M[N],label:""};break;case 15:k.getLogger().debug("Rule: LABEL link: ",M[N-3],M[N-1],M[N]),this.$={edgeTypeStr:M[N],label:M[N-1]};break;case 18:let _=parseInt(M[N]),A=k.generateId();this.$={id:A,type:"space",label:"",width:_,children:[]};break;case 23:k.getLogger().debug("Rule: (nodeStatement link node) ",M[N-2],M[N-1],M[N]," typestr: ",M[N-1].edgeTypeStr);let I=k.edgeStrToEdgeData(M[N-1].edgeTypeStr),D=k.edgeStrToEdgeStartData(M[N-1].edgeTypeStr),L=k.edgeStrToThickness(M[N-1].edgeTypeStr),B=k.edgeStrToPattern(M[N-1].edgeTypeStr);this.$=[{id:M[N-2].id,label:M[N-2].label,type:M[N-2].type,directions:M[N-2].directions},{id:M[N-2].id+"-"+M[N].id,start:M[N-2].id,end:M[N].id,label:M[N-1].label,type:"edge",thickness:L,pattern:B,directions:M[N].directions,arrowTypeEnd:I,arrowTypeStart:D},{id:M[N].id,label:M[N].label,type:k.typeStr2Type(M[N].typeStr),directions:M[N].directions}];break;case 24:k.getLogger().debug("Rule: nodeStatement (abc88 node size) ",M[N-1],M[N]),this.$={id:M[N-1].id,label:M[N-1].label,type:k.typeStr2Type(M[N-1].typeStr),directions:M[N-1].directions,widthInColumns:parseInt(M[N],10)};break;case 25:k.getLogger().debug("Rule: nodeStatement (node) ",M[N]),this.$={id:M[N].id,label:M[N].label,type:k.typeStr2Type(M[N].typeStr),directions:M[N].directions,widthInColumns:1};break;case 26:k.getLogger().debug("APA123",this?this:"na"),k.getLogger().debug("COLUMNS: ",M[N]),this.$={type:"column-setting",columns:M[N]==="auto"?-1:parseInt(M[N])};break;case 27:k.getLogger().debug("Rule: id-block statement : ",M[N-2],M[N-1]);let O=k.generateId();this.$={...M[N-2],type:"composite",children:M[N-1]};break;case 28:k.getLogger().debug("Rule: blockStatement : ",M[N-2],M[N-1],M[N]);let $=k.generateId();this.$={id:$,type:"composite",label:"",children:M[N-1]};break;case 29:k.getLogger().debug("Rule: node (NODE_ID separator): ",M[N]),this.$={id:M[N]};break;case 30:k.getLogger().debug("Rule: node (NODE_ID nodeShapeNLabel separator): ",M[N-1],M[N]),this.$={id:M[N-1],label:M[N].label,typeStr:M[N].typeStr,directions:M[N].directions};break;case 31:k.getLogger().debug("Rule: dirList: ",M[N]),this.$=[M[N]];break;case 32:k.getLogger().debug("Rule: dirList: ",M[N-1],M[N]),this.$=[M[N-1]].concat(M[N]);break;case 33:k.getLogger().debug("Rule: nodeShapeNLabel: ",M[N-2],M[N-1],M[N]),this.$={typeStr:M[N-2]+M[N],label:M[N-1]};break;case 34:k.getLogger().debug("Rule: BLOCK_ARROW nodeShapeNLabel: ",M[N-3],M[N-2]," #3:",M[N-1],M[N]),this.$={typeStr:M[N-3]+M[N],label:M[N-2],directions:M[N-1]};break;case 35:case 36:this.$={type:"classDef",id:M[N-1].trim(),css:M[N].trim()};break;case 37:this.$={type:"applyClass",id:M[N-1].trim(),styleClass:M[N].trim()};break;case 38:this.$={type:"applyStyles",id:M[N-1].trim(),stylesStr:M[N].trim()};break}},"anonymous"),table:[{9:1,10:[1,2]},{1:[3]},{10:t,11:3,13:4,19:5,20:6,21:r,22:8,23:9,24:10,25:11,26:12,28:n,29:i,31:a,39:o,43:l,46:u},{8:[1,20]},e(h,[2,12],{13:4,19:5,20:6,22:8,23:9,24:10,25:11,26:12,11:21,10:t,21:r,28:n,29:i,31:a,39:o,43:l,46:u}),e(d,[2,16],{14:22,15:f,16:p}),e(d,[2,17]),e(d,[2,18]),e(d,[2,19]),e(d,[2,20]),e(d,[2,21]),e(d,[2,22]),e(m,[2,25],{27:[1,25]}),e(d,[2,26]),{19:26,26:12,31:a},{10:t,11:27,13:4,19:5,20:6,21:r,22:8,23:9,24:10,25:11,26:12,28:n,29:i,31:a,39:o,43:l,46:u},{40:[1,28],42:[1,29]},{44:[1,30]},{47:[1,31]},e(g,[2,29],{32:32,35:[1,33],37:[1,34]}),{1:[2,7]},e(h,[2,13]),{26:35,31:a},{31:[2,14]},{17:[1,36]},e(m,[2,24]),{10:t,11:37,13:4,14:22,15:f,16:p,19:5,20:6,21:r,22:8,23:9,24:10,25:11,26:12,28:n,29:i,31:a,39:o,43:l,46:u},{30:[1,38]},{41:[1,39]},{41:[1,40]},{45:[1,41]},{48:[1,42]},e(g,[2,30]),{18:[1,43]},{18:[1,44]},e(m,[2,23]),{18:[1,45]},{30:[1,46]},e(d,[2,28]),e(d,[2,35]),e(d,[2,36]),e(d,[2,37]),e(d,[2,38]),{36:[1,47]},{33:48,34:y},{15:[1,50]},e(d,[2,27]),e(g,[2,33]),{38:[1,51]},{33:52,34:y,38:[2,31]},{31:[2,15]},e(g,[2,34]),{38:[2,32]}],defaultActions:{20:[2,7],23:[2,14],50:[2,15],52:[2,32]},parseError:s(function(w,C){if(C.recoverable)this.trace(w);else{var S=new Error(w);throw S.hash=C,S}},"parseError"),parse:s(function(w){var C=this,S=[0],k=[],R=[null],M=[],P=this.table,N="",_=0,A=0,I=0,D=2,L=1,B=M.slice.call(arguments,1),O=Object.create(this.lexer),$={yy:{}};for(var W in this.yy)Object.prototype.hasOwnProperty.call(this.yy,W)&&($.yy[W]=this.yy[W]);O.setInput(w,$.yy),$.yy.lexer=O,$.yy.parser=this,typeof O.yylloc>"u"&&(O.yylloc={});var V=O.yylloc;M.push(V);var z=O.options&&O.options.ranges;typeof $.yy.parseError=="function"?this.parseError=$.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function H(He){S.length=S.length-2*He,R.length=R.length-He,M.length=M.length-He}s(H,"popStack");function K(){var He;return He=k.pop()||O.lex()||L,typeof He!="number"&&(He instanceof Array&&(k=He,He=k.pop()),He=C.symbols_[He]||He),He}s(K,"lex");for(var J,ne,Z,pe,te,fe,ce={},de,Ee,we,Fe;;){if(Z=S[S.length-1],this.defaultActions[Z]?pe=this.defaultActions[Z]:((J===null||typeof J>"u")&&(J=K()),pe=P[Z]&&P[Z][J]),typeof pe>"u"||!pe.length||!pe[0]){var Ye="";Fe=[];for(de in P[Z])this.terminals_[de]&&de>D&&Fe.push("'"+this.terminals_[de]+"'");O.showPosition?Ye="Parse error on line "+(_+1)+`: +`+O.showPosition()+` +Expecting `+Fe.join(", ")+", got '"+(this.terminals_[J]||J)+"'":Ye="Parse error on line "+(_+1)+": Unexpected "+(J==L?"end of input":"'"+(this.terminals_[J]||J)+"'"),this.parseError(Ye,{text:O.match,token:this.terminals_[J]||J,line:O.yylineno,loc:V,expected:Fe})}if(pe[0]instanceof Array&&pe.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Z+", token: "+J);switch(pe[0]){case 1:S.push(J),R.push(O.yytext),M.push(O.yylloc),S.push(pe[1]),J=null,ne?(J=ne,ne=null):(A=O.yyleng,N=O.yytext,_=O.yylineno,V=O.yylloc,I>0&&I--);break;case 2:if(Ee=this.productions_[pe[1]][1],ce.$=R[R.length-Ee],ce._$={first_line:M[M.length-(Ee||1)].first_line,last_line:M[M.length-1].last_line,first_column:M[M.length-(Ee||1)].first_column,last_column:M[M.length-1].last_column},z&&(ce._$.range=[M[M.length-(Ee||1)].range[0],M[M.length-1].range[1]]),fe=this.performAction.apply(ce,[N,A,_,$.yy,pe[1],R,M].concat(B)),typeof fe<"u")return fe;Ee&&(S=S.slice(0,-1*Ee*2),R=R.slice(0,-1*Ee),M=M.slice(0,-1*Ee)),S.push(this.productions_[pe[1]][0]),R.push(ce.$),M.push(ce._$),we=P[S[S.length-2]][S[S.length-1]],S.push(we);break;case 3:return!0}}return!0},"parse")},x=(function(){var T={EOF:1,parseError:s(function(C,S){if(this.yy.parser)this.yy.parser.parseError(C,S);else throw new Error(C)},"parseError"),setInput:s(function(w,C){return this.yy=C||this.yy||{},this._input=w,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var w=this._input[0];this.yytext+=w,this.yyleng++,this.offset++,this.match+=w,this.matched+=w;var C=w.match(/(?:\r\n?|\n).*/g);return C?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),w},"input"),unput:s(function(w){var C=w.length,S=w.split(/(?:\r\n?|\n)/g);this._input=w+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-C),this.offset-=C;var k=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),S.length-1&&(this.yylineno-=S.length-1);var R=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:S?(S.length===k.length?this.yylloc.first_column:0)+k[k.length-S.length].length-S[0].length:this.yylloc.first_column-C},this.options.ranges&&(this.yylloc.range=[R[0],R[0]+this.yyleng-C]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(w){this.unput(this.match.slice(w))},"less"),pastInput:s(function(){var w=this.matched.substr(0,this.matched.length-this.match.length);return(w.length>20?"...":"")+w.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var w=this.match;return w.length<20&&(w+=this._input.substr(0,20-w.length)),(w.substr(0,20)+(w.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var w=this.pastInput(),C=new Array(w.length+1).join("-");return w+this.upcomingInput()+` +`+C+"^"},"showPosition"),test_match:s(function(w,C){var S,k,R;if(this.options.backtrack_lexer&&(R={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(R.yylloc.range=this.yylloc.range.slice(0))),k=w[0].match(/(?:\r\n?|\n).*/g),k&&(this.yylineno+=k.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:k?k[k.length-1].length-k[k.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+w[0].length},this.yytext+=w[0],this.match+=w[0],this.matches=w,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(w[0].length),this.matched+=w[0],S=this.performAction.call(this,this.yy,this,C,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),S)return S;if(this._backtrack){for(var M in R)this[M]=R[M];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var w,C,S,k;this._more||(this.yytext="",this.match="");for(var R=this._currentRules(),M=0;MC[0].length)){if(C=S,k=M,this.options.backtrack_lexer){if(w=this.test_match(S,R[M]),w!==!1)return w;if(this._backtrack){C=!1;continue}else return!1}else if(!this.options.flex)break}return C?(w=this.test_match(C,R[k]),w!==!1?w:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var C=this.next();return C||this.lex()},"lex"),begin:s(function(C){this.conditionStack.push(C)},"begin"),popState:s(function(){var C=this.conditionStack.length-1;return C>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(C){return C=this.conditionStack.length-1-Math.abs(C||0),C>=0?this.conditionStack[C]:"INITIAL"},"topState"),pushState:s(function(C){this.begin(C)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:s(function(C,S,k,R){var M=R;switch(k){case 0:return C.getLogger().debug("Found block-beta"),10;break;case 1:return C.getLogger().debug("Found id-block"),29;break;case 2:return C.getLogger().debug("Found block"),10;break;case 3:C.getLogger().debug(".",S.yytext);break;case 4:C.getLogger().debug("_",S.yytext);break;case 5:return 5;case 6:return S.yytext=-1,28;break;case 7:return S.yytext=S.yytext.replace(/columns\s+/,""),C.getLogger().debug("COLUMNS (LEX)",S.yytext),28;break;case 8:this.pushState("md_string");break;case 9:return"MD_STR";case 10:this.popState();break;case 11:this.pushState("string");break;case 12:C.getLogger().debug("LEX: POPPING STR:",S.yytext),this.popState();break;case 13:return C.getLogger().debug("LEX: STR end:",S.yytext),"STR";break;case 14:return S.yytext=S.yytext.replace(/space\:/,""),C.getLogger().debug("SPACE NUM (LEX)",S.yytext),21;break;case 15:return S.yytext="1",C.getLogger().debug("COLUMNS (LEX)",S.yytext),21;break;case 16:return 42;case 17:return"LINKSTYLE";case 18:return"INTERPOLATE";case 19:return this.pushState("CLASSDEF"),39;break;case 20:return this.popState(),this.pushState("CLASSDEFID"),"DEFAULT_CLASSDEF_ID";break;case 21:return this.popState(),this.pushState("CLASSDEFID"),40;break;case 22:return this.popState(),41;break;case 23:return this.pushState("CLASS"),43;break;case 24:return this.popState(),this.pushState("CLASS_STYLE"),44;break;case 25:return this.popState(),45;break;case 26:return this.pushState("STYLE_STMNT"),46;break;case 27:return this.popState(),this.pushState("STYLE_DEFINITION"),47;break;case 28:return this.popState(),48;break;case 29:return this.pushState("acc_title"),"acc_title";break;case 30:return this.popState(),"acc_title_value";break;case 31:return this.pushState("acc_descr"),"acc_descr";break;case 32:return this.popState(),"acc_descr_value";break;case 33:this.pushState("acc_descr_multiline");break;case 34:this.popState();break;case 35:return"acc_descr_multiline_value";case 36:return 30;case 37:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 38:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 39:return this.popState(),C.getLogger().debug("Lex: ))"),"NODE_DEND";break;case 40:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 41:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 42:return this.popState(),C.getLogger().debug("Lex: (-"),"NODE_DEND";break;case 43:return this.popState(),C.getLogger().debug("Lex: -)"),"NODE_DEND";break;case 44:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 45:return this.popState(),C.getLogger().debug("Lex: ]]"),"NODE_DEND";break;case 46:return this.popState(),C.getLogger().debug("Lex: ("),"NODE_DEND";break;case 47:return this.popState(),C.getLogger().debug("Lex: ])"),"NODE_DEND";break;case 48:return this.popState(),C.getLogger().debug("Lex: /]"),"NODE_DEND";break;case 49:return this.popState(),C.getLogger().debug("Lex: /]"),"NODE_DEND";break;case 50:return this.popState(),C.getLogger().debug("Lex: )]"),"NODE_DEND";break;case 51:return this.popState(),C.getLogger().debug("Lex: )"),"NODE_DEND";break;case 52:return this.popState(),C.getLogger().debug("Lex: ]>"),"NODE_DEND";break;case 53:return this.popState(),C.getLogger().debug("Lex: ]"),"NODE_DEND";break;case 54:return C.getLogger().debug("Lexa: -)"),this.pushState("NODE"),35;break;case 55:return C.getLogger().debug("Lexa: (-"),this.pushState("NODE"),35;break;case 56:return C.getLogger().debug("Lexa: ))"),this.pushState("NODE"),35;break;case 57:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 58:return C.getLogger().debug("Lex: ((("),this.pushState("NODE"),35;break;case 59:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 60:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 61:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 62:return C.getLogger().debug("Lexc: >"),this.pushState("NODE"),35;break;case 63:return C.getLogger().debug("Lexa: (["),this.pushState("NODE"),35;break;case 64:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 65:return this.pushState("NODE"),35;break;case 66:return this.pushState("NODE"),35;break;case 67:return this.pushState("NODE"),35;break;case 68:return this.pushState("NODE"),35;break;case 69:return this.pushState("NODE"),35;break;case 70:return this.pushState("NODE"),35;break;case 71:return this.pushState("NODE"),35;break;case 72:return C.getLogger().debug("Lexa: ["),this.pushState("NODE"),35;break;case 73:return this.pushState("BLOCK_ARROW"),C.getLogger().debug("LEX ARR START"),37;break;case 74:return C.getLogger().debug("Lex: NODE_ID",S.yytext),31;break;case 75:return C.getLogger().debug("Lex: EOF",S.yytext),8;break;case 76:this.pushState("md_string");break;case 77:this.pushState("md_string");break;case 78:return"NODE_DESCR";case 79:this.popState();break;case 80:C.getLogger().debug("Lex: Starting string"),this.pushState("string");break;case 81:C.getLogger().debug("LEX ARR: Starting string"),this.pushState("string");break;case 82:return C.getLogger().debug("LEX: NODE_DESCR:",S.yytext),"NODE_DESCR";break;case 83:C.getLogger().debug("LEX POPPING"),this.popState();break;case 84:C.getLogger().debug("Lex: =>BAE"),this.pushState("ARROW_DIR");break;case 85:return S.yytext=S.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (right): dir:",S.yytext),"DIR";break;case 86:return S.yytext=S.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (left):",S.yytext),"DIR";break;case 87:return S.yytext=S.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (x):",S.yytext),"DIR";break;case 88:return S.yytext=S.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (y):",S.yytext),"DIR";break;case 89:return S.yytext=S.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (up):",S.yytext),"DIR";break;case 90:return S.yytext=S.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (down):",S.yytext),"DIR";break;case 91:return S.yytext="]>",C.getLogger().debug("Lex (ARROW_DIR end):",S.yytext),this.popState(),this.popState(),"BLOCK_ARROW_END";break;case 92:return C.getLogger().debug("Lex: LINK","#"+S.yytext+"#"),15;break;case 93:return C.getLogger().debug("Lex: LINK",S.yytext),15;break;case 94:return C.getLogger().debug("Lex: LINK",S.yytext),15;break;case 95:return C.getLogger().debug("Lex: LINK",S.yytext),15;break;case 96:return C.getLogger().debug("Lex: START_LINK",S.yytext),this.pushState("LLABEL"),16;break;case 97:return C.getLogger().debug("Lex: START_LINK",S.yytext),this.pushState("LLABEL"),16;break;case 98:return C.getLogger().debug("Lex: START_LINK",S.yytext),this.pushState("LLABEL"),16;break;case 99:this.pushState("md_string");break;case 100:return C.getLogger().debug("Lex: Starting string"),this.pushState("string"),"LINK_LABEL";break;case 101:return this.popState(),C.getLogger().debug("Lex: LINK","#"+S.yytext+"#"),15;break;case 102:return this.popState(),C.getLogger().debug("Lex: LINK",S.yytext),15;break;case 103:return this.popState(),C.getLogger().debug("Lex: LINK",S.yytext),15;break;case 104:return C.getLogger().debug("Lex: COLON",S.yytext),S.yytext=S.yytext.slice(1),27;break}},"anonymous"),rules:[/^(?:block-beta\b)/,/^(?:block:)/,/^(?:block\b)/,/^(?:[\s]+)/,/^(?:[\n]+)/,/^(?:((\u000D\u000A)|(\u000A)))/,/^(?:columns\s+auto\b)/,/^(?:columns\s+[\d]+)/,/^(?:["][`])/,/^(?:[^`"]+)/,/^(?:[`]["])/,/^(?:["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:space[:]\d+)/,/^(?:space\b)/,/^(?:default\b)/,/^(?:linkStyle\b)/,/^(?:interpolate\b)/,/^(?:classDef\s+)/,/^(?:DEFAULT\s+)/,/^(?:\w+\s+)/,/^(?:[^\n]*)/,/^(?:class\s+)/,/^(?:(\w+)+((,\s*\w+)*))/,/^(?:[^\n]*)/,/^(?:style\s+)/,/^(?:(\w+)+((,\s*\w+)*))/,/^(?:[^\n]*)/,/^(?:accTitle\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*\{\s*)/,/^(?:[\}])/,/^(?:[^\}]*)/,/^(?:end\b\s*)/,/^(?:\(\(\()/,/^(?:\)\)\))/,/^(?:[\)]\))/,/^(?:\}\})/,/^(?:\})/,/^(?:\(-)/,/^(?:-\))/,/^(?:\(\()/,/^(?:\]\])/,/^(?:\()/,/^(?:\]\))/,/^(?:\\\])/,/^(?:\/\])/,/^(?:\)\])/,/^(?:[\)])/,/^(?:\]>)/,/^(?:[\]])/,/^(?:-\))/,/^(?:\(-)/,/^(?:\)\))/,/^(?:\))/,/^(?:\(\(\()/,/^(?:\(\()/,/^(?:\{\{)/,/^(?:\{)/,/^(?:>)/,/^(?:\(\[)/,/^(?:\()/,/^(?:\[\[)/,/^(?:\[\|)/,/^(?:\[\()/,/^(?:\)\)\))/,/^(?:\[\\)/,/^(?:\[\/)/,/^(?:\[\\)/,/^(?:\[)/,/^(?:<\[)/,/^(?:[^\(\[\n\-\)\{\}\s\<\>:=]+)/,/^(?:$)/,/^(?:["][`])/,/^(?:["][`])/,/^(?:[^`"]+)/,/^(?:[`]["])/,/^(?:["])/,/^(?:["])/,/^(?:[^"]+)/,/^(?:["])/,/^(?:\]>\s*\()/,/^(?:,?\s*right\s*)/,/^(?:,?\s*left\s*)/,/^(?:,?\s*x\s*)/,/^(?:,?\s*y\s*)/,/^(?:,?\s*up\s*)/,/^(?:,?\s*down\s*)/,/^(?:\)\s*)/,/^(?:\s*[xo<]?--+[-xo>]\s*)/,/^(?:\s*[xo<]?==+[=xo>]\s*)/,/^(?:\s*[xo<]?-?\.+-[xo>]?\s*)/,/^(?:\s*~~[\~]+\s*)/,/^(?:\s*[xo<]?--\s*)/,/^(?:\s*[xo<]?==\s*)/,/^(?:\s*[xo<]?-\.\s*)/,/^(?:["][`])/,/^(?:["])/,/^(?:\s*[xo<]?--+[-xo>]\s*)/,/^(?:\s*[xo<]?==+[=xo>]\s*)/,/^(?:\s*[xo<]?-?\.+-[xo>]?\s*)/,/^(?::\d+)/],conditions:{STYLE_DEFINITION:{rules:[28],inclusive:!1},STYLE_STMNT:{rules:[27],inclusive:!1},CLASSDEFID:{rules:[22],inclusive:!1},CLASSDEF:{rules:[20,21],inclusive:!1},CLASS_STYLE:{rules:[25],inclusive:!1},CLASS:{rules:[24],inclusive:!1},LLABEL:{rules:[99,100,101,102,103],inclusive:!1},ARROW_DIR:{rules:[85,86,87,88,89,90,91],inclusive:!1},BLOCK_ARROW:{rules:[76,81,84],inclusive:!1},NODE:{rules:[37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,77,80],inclusive:!1},md_string:{rules:[9,10,78,79],inclusive:!1},space:{rules:[],inclusive:!1},string:{rules:[12,13,82,83],inclusive:!1},acc_descr_multiline:{rules:[34,35],inclusive:!1},acc_descr:{rules:[32],inclusive:!1},acc_title:{rules:[30],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8,11,14,15,16,17,18,19,23,26,29,31,33,36,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,92,93,94,95,96,97,98,104],inclusive:!0}}};return T})();v.lexer=x;function b(){this.yy={}}return s(b,"Parser"),b.prototype=v,v.Parser=b,new b})();Aj.parser=Aj;WOe=Aj});function LLt(e){switch(se.debug("typeStr2Type",e),e){case"[]":return"square";case"()":return se.debug("we have a round"),"round";case"(())":return"circle";case">]":return"rect_left_inv_arrow";case"{}":return"diamond";case"{{}}":return"hexagon";case"([])":return"stadium";case"[[]]":return"subroutine";case"[()]":return"cylinder";case"((()))":return"doublecircle";case"[//]":return"lean_right";case"[\\\\]":return"lean_left";case"[/\\]":return"trapezoid";case"[\\/]":return"inv_trapezoid";case"<[]>":return"block_arrow";default:return"na"}}function DLt(e){switch(se.debug("typeStr2Type",e),e){case"==":return"thick";default:return"normal"}}function ILt(e){switch(e.trim().slice(-1)){case"x":return"arrow_cross";case"o":return"arrow_circle";case">":return"arrow_point";default:return""}}function MLt(e){switch(e.trim().charAt(0)){case"x":return"arrow_cross";case"o":return"arrow_circle";case"<":return"arrow_point";default:return"arrow_open"}}function NLt(e){return e.includes("==")?"thick":"normal"}function PLt(e){return e.includes(".-")?"dotted":"solid"}var Lc,_j,Rj,HOe,UOe,wLt,jOe,OL,Lj,kLt,ELt,ALt,RLt,XOe,Dj,KS,_Lt,YOe,OLt,BLt,$Lt,FLt,GLt,zLt,VLt,WLt,qLt,HLt,ULt,YLt,jLt,KOe,ZOe=F(()=>{"use strict";t4();br();Jt();St();Yr();$n();Lc=new Map,_j=[],Rj=new Map,HOe="color",UOe="fill",wLt="bgFill",jOe=",",OL=new Map,Lj="",kLt=s(e=>Tt.sanitizeText(e,Pe()),"sanitizeText"),ELt=s(function(e,t=""){let r=OL.get(e);r||(r={id:e,styles:[],textStyles:[]},OL.set(e,r)),t?.split(jOe).forEach(n=>{let i=n.replace(/([^;]*);/,"$1").trim();if(RegExp(HOe).exec(n)){let o=i.replace(UOe,wLt).replace(HOe,UOe);r.textStyles.push(o)}r.styles.push(i)})},"addStyleClass"),ALt=s(function(e,t=""){let r=Lc.get(e);t!=null&&(r.styles=t.split(jOe))},"addStyle2Node"),RLt=s(function(e,t){e.split(",").forEach(function(r){let n=Lc.get(r);if(n===void 0){let i=r.trim();n={id:i,type:"na",children:[]},Lc.set(i,n)}n.classes||(n.classes=[]),n.classes.push(t)})},"setCssClass"),XOe=s((e,t)=>{let r=e.flat(),n=[],a=r.find(o=>o?.type==="column-setting")?.columns??-1;for(let o of r){if(typeof a=="number"&&a>0&&o.type!=="column-setting"&&typeof o.widthInColumns=="number"&&o.widthInColumns>a&&se.warn(`Block ${o.id} width ${o.widthInColumns} exceeds configured column width ${a}`),o.label&&(o.label=kLt(o.label)),o.type==="classDef"){ELt(o.id,o.css);continue}if(o.type==="applyClass"){RLt(o.id,o?.styleClass??"");continue}if(o.type==="applyStyles"){o?.stylesStr&&ALt(o.id,o?.stylesStr);continue}if(o.type==="column-setting")t.columns=o.columns??-1;else if(o.type==="edge"){let l=(Rj.get(o.id)??0)+1;Rj.set(o.id,l),o.id=l+"-"+o.id,_j.push(o)}else{o.label||(o.type==="composite"?o.label="":o.label=o.id);let l=Lc.get(o.id);if(l===void 0?Lc.set(o.id,o):(o.type!=="na"&&(l.type=o.type),o.label!==o.id&&(l.label=o.label)),o.children&&XOe(o.children,o),o.type==="space"){let u=o.width??1;for(let h=0;h{se.debug("Clear called"),Tr(),KS={id:"root",type:"composite",children:[],columns:-1},Lc=new Map([["root",KS]]),Dj=[],OL=new Map,_j=[],Rj=new Map,Lj=""},"clear");s(LLt,"typeStr2Type");s(DLt,"edgeTypeStr2Type");s(ILt,"edgeStrToEdgeData");s(MLt,"edgeStrToEdgeStartData");s(NLt,"edgeStrToThickness");s(PLt,"edgeStrToPattern");YOe=0,OLt=s(()=>(YOe++,"id-"+Math.random().toString(36).substr(2,12)+"-"+YOe),"generateId"),BLt=s(e=>{KS.children=e,XOe(e,KS),Dj=KS.children},"setHierarchy"),$Lt=s(e=>{let t=Lc.get(e);return t?t.columns?t.columns:t.children?t.children.length:-1:-1},"getColumns"),FLt=s(()=>[...Lc.values()],"getBlocksFlat"),GLt=s(()=>Dj||[],"getBlocks"),zLt=s(()=>_j,"getEdges"),VLt=s(e=>Lc.get(e),"getBlock"),WLt=s(e=>{Lc.set(e.id,e)},"setBlock"),qLt=s(e=>{Lj=e},"setDiagramId"),HLt=s(()=>Lj,"getDiagramId"),ULt=s(()=>se,"getLogger"),YLt=s(function(){return OL},"getClasses"),jLt={getConfig:s(()=>Lt().block,"getConfig"),typeStr2Type:LLt,edgeTypeStr2Type:DLt,edgeStrToEdgeData:ILt,edgeStrToEdgeStartData:MLt,edgeStrToThickness:NLt,edgeStrToPattern:PLt,getLogger:ULt,getBlocksFlat:FLt,getBlocks:GLt,getEdges:zLt,setHierarchy:BLt,getBlock:VLt,setBlock:WLt,getColumns:$Lt,getClasses:YLt,clear:_Lt,generateId:OLt,setDiagramId:qLt,getDiagramId:HLt},KOe=jLt});var Ij,XLt,QOe,JOe=F(()=>{"use strict";Xi();U1();Ij=s((e,t)=>{let r=Gp,n=r(e,"r"),i=r(e,"g"),a=r(e,"b");return Wi(n,i,a,t)},"fade"),XLt=s(e=>`.label { + font-family: ${e.fontFamily}; + color: ${e.nodeTextColor||e.textColor}; + } + .cluster-label text { + fill: ${e.titleColor}; + } + .cluster-label span { + color: ${e.titleColor}; + } + + + + .label text,span { + fill: ${e.nodeTextColor||e.textColor}; + color: ${e.nodeTextColor||e.textColor}; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: 1px; + } + .flowchart-label text { + text-anchor: middle; + } + // .flowchart-label .text-outer-tspan { + // text-anchor: middle; + // } + // .flowchart-label .text-inner-tspan { + // text-anchor: start; + // } + + .node .label { + text-align: center; + } + .node.clickable { + cursor: pointer; + } + + .arrowheadPath { + fill: ${e.arrowheadColor}; + } + + .edgePaths .path { + stroke: ${e.lineColor}; + stroke-width: 2.0px; + } + + .flowchart-link { + stroke: ${e.lineColor}; + fill: none; + } + + .edgeLabel { + background-color: ${e.edgeLabelBackground}; + /* + * This is for backward compatibility with existing code that didn't + * add a \`

    \` around edge labels. + * + * TODO: We should probably remove this in a future release. + */ + p { + margin: 0; + padding: 0; + display: inline; + } + rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; + } + + /* For html labels only */ + .labelBkg { + background-color: ${e.edgeLabelBackground}; + } + + .node .cluster { + // fill: ${Ij(e.mainBkg,.5)}; + fill: ${Ij(e.clusterBkg,.5)}; + stroke: ${Ij(e.clusterBorder,.2)}; + box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px; + stroke-width: 1px; + } + + .cluster text { + fill: ${e.titleColor}; + } + + .cluster span { + color: ${e.titleColor}; + } + /* .cluster div { + color: ${e.titleColor}; + } */ + + div.mermaidTooltip { + position: absolute; + text-align: center; + max-width: 200px; + padding: 2px; + font-family: ${e.fontFamily}; + font-size: 12px; + background: ${e.tertiaryColor}; + border: 1px solid ${e.border2}; + border-radius: 2px; + pointer-events: none; + z-index: 100; + } + + .flowchartTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; + } + ${Ru()} +`,"getStyles"),QOe=XLt});function e9e(e,t){if(e===0||!Number.isInteger(e))throw new Error("Columns must be an integer !== 0.");if(t<0||!Number.isInteger(t))throw new Error("Position must be a non-negative integer."+t);if(e<0)return{px:t,py:0};if(e===1)return{px:0,py:t};let r=t%e,n=Math.floor(t/e);return{px:r,py:n}}function Mj(e,t,r=0,n=0,i=8){se.debug("setBlockSizes abc95 (start)",e.id,e?.size?.x,"block width =",e?.size,"siblingWidth",r),e?.size?.width||(e.size={width:r,height:n,x:0,y:0});let a=0,o=0;if(e.children?.length>0){for(let g of e.children)Mj(g,t,0,0,i);let l=KLt(e);a=l.width,o=l.height,se.debug("setBlockSizes abc95 maxWidth of",e.id,":s children is ",a,o);for(let g of e.children)g.size&&(se.debug(`abc95 Setting size of children of ${e.id} id=${g.id} ${a} ${o} ${JSON.stringify(g.size)}`),g.size.width=a*(g.widthInColumns??1)+i*((g.widthInColumns??1)-1),g.size.height=o,g.size.x=0,g.size.y=0,se.debug(`abc95 updating size of ${e.id} children child:${g.id} maxWidth:${a} maxHeight:${o}`));for(let g of e.children)Mj(g,t,a,o,i);let u=e.columns??-1,h=0;for(let g of e.children)h+=g.widthInColumns??1;let d=e.children.length;u>0&&u0?Math.min(e.children.length,u):e.children.length;if(g>0){let y=(p-g*i-i)/g;se.debug("abc95 (growing to fit) width",e.id,p,e.size?.width,y);for(let v of e.children)v.size&&(v.size.width=y)}}e.size={width:p,height:m,x:0,y:0}}se.debug("setBlockSizes abc94 (done)",e.id,e?.size?.x,e?.size?.width,e?.size?.y,e?.size?.height)}function t9e(e,t,r=8){se.debug(`abc85 layout blocks (=>layoutBlocks) ${e.id} x: ${e?.size?.x} y: ${e?.size?.y} width: ${e?.size?.width}`);let n=e.columns??-1;if(se.debug("layoutBlocks columns abc95",e.id,"=>",n,e),e.children&&e.children.length>0){let i=e?.children[0]?.size?.width??0,a=e.children.length*i+(e.children.length-1)*r;se.debug("widthOfChildren 88",a,"posX");let o=new Map;{let f=0;for(let p of e.children){if(!p.size)continue;let{py:m}=e9e(n,f),g=o.get(m)??0;p.size.height>g&&o.set(m,p.size.height);let y=p?.widthInColumns??1;n>0&&(y=Math.min(y,n-f%n)),f+=y}}let l=new Map;{let f=0,p=[...o.keys()].sort((m,g)=>m-g);for(let m of p)l.set(m,f),f+=(o.get(m)??0)+r}let u=0;se.debug("abc91 block?.size?.x",e.id,e?.size?.x);let h=e?.size?.x?e?.size?.x+(-e?.size?.width/2||0):-r,d=0;for(let f of e.children){let p=e;if(!f.size)continue;let{width:m,height:g}=f.size,{px:y,py:v}=e9e(n,u);if(v!=d&&(d=v,h=e?.size?.x?e?.size?.x+(-e?.size?.width/2||0):-r,se.debug("New row in layout for block",e.id," and child ",f.id,d)),se.debug(`abc89 layout blocks (child) id: ${f.id} Pos: ${u} (px, py) ${y},${v} (${p?.size?.x},${p?.size?.y}) parent: ${p.id} width: ${m}${r}`),p.size){let b=m/2;f.size.x=h+r+b,se.debug(`abc91 layout blocks (calc) px, pyid:${f.id} startingPos=X${h} new startingPosX${f.size.x} ${b} padding=${r} width=${m} halfWidth=${b} => x:${f.size.x} y:${f.size.y} ${f.widthInColumns} (width * (child?.w || 1)) / 2 ${m*(f?.widthInColumns??1)/2}`),h=f.size.x+b;let T=l.get(v)??0,w=o.get(v)??g;f.size.y=p.size.y-p.size.height/2+T+w/2+r,se.debug(`abc88 layout blocks (calc) px, pyid:${f.id}startingPosX${h}${r}${b}=>x:${f.size.x}y:${f.size.y}${f.widthInColumns}(width * (child?.w || 1)) / 2${m*(f?.widthInColumns??1)/2}`)}f.children&&t9e(f,t,r);let x=f?.widthInColumns??1;n>0&&(x=Math.min(x,n-u%n)),u+=x,se.debug("abc88 columnsPos",f,u)}}se.debug(`layout blocks (<==layoutBlocks) ${e.id} x: ${e?.size?.x} y: ${e?.size?.y} width: ${e?.size?.width}`)}function r9e(e,{minX:t,minY:r,maxX:n,maxY:i}={minX:0,minY:0,maxX:0,maxY:0}){if(e.size&&e.id!=="root"){let{x:a,y:o,width:l,height:u}=e.size;a-l/2n&&(n=a+l/2),o+u/2>i&&(i=o+u/2)}if(e.children)for(let a of e.children)({minX:t,minY:r,maxX:n,maxY:i}=r9e(a,{minX:t,minY:r,maxX:n,maxY:i}));return{minX:t,minY:r,maxX:n,maxY:i}}function n9e(e){let t=e.getBlock("root");if(!t)return;let r=Pe()?.block?.padding??8;Mj(t,e,0,0,r),t9e(t,e,r),se.debug("getBlocks",JSON.stringify(t,null,2));let{minX:n,minY:i,maxX:a,maxY:o}=r9e(t),l=o-i,u=a-n;return{x:n,y:i,width:u,height:l}}var KLt,i9e=F(()=>{"use strict";St();Jt();s(e9e,"calculateBlockPosition");KLt=s(e=>{let t=0,r=0;for(let n of e.children){let{width:i,height:a,x:o,y:l}=n.size??{width:0,height:0,x:0,y:0};if(se.debug("getMaxChildSize abc95 child:",n.id,"width:",i,"height:",a,"x:",o,"y:",l,n.type),n.type==="space")continue;let u=i/(n.widthInColumns??1);u>t&&(t=u),a>r&&(r=a)}return{width:t,height:r}},"getMaxChildSize");s(Mj,"setBlockSizes");s(t9e,"layoutBlocks");s(r9e,"findBounds");s(n9e,"layout")});function a9e(e,t,r=!1){let n=e,i="default";(n?.classes?.length||0)>0&&(i=(n?.classes??[]).join(" ")),i=i+" flowchart-label";let a=(n?.classes??[]).flatMap(g=>t.getClasses().get(g)?.styles??[]),o=0,l="rect",u;switch(n.type){case"round":o=5,l="rect";break;case"composite":o=0,l="composite",u=0;break;case"square":l="rect";break;case"diamond":l="question";break;case"hexagon":l="hexagon";break;case"block_arrow":l="block_arrow";break;case"odd":l="rect_left_inv_arrow";break;case"lean_right":l="lean_right";break;case"lean_left":l="lean_left";break;case"trapezoid":l="trapezoid";break;case"inv_trapezoid":l="inv_trapezoid";break;case"rect_left_inv_arrow":l="rect_left_inv_arrow";break;case"circle":l="circle";break;case"ellipse":l="ellipse";break;case"stadium":l="stadium";break;case"subroutine":l="subroutine";break;case"cylinder":l="cylinder";break;case"group":l="rect";break;case"doublecircle":l="doublecircle";break;default:l="rect"}let h=UN(n?.styles??[]),d=n.label,f=n.size??{width:0,height:0,x:0,y:0},p=t.getDiagramId();return{labelStyle:h.labelStyle,shape:l,label:d,labelText:d,rx:o,ry:o,class:i,cssClasses:i,cssStyles:n?.styles??[],cssCompiledStyles:a,style:h.style,id:n.id,domId:p?`${p}-${n.id}`:n.id,isGroup:!1,directions:n.directions,width:f.width||void 0,height:f.height||void 0,wrappingWidth:f.width||Number.POSITIVE_INFINITY,x:f.x,y:f.y,positioned:r,intersect:void 0,padding:u??Lt()?.block?.padding??0,widthInColumns:n.widthInColumns??1}}async function ZLt(e,t,r){let n=a9e(t,r,!1);if(t.type==="group")return;let i=Lt(),a=await Eh(e,n,{config:i}),o=a.node()?.getBBox()??{width:0,height:0},l=r.getBlock(n.id);l.size={width:o.width,height:o.height,x:0,y:0,node:a},r.setBlock(l),a.remove()}async function QLt(e,t,r){let n=a9e(t,r,!0);if(r.getBlock(n.id).type!=="space"){let a=Lt();await Eh(e,n,{config:a}),t.intersect=n?.intersect,au(n)}}async function Nj(e,t,r,n){for(let i of t)await n(e,i,r),i.children&&await Nj(e,i.children,r,n)}async function s9e(e,t,r){await Nj(e,t,r,ZLt)}async function o9e(e,t,r){await Nj(e,t,r,QLt)}async function l9e(e,t,r,n,i){let a=new Ln({multigraph:!0,compound:!0});a.setGraph({rankdir:"TB",nodesep:10,ranksep:10,marginx:8,marginy:8});for(let o of r)o.size&&a.setNode(o.id,{width:o.size.width,height:o.size.height,intersect:o.intersect});for(let o of t)if(o.start&&o.end){let l=n.getBlock(o.start),u=n.getBlock(o.end);if(l?.size&&u?.size){let h=l.size,d=u.size,f=[{x:h.x,y:h.y},{x:h.x+(d.x-h.x)/2,y:h.y+(d.y-h.y)/2},{x:d.x,y:d.y}],p=i?`${i}-${o.id}`:o.id,m=o.thickness==="thick"?"edge-thickness-thick":"edge-thickness-normal",g=o.pattern==="dotted"?"edge-pattern-dotted":"edge-pattern-solid",y=`${m} ${g} flowchart-link LS-a1 LE-b1`;of(e,{...o,id:p,arrowTypeEnd:o.arrowTypeEnd,arrowTypeStart:o.arrowTypeStart,points:f,classes:y},{},"block",a.node(o.start),a.node(o.end),i),o.label&&(await oc(e,{...o,label:o.label,labelStyle:"stroke: #333; stroke-width: 1.5px;fill:none;",arrowTypeEnd:o.arrowTypeEnd,arrowTypeStart:o.arrowTypeStart,points:f,classes:y}),$y({...o,x:f[1].x,y:f[1].y},{originalPath:f}))}}}var c9e=F(()=>{"use strict";Ko();br();Fy();Lm();er();s(a9e,"getNodeFromBlock");s(ZLt,"calculateBlockSize");s(QLt,"insertBlockPositioned");s(Nj,"performOperations");s(s9e,"calculateBlockSizes");s(o9e,"insertBlocks");s(l9e,"insertEdges")});var JLt,eDt,u9e,h9e=F(()=>{"use strict";Wr();br();St();B4();Vn();i9e();c9e();JLt=s(function(e,t){return t.db.getClasses()},"getClasses"),eDt=s(async function(e,t,r,n){let{securityLevel:i,block:a}=Lt(),o=n.db;o.setDiagramId(t);let l;i==="sandbox"&&(l=ut("#i"+t));let u=i==="sandbox"?ut(l.nodes()[0].contentDocument.body):ut("body"),h=i==="sandbox"?u.select(`[id="${t}"]`):ut(`[id="${t}"]`);Gy(h,["point","circle","cross"],n.type,t);let f=o.getBlocks(),p=o.getBlocksFlat(),m=o.getEdges(),g=h.insert("g").attr("class","block");await s9e(g,f,o);let y=n9e(o);await o9e(g,f,o),await l9e(g,m,p,o,t);let v=g.node()?.getBBox(),x=v&&Number.isFinite(v.width)&&Number.isFinite(v.height)?v:y;if(x){let b=Math.max(1,Math.round(.125*(x.width/x.height))),T=x.height+b+10,w=x.width+10,{useMaxWidth:C}=a;jr(h,T,w,!!C),se.debug("Here Bounds",y,x),h.attr("viewBox",`${x.x-5} ${x.y-5} ${x.width+10} ${x.height+10}`)}},"draw"),u9e={draw:eDt,getClasses:JLt}});var d9e={};sr(d9e,{diagram:()=>tDt});var tDt,f9e=F(()=>{"use strict";qOe();ZOe();JOe();h9e();tDt={parser:WOe,db:KOe,renderer:u9e,styles:QOe}});function oDt(e){return e.some(t=>x9e.test(t))}function lDt(e){for(let t of e){let r=b9e.exec(t);if(r?.index&&r.index>0)return r.index}return 4}function T9e(e,t){return e.replace(/\bline\s+(\d+)\b/gi,(r,n)=>{let i=parseInt(n,10),a=t.get(i);return a?`line ${a}`:r})}function C9e(e){let t=e.split(` +`),r=new Map,n=-1;for(let[u,h]of t.entries())if(h.trim()==="treeView-beta"){n=u;break}if(n===-1)return{text:e,lineMap:r};let i=[];for(let u=n+1;u{"use strict";x9e=/[─━│┃└┗├┣]/,b9e=/[└┗├┣]/,aDt=/[─━]/,g9e=/^[\s│┃]+$/,y9e=/^\s*(title[\t ]|accTitle[\t ]*:|accDescr[\t ]*[:{])/,v9e=/^\s*%%/,sDt=" ";s(oDt,"isBoxDrawingFormat");s(lDt,"inferSegmentWidth");s(T9e,"remapErrorLines");s(C9e,"preprocessBoxDrawing")});var Xu,cDt,uDt,hDt,dDt,fDt,pDt,mDt,ZS,Pj=F(()=>{"use strict";br();Qi();er();B_();$n();Xu=new wp(()=>({cnt:1,stack:[{id:0,level:-1,name:"/",nodeType:"directory",children:[]}]})),cDt=s(()=>{Xu.reset(),Tr()},"clear"),uDt=s(()=>Xu.records.stack[0],"getRoot"),hDt=s(()=>Xu.records.cnt,"getCount"),dDt=mr.treeView,fDt=s(()=>Xr(dDt,Lt().treeView),"getConfig"),pDt=s((e,t,r,n,i,a)=>{for(;e<=Xu.records.stack[Xu.records.stack.length-1].level;)Xu.records.stack.pop();let o={id:Xu.records.cnt++,level:e,name:t,nodeType:r,icon:i,cssClass:n,description:a,children:[]};Xu.records.stack[Xu.records.stack.length-1].children.push(o),Xu.records.stack.push(o)},"addNode"),mDt={clear:cDt,addNode:pDt,getRoot:uDt,getCount:hDt,getConfig:fDt,getAccTitle:Ir,getAccDescription:Nr,getDiagramTitle:Pr,setAccDescription:Mr,setAccTitle:Lr,setDiagramTitle:Gr},ZS=mDt});var gDt,w9e,k9e=F(()=>{"use strict";ls();br();St();Yr();to();S9e();Pj();gDt=s(e=>{Hn(e,ZS);for(let t of e.nodes){let r=typeof t.indent=="number"?t.indent:0,n=t.name,i=n.endsWith("/");i&&(n=n.slice(0,-1));let a=i?"directory":"file",o=t.classAnnotation||void 0,l=t.iconAnnotation,u=l!==void 0?l||"none":void 0,h=t.descAnnotation||void 0,d=h?xr(h,Lt()):void 0;ZS.addNode(r,n,a,o,u,d)}},"populate"),w9e={parse:s(async e=>{let{text:t,lineMap:r}=C9e(e);try{let n=await Li("treeView",t);se.debug(n),gDt(n)}catch(n){throw r.size>0&&n instanceof Error&&(n.message=T9e(n.message,r)),n}},"parse")}});function yDt(e,t){let r=t?.filenameIcons?.[e];if(r)return r;let n=e.lastIndexOf(".");if(n>0){let i=e.substring(n).toLowerCase(),a=t?.extensionIcons;return a?.[i]??a?.[i.slice(1)]}}function E9e(e,t){return e.includes(":")?e:e in hx.icons||!t?`${hx.prefix}:${e}`:`${t}:${e}`}function Oj(e,t){if(e.icon!=="none"){if(e.icon)return E9e(e.icon,t.defaultIconPack);if(t.showIcons){if(e.nodeType==="file"){let r=yDt(e.name,t);if(r==="none")return;if(r)return E9e(r,t.defaultIconPack)}return`${hx.prefix}:${e.nodeType==="directory"?"folder":"file"}`}}}var hx,A9e=F(()=>{"use strict";hx={prefix:"mermaid-treeview",height:24,width:24,icons:{folder:{body:''},file:{body:''}}};s(yDt,"detectIcon");s(E9e,"qualifyIcon");s(Oj,"getNodeIcon")});var Bj,vDt,xDt,bDt,TDt,R9e,CDt,SDt,wDt,_9e,L9e=F(()=>{"use strict";St();Hl();cs();Vn();A9e();ey([{name:hx.prefix,icons:hx}]);Bj=14,vDt=4,xDt=16,bDt=s(async(e,t)=>{let r=[],n=s(a=>{let o=Oj(a,t);o&&r.push({icon:o,node:a}),a.children.forEach(n)},"collect");n(e);let i=await Promise.all(r.map(async({icon:a,node:o})=>({id:o.id,svg:await ps(a,{height:Bj,width:Bj})})));return new Map(i.map(({id:a,svg:o})=>[a,o]))},"resolveNodeIcons"),TDt=s((e,t,r,n,i,a)=>{let o=n.append("g"),l="treeView-node-label";r.nodeType==="directory"&&(l+=" treeView-node-dir"),r.cssClass&&(l+=` ${r.cssClass}`);let u=Bj+vDt,h=Oj(r,i),d=h!==void 0;h&&o.append("g").attr("class","treeView-node-icon").attr("transform",`translate(${e+i.paddingX}, ${t+i.paddingY})`).html(a.get(r.id)??"");let f=o.append("text").text(r.name).attr("dominant-baseline","middle").attr("class",l),{height:p,width:m}=f.node().getBBox(),g=p+i.paddingY*2,y=e+i.paddingX+(d?u:0);f.attr("x",y),f.attr("y",t+g/2);let v=y+m,x=m+i.paddingX*2+(d?u:0);return r.BBox={x:e,y:t,width:x,height:g},r.cssClass?.split(/\s+/).includes("highlight")&&o.insert("rect",":first-child").attr("x",e).attr("y",t+1).attr("width",0).attr("height",g-2).attr("rx",3).attr("class","treeView-highlight-bg"),{node:r,nodeGroup:o,labelRightEdge:v,centerY:t+g/2}},"positionLabel"),R9e=s((e,t,r,n,i,a)=>e.append("line").attr("x1",t).attr("y1",r).attr("x2",n).attr("y2",i).attr("stroke-width",a).attr("class","treeView-node-line"),"positionLine"),CDt=s((e,t,r,n)=>{let i=0,a=0,o=[],l=s((d,f,p,m)=>{let g=m*(p.rowIndent+p.paddingX),y=TDt(g,i,f,d,p,n);o.push(y);let{height:v,width:x}=f.BBox;R9e(d,g-p.rowIndent,i+v/2,g,i+v/2,p.lineThickness),a=Math.max(a,g+x),i+=v},"drawNode"),u=s((d,f=0)=>{l(e,d,r,f),d.children.forEach(y=>{u(y,f+1)});let{x:p,y:m,height:g}=d.BBox;if(d.children.length){let{y,height:v}=d.children[d.children.length-1].BBox;R9e(e,p+r.paddingX,m+g,p+r.paddingX,y+v/2+r.lineThickness/2,r.lineThickness)}},"processNode");u(t);let h=o.filter(d=>d.node.description);if(h.length>0){let f=Math.max(...o.map(p=>p.labelRightEdge))+xDt;for(let p of h){let g=p.nodeGroup.append("text").text(p.node.description).attr("dominant-baseline","middle").attr("class","treeView-node-description").attr("x",f).attr("y",p.centerY).node().getBBox();a=Math.max(a,f+g.width+r.paddingX)}}for(let d of o)if(d.node.cssClass?.split(/\s+/).includes("highlight")){let f=d.nodeGroup.select(".treeView-highlight-bg");if(!f.empty()){let p=a-d.node.BBox.x+8;f.attr("width",p),a=Math.max(a,d.node.BBox.x+p+2)}}return{totalHeight:i,totalWidth:a}},"drawTree"),SDt=s(async(e,t,r,n)=>{se.debug(`Rendering treeView diagram +`+e);let i=n.db,a=i.getRoot(),o=i.getConfig(),l=wn(t),u=l.append("g");u.attr("class","tree-view");let h=await bDt(a,o),{totalHeight:d,totalWidth:f}=CDt(u,a,o,h);l.attr("viewBox",`-${o.lineThickness/2} 0 ${f} ${d}`),jr(l,d,f,o.useMaxWidth)},"draw"),wDt={draw:SDt},_9e=wDt});var kDt,EDt,D9e,I9e=F(()=>{"use strict";er();kDt={labelFontSize:"16px",labelColor:"black",lineColor:"black",iconColor:"#546e7a",descriptionColor:"#6a9955",highlightBg:"rgba(255, 193, 7, 0.15)",highlightStroke:"#ffc107"},EDt=s(({treeView:e})=>{let{labelFontSize:t,labelColor:r,lineColor:n,iconColor:i,descriptionColor:a,highlightBg:o,highlightStroke:l}=Xr(kDt,e);return` + .treeView-node-label { + font-size: ${t}; + fill: ${r}; + white-space: pre; + } + .treeView-node-dir { + font-weight: bold; + } + .treeView-node-line { + stroke: ${n}; + } + .treeView-node-icon { + color: ${i}; + } + .treeView-node-description { + font-size: ${t}; + fill: ${a}; + font-style: italic; + white-space: pre; + } + .treeView-highlight-bg { + fill: ${o}; + stroke: ${l}; + stroke-width: 1; + } + `},"styles"),D9e=EDt});var M9e={};sr(M9e,{diagram:()=>ADt});var ADt,N9e=F(()=>{"use strict";k9e();Pj();L9e();I9e();ADt={db:ZS,renderer:_9e,parser:w9e,styles:D9e}});var $j,Fj,QS,B9e,Gj,ks,Ku,JS,$9e,DDt,ew,F9e,G9e,z9e,V9e,W9e,BL,$L,Np,FL=F(()=>{"use strict";$j={L:"left",R:"right",T:"top",B:"bottom"},Fj={L:s(e=>`${e},${e/2} 0,${e} 0,0`,"L"),R:s(e=>`0,${e/2} ${e},0 ${e},${e}`,"R"),T:s(e=>`0,0 ${e},0 ${e/2},${e}`,"T"),B:s(e=>`${e/2},0 ${e},${e} 0,${e}`,"B")},QS={L:s((e,t)=>e-t+2,"L"),R:s((e,t)=>e-2,"R"),T:s((e,t)=>e-t+2,"T"),B:s((e,t)=>e-2,"B")},B9e=s(function(e){return ks(e)?e==="L"?"R":"L":e==="T"?"B":"T"},"getOppositeArchitectureDirection"),Gj=s(function(e){let t=e;return t==="L"||t==="R"||t==="T"||t==="B"},"isArchitectureDirection"),ks=s(function(e){let t=e;return t==="L"||t==="R"},"isArchitectureDirectionX"),Ku=s(function(e){let t=e;return t==="T"||t==="B"},"isArchitectureDirectionY"),JS=s(function(e,t){let r=ks(e)&&Ku(t),n=Ku(e)&&ks(t);return r||n},"isArchitectureDirectionXY"),$9e=s(function(e){let t=e[0],r=e[1],n=ks(t)&&Ku(r),i=Ku(t)&&ks(r);return n||i},"isArchitecturePairXY"),DDt=s(function(e){return e!=="LL"&&e!=="RR"&&e!=="TT"&&e!=="BB"},"isValidArchitectureDirectionPair"),ew=s(function(e,t){let r=`${e}${t}`;return DDt(r)?r:void 0},"getArchitectureDirectionPair"),F9e=s(function([e,t],r){let n=r[0],i=r[1];return ks(n)?Ku(i)?[e+(n==="L"?-1:1),t+(i==="T"?1:-1)]:[e+(n==="L"?-1:1),t]:ks(i)?[e+(i==="L"?1:-1),t+(n==="T"?1:-1)]:[e,t+(n==="T"?1:-1)]},"shiftPositionByArchitectureDirectionPair"),G9e=s(function(e){return e==="LT"||e==="TL"?[1,1]:e==="BL"||e==="LB"?[1,-1]:e==="BR"||e==="RB"?[-1,-1]:[-1,1]},"getArchitectureDirectionXYFactors"),z9e=s(function(e,t){return JS(e,t)?"bend":ks(e)?"horizontal":"vertical"},"getArchitectureDirectionAlignment"),V9e=s(function(e){return e.type==="service"},"isArchitectureService"),W9e=s(function(e){return e.type==="junction"},"isArchitectureJunction"),BL=s((e,t)=>{let[r,n]=[e,t].sort();return`${JSON.stringify(r)}-${JSON.stringify(n)}`},"architectureGroupAlignmentKey"),$L=s(e=>e.data(),"edgeData"),Np=s(e=>e.data(),"nodeData")});var IDt,dx,zj=F(()=>{"use strict";br();Qi();er();$n();FL();IDt=mr.architecture,dx=class{constructor(){this.nodes=new Map;this.groups=new Map;this.edges=[];this.layoutHints=[];this.registeredIds=new Map;this.elements=new Map;this.diagramId="";this.setAccTitle=Lr;this.getAccTitle=Ir;this.setDiagramTitle=Gr;this.getDiagramTitle=Pr;this.getAccDescription=Nr;this.setAccDescription=Mr;this.clear()}static{s(this,"ArchitectureDB")}setDiagramId(t){this.diagramId=t}getDiagramId(){return this.diagramId}clear(){this.nodes=new Map,this.groups=new Map,this.edges=[],this.layoutHints=[],this.registeredIds=new Map,this.dataStructures=void 0,this.elements=new Map,this.diagramId="",Tr()}addService({id:t,icon:r,in:n,title:i,iconText:a}){if(this.registeredIds.has(t))throw new Error(`The service id [${t}] is already in use by another ${this.registeredIds.get(t)}`);if(n!==void 0){if(t===n)throw new Error(`The service [${t}] cannot be placed within itself`);if(!this.registeredIds.has(n))throw new Error(`The service [${t}]'s parent does not exist. Please make sure the parent is created before this service`);if(this.registeredIds.get(n)==="node")throw new Error(`The service [${t}]'s parent is not a group`)}this.registeredIds.set(t,"node"),this.nodes.set(t,{id:t,type:"service",icon:r,iconText:a,title:i,edges:[],in:n})}getServices(){return[...this.nodes.values()].filter(V9e)}addJunction({id:t,in:r}){if(this.registeredIds.has(t))throw new Error(`The junction id [${t}] is already in use by another ${this.registeredIds.get(t)}`);if(r!==void 0){if(t===r)throw new Error(`The junction [${t}] cannot be placed within itself`);if(!this.registeredIds.has(r))throw new Error(`The junction [${t}]'s parent does not exist. Please make sure the parent is created before this junction`);if(this.registeredIds.get(r)==="node")throw new Error(`The junction [${t}]'s parent is not a group`)}this.registeredIds.set(t,"node"),this.nodes.set(t,{id:t,type:"junction",edges:[],in:r})}getJunctions(){return[...this.nodes.values()].filter(W9e)}getNodes(){return[...this.nodes.values()]}getNode(t){return this.nodes.get(t)??null}addGroup({id:t,icon:r,in:n,title:i}){if(this.registeredIds.has(t))throw new Error(`The group id [${t}] is already in use by another ${this.registeredIds.get(t)}`);if(n!==void 0){if(t===n)throw new Error(`The group [${t}] cannot be placed within itself`);if(!this.registeredIds.has(n))throw new Error(`The group [${t}]'s parent does not exist. Please make sure the parent is created before this group`);if(this.registeredIds.get(n)==="node")throw new Error(`The group [${t}]'s parent is not a group`)}this.registeredIds.set(t,"group"),this.groups.set(t,{id:t,icon:r,title:i,in:n})}getGroups(){return[...this.groups.values()]}addEdge({lhsId:t,rhsId:r,lhsDir:n,rhsDir:i,lhsInto:a,rhsInto:o,lhsGroup:l,rhsGroup:u,title:h}){if(!Gj(n))throw new Error(`Invalid direction given for left hand side of edge ${t}--${r}. Expected (L,R,T,B) got ${String(n)}`);if(!Gj(i))throw new Error(`Invalid direction given for right hand side of edge ${t}--${r}. Expected (L,R,T,B) got ${String(i)}`);if(!this.nodes.has(t)&&!this.groups.has(t))throw new Error(`The left-hand id [${t}] does not yet exist. Please create the service/group before declaring an edge to it.`);if(!this.nodes.has(r)&&!this.groups.has(r))throw new Error(`The right-hand id [${r}] does not yet exist. Please create the service/group before declaring an edge to it.`);let d=this.nodes.get(t).in,f=this.nodes.get(r).in;if(l&&d&&f&&d==f)throw new Error(`The left-hand id [${t}] is modified to traverse the group boundary, but the edge does not pass through two groups.`);if(u&&d&&f&&d==f)throw new Error(`The right-hand id [${r}] is modified to traverse the group boundary, but the edge does not pass through two groups.`);let p={lhsId:t,lhsDir:n,lhsInto:a,lhsGroup:l,rhsId:r,rhsDir:i,rhsInto:o,rhsGroup:u,title:h};this.edges.push(p);let m=this.nodes.get(t),g=this.nodes.get(r);m&&g&&(m.edges.push(this.edges[this.edges.length-1]),g.edges.push(this.edges[this.edges.length-1]))}getEdges(){return this.edges}addLayoutHint(t){if(t.members.length<2)throw new Error(`An align directive requires at least two members; got ${t.members.length}`);let r=new Set;t.members.forEach(n=>{if(this.registeredIds.get(n)!=="node")throw new Error(`align ${t.direction} references [${n}], which is not a service or junction`);if(r.has(n))throw new Error(`align ${t.direction} lists [${n}] more than once`);r.add(n)}),this.layoutHints.push(t)}getLayoutHints(){return this.layoutHints}getDataStructures(){if(this.dataStructures===void 0){let t=new Map,r=new Map;for(let[l,u]of this.nodes.entries()){let h=new Map;for(let d of u.edges){let f=this.getNode(d.lhsId)?.in,p=this.getNode(d.rhsId)?.in;if(f&&p&&f!==p){let m=z9e(d.lhsDir,d.rhsDir);m!=="bend"&&t.set(BL(f,p),m)}if(d.lhsId===l){let m=ew(d.lhsDir,d.rhsDir);m&&h.set(m,d.rhsId)}else{let m=ew(d.rhsDir,d.lhsDir);m&&h.set(m,d.lhsId)}}r.set(l,h)}let n=new Set,i=new Set(r.keys()),a=s(l=>{let u=new Map([[l,[0,0]]]),h=[l];for(;h.length>0;){let d=h.shift();if(d){n.add(d),i.delete(d);let f=r.get(d);if(!f)throw new Error(`BFS error: adjacency list for id ${d} not found. Please report this as a bug.`);let p=u.get(d);if(!p)throw new Error(`BFS error: position for id ${d} not found in spatial map. Please report this as a bug.`);let[m,g]=p;f.forEach((y,v)=>{n.has(y)||(u.set(y,F9e([m,g],v)),h.push(y))})}}return u},"BFS"),o=[];for(;i.size>0;){let l=i.values().next().value;o.push(a(l))}this.dataStructures={adjList:r,spatialMaps:o,groupAlignments:t}}return this.dataStructures}setElementForId(t,r){this.elements.set(t,r)}getElementById(t){return this.elements.get(t)}getConfig(){return Xr({...IDt,...Lt().architecture})}getConfigField(t){return this.getConfig()[t]}}});var MDt,Vj,q9e=F(()=>{"use strict";ls();St();to();zj();MDt=s((e,t)=>{Hn(e,t),e.groups.map(r=>t.addGroup(r)),e.services.map(r=>t.addService({...r,type:"service"})),e.junctions.map(r=>t.addJunction({...r,type:"junction"})),e.edges.map(r=>t.addEdge(r)),e.alignments?.map(r=>t.addLayoutHint({direction:r.direction,members:[...r.members]}))},"populateDb"),Vj={parser:{yy:void 0},parse:s(async e=>{let t=await Li("architecture",e);se.debug(t);let r=Vj.parser?.yy;if(!(r instanceof dx))throw new Error("parser.parser?.yy was not a ArchitectureDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");MDt(t,r)},"parse")}});var NDt,H9e,U9e=F(()=>{"use strict";NDt=s(e=>` + .edge { + stroke-width: ${e.archEdgeWidth}; + stroke: ${e.archEdgeColor}; + fill: none; + } + + .arrow { + fill: ${e.archEdgeArrowColor}; + } + + .node-bkg { + fill: none; + stroke: ${e.archGroupBorderColor}; + stroke-width: ${e.archGroupBorderWidth}; + stroke-dasharray: 8; + } + .node-icon-text { + display: flex; + align-items: center; + } + + .node-icon-text > div { + color: #fff; + margin: 1px; + height: fit-content; + text-align: center; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + } +`,"getStyles"),H9e=NDt});var qj=As((tw,Wj)=>{"use strict";s((function(t,r){typeof tw=="object"&&typeof Wj=="object"?Wj.exports=r():typeof define=="function"&&define.amd?define([],r):typeof tw=="object"?tw.layoutBase=r():t.layoutBase=r()}),"webpackUniversalModuleDefinition")(tw,function(){return(function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return s(r,"__webpack_require__"),r.m=e,r.c=t,r.i=function(n){return n},r.d=function(n,i,a){r.o(n,i)||Object.defineProperty(n,i,{configurable:!1,enumerable:!0,get:a})},r.n=function(n){var i=n&&n.__esModule?s(function(){return n.default},"getDefault"):s(function(){return n},"getModuleExports");return r.d(i,"a",i),i},r.o=function(n,i){return Object.prototype.hasOwnProperty.call(n,i)},r.p="",r(r.s=28)})([(function(e,t,r){"use strict";function n(){}s(n,"LayoutConstants"),n.QUALITY=1,n.DEFAULT_CREATE_BENDS_AS_NEEDED=!1,n.DEFAULT_INCREMENTAL=!1,n.DEFAULT_ANIMATION_ON_LAYOUT=!0,n.DEFAULT_ANIMATION_DURING_LAYOUT=!1,n.DEFAULT_ANIMATION_PERIOD=50,n.DEFAULT_UNIFORM_LEAF_NODE_SIZES=!1,n.DEFAULT_GRAPH_MARGIN=15,n.NODE_DIMENSIONS_INCLUDE_LABELS=!1,n.SIMPLE_NODE_SIZE=40,n.SIMPLE_NODE_HALF_SIZE=n.SIMPLE_NODE_SIZE/2,n.EMPTY_COMPOUND_NODE_SIZE=40,n.MIN_EDGE_LENGTH=1,n.WORLD_BOUNDARY=1e6,n.INITIAL_WORLD_BOUNDARY=n.WORLD_BOUNDARY/1e3,n.WORLD_CENTER_X=1200,n.WORLD_CENTER_Y=900,e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(8),a=r(9);function o(u,h,d){n.call(this,d),this.isOverlapingSourceAndTarget=!1,this.vGraphObject=d,this.bendpoints=[],this.source=u,this.target=h}s(o,"LEdge"),o.prototype=Object.create(n.prototype);for(var l in n)o[l]=n[l];o.prototype.getSource=function(){return this.source},o.prototype.getTarget=function(){return this.target},o.prototype.isInterGraph=function(){return this.isInterGraph},o.prototype.getLength=function(){return this.length},o.prototype.isOverlapingSourceAndTarget=function(){return this.isOverlapingSourceAndTarget},o.prototype.getBendpoints=function(){return this.bendpoints},o.prototype.getLca=function(){return this.lca},o.prototype.getSourceInLca=function(){return this.sourceInLca},o.prototype.getTargetInLca=function(){return this.targetInLca},o.prototype.getOtherEnd=function(u){if(this.source===u)return this.target;if(this.target===u)return this.source;throw"Node is not incident with this edge"},o.prototype.getOtherEndInGraph=function(u,h){for(var d=this.getOtherEnd(u),f=h.getGraphManager().getRoot();;){if(d.getOwner()==h)return d;if(d.getOwner()==f)break;d=d.getOwner().getParent()}return null},o.prototype.updateLength=function(){var u=new Array(4);this.isOverlapingSourceAndTarget=i.getIntersection(this.target.getRect(),this.source.getRect(),u),this.isOverlapingSourceAndTarget||(this.lengthX=u[0]-u[2],this.lengthY=u[1]-u[3],Math.abs(this.lengthX)<1&&(this.lengthX=a.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=a.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY))},o.prototype.updateLengthSimple=function(){this.lengthX=this.target.getCenterX()-this.source.getCenterX(),this.lengthY=this.target.getCenterY()-this.source.getCenterY(),Math.abs(this.lengthX)<1&&(this.lengthX=a.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=a.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY)},e.exports=o}),(function(e,t,r){"use strict";function n(i){this.vGraphObject=i}s(n,"LGraphObject"),e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(10),a=r(13),o=r(0),l=r(16),u=r(5);function h(f,p,m,g){m==null&&g==null&&(g=p),n.call(this,g),f.graphManager!=null&&(f=f.graphManager),this.estimatedSize=i.MIN_VALUE,this.inclusionTreeDepth=i.MAX_VALUE,this.vGraphObject=g,this.edges=[],this.graphManager=f,m!=null&&p!=null?this.rect=new a(p.x,p.y,m.width,m.height):this.rect=new a}s(h,"LNode"),h.prototype=Object.create(n.prototype);for(var d in n)h[d]=n[d];h.prototype.getEdges=function(){return this.edges},h.prototype.getChild=function(){return this.child},h.prototype.getOwner=function(){return this.owner},h.prototype.getWidth=function(){return this.rect.width},h.prototype.setWidth=function(f){this.rect.width=f},h.prototype.getHeight=function(){return this.rect.height},h.prototype.setHeight=function(f){this.rect.height=f},h.prototype.getCenterX=function(){return this.rect.x+this.rect.width/2},h.prototype.getCenterY=function(){return this.rect.y+this.rect.height/2},h.prototype.getCenter=function(){return new u(this.rect.x+this.rect.width/2,this.rect.y+this.rect.height/2)},h.prototype.getLocation=function(){return new u(this.rect.x,this.rect.y)},h.prototype.getRect=function(){return this.rect},h.prototype.getDiagonal=function(){return Math.sqrt(this.rect.width*this.rect.width+this.rect.height*this.rect.height)},h.prototype.getHalfTheDiagonal=function(){return Math.sqrt(this.rect.height*this.rect.height+this.rect.width*this.rect.width)/2},h.prototype.setRect=function(f,p){this.rect.x=f.x,this.rect.y=f.y,this.rect.width=p.width,this.rect.height=p.height},h.prototype.setCenter=function(f,p){this.rect.x=f-this.rect.width/2,this.rect.y=p-this.rect.height/2},h.prototype.setLocation=function(f,p){this.rect.x=f,this.rect.y=p},h.prototype.moveBy=function(f,p){this.rect.x+=f,this.rect.y+=p},h.prototype.getEdgeListToNode=function(f){var p=[],m,g=this;return g.edges.forEach(function(y){if(y.target==f){if(y.source!=g)throw"Incorrect edge source!";p.push(y)}}),p},h.prototype.getEdgesBetween=function(f){var p=[],m,g=this;return g.edges.forEach(function(y){if(!(y.source==g||y.target==g))throw"Incorrect edge source and/or target";(y.target==f||y.source==f)&&p.push(y)}),p},h.prototype.getNeighborsList=function(){var f=new Set,p=this;return p.edges.forEach(function(m){if(m.source==p)f.add(m.target);else{if(m.target!=p)throw"Incorrect incidency!";f.add(m.source)}}),f},h.prototype.withChildren=function(){var f=new Set,p,m;if(f.add(this),this.child!=null)for(var g=this.child.getNodes(),y=0;yp?(this.rect.x-=(this.labelWidth-p)/2,this.setWidth(this.labelWidth)):this.labelPosHorizontal=="right"&&this.setWidth(p+this.labelWidth)),this.labelHeight&&(this.labelPosVertical=="top"?(this.rect.y-=this.labelHeight,this.setHeight(m+this.labelHeight)):this.labelPosVertical=="center"&&this.labelHeight>m?(this.rect.y-=(this.labelHeight-m)/2,this.setHeight(this.labelHeight)):this.labelPosVertical=="bottom"&&this.setHeight(m+this.labelHeight))}}},h.prototype.getInclusionTreeDepth=function(){if(this.inclusionTreeDepth==i.MAX_VALUE)throw"assert failed";return this.inclusionTreeDepth},h.prototype.transform=function(f){var p=this.rect.x;p>o.WORLD_BOUNDARY?p=o.WORLD_BOUNDARY:p<-o.WORLD_BOUNDARY&&(p=-o.WORLD_BOUNDARY);var m=this.rect.y;m>o.WORLD_BOUNDARY?m=o.WORLD_BOUNDARY:m<-o.WORLD_BOUNDARY&&(m=-o.WORLD_BOUNDARY);var g=new u(p,m),y=f.inverseTransformPoint(g);this.setLocation(y.x,y.y)},h.prototype.getLeft=function(){return this.rect.x},h.prototype.getRight=function(){return this.rect.x+this.rect.width},h.prototype.getTop=function(){return this.rect.y},h.prototype.getBottom=function(){return this.rect.y+this.rect.height},h.prototype.getParent=function(){return this.owner==null?null:this.owner.getParent()},e.exports=h}),(function(e,t,r){"use strict";var n=r(0);function i(){}s(i,"FDLayoutConstants");for(var a in n)i[a]=n[a];i.MAX_ITERATIONS=2500,i.DEFAULT_EDGE_LENGTH=50,i.DEFAULT_SPRING_STRENGTH=.45,i.DEFAULT_REPULSION_STRENGTH=4500,i.DEFAULT_GRAVITY_STRENGTH=.4,i.DEFAULT_COMPOUND_GRAVITY_STRENGTH=1,i.DEFAULT_GRAVITY_RANGE_FACTOR=3.8,i.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=1.5,i.DEFAULT_USE_SMART_IDEAL_EDGE_LENGTH_CALCULATION=!0,i.DEFAULT_USE_SMART_REPULSION_RANGE_CALCULATION=!0,i.DEFAULT_COOLING_FACTOR_INCREMENTAL=.3,i.COOLING_ADAPTATION_FACTOR=.33,i.ADAPTATION_LOWER_NODE_LIMIT=1e3,i.ADAPTATION_UPPER_NODE_LIMIT=5e3,i.MAX_NODE_DISPLACEMENT_INCREMENTAL=100,i.MAX_NODE_DISPLACEMENT=i.MAX_NODE_DISPLACEMENT_INCREMENTAL*3,i.MIN_REPULSION_DIST=i.DEFAULT_EDGE_LENGTH/10,i.CONVERGENCE_CHECK_PERIOD=100,i.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=.1,i.MIN_EDGE_LENGTH=1,i.GRID_CALCULATION_CHECK_PERIOD=10,e.exports=i}),(function(e,t,r){"use strict";function n(i,a){i==null&&a==null?(this.x=0,this.y=0):(this.x=i,this.y=a)}s(n,"PointD"),n.prototype.getX=function(){return this.x},n.prototype.getY=function(){return this.y},n.prototype.setX=function(i){this.x=i},n.prototype.setY=function(i){this.y=i},n.prototype.getDifference=function(i){return new DimensionD(this.x-i.x,this.y-i.y)},n.prototype.getCopy=function(){return new n(this.x,this.y)},n.prototype.translate=function(i){return this.x+=i.width,this.y+=i.height,this},e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(10),a=r(0),o=r(7),l=r(3),u=r(1),h=r(13),d=r(12),f=r(11);function p(g,y,v){n.call(this,v),this.estimatedSize=i.MIN_VALUE,this.margin=a.DEFAULT_GRAPH_MARGIN,this.edges=[],this.nodes=[],this.isConnected=!1,this.parent=g,y!=null&&y instanceof o?this.graphManager=y:y!=null&&y instanceof Layout&&(this.graphManager=y.graphManager)}s(p,"LGraph"),p.prototype=Object.create(n.prototype);for(var m in n)p[m]=n[m];p.prototype.getNodes=function(){return this.nodes},p.prototype.getEdges=function(){return this.edges},p.prototype.getGraphManager=function(){return this.graphManager},p.prototype.getParent=function(){return this.parent},p.prototype.getLeft=function(){return this.left},p.prototype.getRight=function(){return this.right},p.prototype.getTop=function(){return this.top},p.prototype.getBottom=function(){return this.bottom},p.prototype.isConnected=function(){return this.isConnected},p.prototype.add=function(g,y,v){if(y==null&&v==null){var x=g;if(this.graphManager==null)throw"Graph has no graph mgr!";if(this.getNodes().indexOf(x)>-1)throw"Node already in graph!";return x.owner=this,this.getNodes().push(x),x}else{var b=g;if(!(this.getNodes().indexOf(y)>-1&&this.getNodes().indexOf(v)>-1))throw"Source or target not in graph!";if(!(y.owner==v.owner&&y.owner==this))throw"Both owners must be this graph!";return y.owner!=v.owner?null:(b.source=y,b.target=v,b.isInterGraph=!1,this.getEdges().push(b),y.edges.push(b),v!=y&&v.edges.push(b),b)}},p.prototype.remove=function(g){var y=g;if(g instanceof l){if(y==null)throw"Node is null!";if(!(y.owner!=null&&y.owner==this))throw"Owner graph is invalid!";if(this.graphManager==null)throw"Owner graph manager is invalid!";for(var v=y.edges.slice(),x,b=v.length,T=0;T-1&&S>-1))throw"Source and/or target doesn't know this edge!";x.source.edges.splice(C,1),x.target!=x.source&&x.target.edges.splice(S,1);var w=x.source.owner.getEdges().indexOf(x);if(w==-1)throw"Not in owner's edge list!";x.source.owner.getEdges().splice(w,1)}},p.prototype.updateLeftTop=function(){for(var g=i.MAX_VALUE,y=i.MAX_VALUE,v,x,b,T=this.getNodes(),w=T.length,C=0;Cv&&(g=v),y>x&&(y=x)}return g==i.MAX_VALUE?null:(T[0].getParent().paddingLeft!=null?b=T[0].getParent().paddingLeft:b=this.margin,this.left=y-b,this.top=g-b,new d(this.left,this.top))},p.prototype.updateBounds=function(g){for(var y=i.MAX_VALUE,v=-i.MAX_VALUE,x=i.MAX_VALUE,b=-i.MAX_VALUE,T,w,C,S,k,R=this.nodes,M=R.length,P=0;PT&&(y=T),vC&&(x=C),bT&&(y=T),vC&&(x=C),b=this.nodes.length){var M=0;v.forEach(function(P){P.owner==g&&M++}),M==this.nodes.length&&(this.isConnected=!0)}},e.exports=p}),(function(e,t,r){"use strict";var n,i=r(1);function a(o){n=r(6),this.layout=o,this.graphs=[],this.edges=[]}s(a,"LGraphManager"),a.prototype.addRoot=function(){var o=this.layout.newGraph(),l=this.layout.newNode(null),u=this.add(o,l);return this.setRootGraph(u),this.rootGraph},a.prototype.add=function(o,l,u,h,d){if(u==null&&h==null&&d==null){if(o==null)throw"Graph is null!";if(l==null)throw"Parent node is null!";if(this.graphs.indexOf(o)>-1)throw"Graph already in this graph mgr!";if(this.graphs.push(o),o.parent!=null)throw"Already has a parent!";if(l.child!=null)throw"Already has a child!";return o.parent=l,l.child=o,o}else{d=u,h=l,u=o;var f=h.getOwner(),p=d.getOwner();if(!(f!=null&&f.getGraphManager()==this))throw"Source not in this graph mgr!";if(!(p!=null&&p.getGraphManager()==this))throw"Target not in this graph mgr!";if(f==p)return u.isInterGraph=!1,f.add(u,h,d);if(u.isInterGraph=!0,u.source=h,u.target=d,this.edges.indexOf(u)>-1)throw"Edge already in inter-graph edge list!";if(this.edges.push(u),!(u.source!=null&&u.target!=null))throw"Edge source and/or target is null!";if(!(u.source.edges.indexOf(u)==-1&&u.target.edges.indexOf(u)==-1))throw"Edge already in source and/or target incidency list!";return u.source.edges.push(u),u.target.edges.push(u),u}},a.prototype.remove=function(o){if(o instanceof n){var l=o;if(l.getGraphManager()!=this)throw"Graph not in this graph mgr";if(!(l==this.rootGraph||l.parent!=null&&l.parent.graphManager==this))throw"Invalid parent node!";var u=[];u=u.concat(l.getEdges());for(var h,d=u.length,f=0;f=o.getRight()?l[0]+=Math.min(o.getX()-a.getX(),a.getRight()-o.getRight()):o.getX()<=a.getX()&&o.getRight()>=a.getRight()&&(l[0]+=Math.min(a.getX()-o.getX(),o.getRight()-a.getRight())),a.getY()<=o.getY()&&a.getBottom()>=o.getBottom()?l[1]+=Math.min(o.getY()-a.getY(),a.getBottom()-o.getBottom()):o.getY()<=a.getY()&&o.getBottom()>=a.getBottom()&&(l[1]+=Math.min(a.getY()-o.getY(),o.getBottom()-a.getBottom()));var d=Math.abs((o.getCenterY()-a.getCenterY())/(o.getCenterX()-a.getCenterX()));o.getCenterY()===a.getCenterY()&&o.getCenterX()===a.getCenterX()&&(d=1);var f=d*l[0],p=l[1]/d;l[0]f)return l[0]=u,l[1]=m,l[2]=d,l[3]=R,!1;if(hd)return l[0]=p,l[1]=h,l[2]=S,l[3]=f,!1;if(ud?(l[0]=y,l[1]=v,_=!0):(l[0]=g,l[1]=m,_=!0):I===L&&(u>d?(l[0]=p,l[1]=m,_=!0):(l[0]=x,l[1]=v,_=!0)),-D===L?d>u?(l[2]=k,l[3]=R,A=!0):(l[2]=S,l[3]=C,A=!0):D===L&&(d>u?(l[2]=w,l[3]=C,A=!0):(l[2]=M,l[3]=R,A=!0)),_&&A)return!1;if(u>d?h>f?(B=this.getCardinalDirection(I,L,4),O=this.getCardinalDirection(D,L,2)):(B=this.getCardinalDirection(-I,L,3),O=this.getCardinalDirection(-D,L,1)):h>f?(B=this.getCardinalDirection(-I,L,1),O=this.getCardinalDirection(-D,L,3)):(B=this.getCardinalDirection(I,L,2),O=this.getCardinalDirection(D,L,4)),!_)switch(B){case 1:W=m,$=u+-T/L,l[0]=$,l[1]=W;break;case 2:$=x,W=h+b*L,l[0]=$,l[1]=W;break;case 3:W=v,$=u+T/L,l[0]=$,l[1]=W;break;case 4:$=y,W=h+-b*L,l[0]=$,l[1]=W;break}if(!A)switch(O){case 1:z=C,V=d+-N/L,l[2]=V,l[3]=z;break;case 2:V=M,z=f+P*L,l[2]=V,l[3]=z;break;case 3:z=R,V=d+N/L,l[2]=V,l[3]=z;break;case 4:V=k,z=f+-P*L,l[2]=V,l[3]=z;break}}return!1},i.getCardinalDirection=function(a,o,l){return a>o?l:1+l%4},i.getIntersection=function(a,o,l,u){if(u==null)return this.getIntersection2(a,o,l);var h=a.x,d=a.y,f=o.x,p=o.y,m=l.x,g=l.y,y=u.x,v=u.y,x=void 0,b=void 0,T=void 0,w=void 0,C=void 0,S=void 0,k=void 0,R=void 0,M=void 0;return T=p-d,C=h-f,k=f*d-h*p,w=v-g,S=m-y,R=y*g-m*v,M=T*S-w*C,M===0?null:(x=(C*R-S*k)/M,b=(w*k-T*R)/M,new n(x,b))},i.angleOfVector=function(a,o,l,u){var h=void 0;return a!==l?(h=Math.atan((u-o)/(l-a)),l=0){var v=(-m+Math.sqrt(m*m-4*p*g))/(2*p),x=(-m-Math.sqrt(m*m-4*p*g))/(2*p),b=null;return v>=0&&v<=1?[v]:x>=0&&x<=1?[x]:b}else return null},i.HALF_PI=.5*Math.PI,i.ONE_AND_HALF_PI=1.5*Math.PI,i.TWO_PI=2*Math.PI,i.THREE_PI=3*Math.PI,e.exports=i}),(function(e,t,r){"use strict";function n(){}s(n,"IMath"),n.sign=function(i){return i>0?1:i<0?-1:0},n.floor=function(i){return i<0?Math.ceil(i):Math.floor(i)},n.ceil=function(i){return i<0?Math.floor(i):Math.ceil(i)},e.exports=n}),(function(e,t,r){"use strict";function n(){}s(n,"Integer"),n.MAX_VALUE=2147483647,n.MIN_VALUE=-2147483648,e.exports=n}),(function(e,t,r){"use strict";var n=(function(){function h(d,f){for(var p=0;p"u"?"undefined":n(a);return a==null||o!="object"&&o!="function"},e.exports=i}),(function(e,t,r){"use strict";function n(m){if(Array.isArray(m)){for(var g=0,y=Array(m.length);g0&&g;){for(T.push(C[0]);T.length>0&&g;){var S=T[0];T.splice(0,1),b.add(S);for(var k=S.getEdges(),x=0;x-1&&C.splice(N,1)}b=new Set,w=new Map}}return m},p.prototype.createDummyNodesForBendpoints=function(m){for(var g=[],y=m.source,v=this.graphManager.calcLowestCommonAncestor(m.source,m.target),x=0;x0){for(var v=this.edgeToDummyNodes.get(y),x=0;x=0&&g.splice(R,1);var M=w.getNeighborsList();M.forEach(function(_){if(y.indexOf(_)<0){var A=v.get(_),I=A-1;I==1&&S.push(_),v.set(_,I)}})}y=y.concat(S),(g.length==1||g.length==2)&&(x=!0,b=g[0])}return b},p.prototype.setGraphManager=function(m){this.graphManager=m},e.exports=p}),(function(e,t,r){"use strict";function n(){}s(n,"RandomSeed"),n.seed=1,n.x=0,n.nextDouble=function(){return n.x=Math.sin(n.seed++)*1e4,n.x-Math.floor(n.x)},e.exports=n}),(function(e,t,r){"use strict";var n=r(5);function i(a,o){this.lworldOrgX=0,this.lworldOrgY=0,this.ldeviceOrgX=0,this.ldeviceOrgY=0,this.lworldExtX=1,this.lworldExtY=1,this.ldeviceExtX=1,this.ldeviceExtY=1}s(i,"Transform"),i.prototype.getWorldOrgX=function(){return this.lworldOrgX},i.prototype.setWorldOrgX=function(a){this.lworldOrgX=a},i.prototype.getWorldOrgY=function(){return this.lworldOrgY},i.prototype.setWorldOrgY=function(a){this.lworldOrgY=a},i.prototype.getWorldExtX=function(){return this.lworldExtX},i.prototype.setWorldExtX=function(a){this.lworldExtX=a},i.prototype.getWorldExtY=function(){return this.lworldExtY},i.prototype.setWorldExtY=function(a){this.lworldExtY=a},i.prototype.getDeviceOrgX=function(){return this.ldeviceOrgX},i.prototype.setDeviceOrgX=function(a){this.ldeviceOrgX=a},i.prototype.getDeviceOrgY=function(){return this.ldeviceOrgY},i.prototype.setDeviceOrgY=function(a){this.ldeviceOrgY=a},i.prototype.getDeviceExtX=function(){return this.ldeviceExtX},i.prototype.setDeviceExtX=function(a){this.ldeviceExtX=a},i.prototype.getDeviceExtY=function(){return this.ldeviceExtY},i.prototype.setDeviceExtY=function(a){this.ldeviceExtY=a},i.prototype.transformX=function(a){var o=0,l=this.lworldExtX;return l!=0&&(o=this.ldeviceOrgX+(a-this.lworldOrgX)*this.ldeviceExtX/l),o},i.prototype.transformY=function(a){var o=0,l=this.lworldExtY;return l!=0&&(o=this.ldeviceOrgY+(a-this.lworldOrgY)*this.ldeviceExtY/l),o},i.prototype.inverseTransformX=function(a){var o=0,l=this.ldeviceExtX;return l!=0&&(o=this.lworldOrgX+(a-this.ldeviceOrgX)*this.lworldExtX/l),o},i.prototype.inverseTransformY=function(a){var o=0,l=this.ldeviceExtY;return l!=0&&(o=this.lworldOrgY+(a-this.ldeviceOrgY)*this.lworldExtY/l),o},i.prototype.inverseTransformPoint=function(a){var o=new n(this.inverseTransformX(a.x),this.inverseTransformY(a.y));return o},e.exports=i}),(function(e,t,r){"use strict";function n(f){if(Array.isArray(f)){for(var p=0,m=Array(f.length);pa.ADAPTATION_LOWER_NODE_LIMIT&&(this.coolingFactor=Math.max(this.coolingFactor*a.COOLING_ADAPTATION_FACTOR,this.coolingFactor-(f-a.ADAPTATION_LOWER_NODE_LIMIT)/(a.ADAPTATION_UPPER_NODE_LIMIT-a.ADAPTATION_LOWER_NODE_LIMIT)*this.coolingFactor*(1-a.COOLING_ADAPTATION_FACTOR))),this.maxNodeDisplacement=a.MAX_NODE_DISPLACEMENT_INCREMENTAL):(f>a.ADAPTATION_LOWER_NODE_LIMIT?this.coolingFactor=Math.max(a.COOLING_ADAPTATION_FACTOR,1-(f-a.ADAPTATION_LOWER_NODE_LIMIT)/(a.ADAPTATION_UPPER_NODE_LIMIT-a.ADAPTATION_LOWER_NODE_LIMIT)*(1-a.COOLING_ADAPTATION_FACTOR)):this.coolingFactor=1,this.initialCoolingFactor=this.coolingFactor,this.maxNodeDisplacement=a.MAX_NODE_DISPLACEMENT),this.maxIterations=Math.max(this.getAllNodes().length*5,this.maxIterations),this.displacementThresholdPerNode=3*a.DEFAULT_EDGE_LENGTH/100,this.totalDisplacementThreshold=this.displacementThresholdPerNode*this.getAllNodes().length,this.repulsionRange=this.calcRepulsionRange()},h.prototype.calcSpringForces=function(){for(var f=this.getAllEdges(),p,m=0;m0&&arguments[0]!==void 0?arguments[0]:!0,p=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1,m,g,y,v,x=this.getAllNodes(),b;if(this.useFRGridVariant)for(this.totalIterations%a.GRID_CALCULATION_CHECK_PERIOD==1&&f&&this.updateGrid(),b=new Set,m=0;mT||b>T)&&(f.gravitationForceX=-this.gravityConstant*y,f.gravitationForceY=-this.gravityConstant*v)):(T=p.getEstimatedSize()*this.compoundGravityRangeFactor,(x>T||b>T)&&(f.gravitationForceX=-this.gravityConstant*y*this.compoundGravityConstant,f.gravitationForceY=-this.gravityConstant*v*this.compoundGravityConstant))},h.prototype.isConverged=function(){var f,p=!1;return this.totalIterations>this.maxIterations/3&&(p=Math.abs(this.totalDisplacement-this.oldTotalDisplacement)<2),f=this.totalDisplacement=x.length||T>=x[0].length)){for(var w=0;wh},"_defaultCompareFunction")}]),l})();e.exports=o}),(function(e,t,r){"use strict";function n(){}s(n,"SVD"),n.svd=function(i){this.U=null,this.V=null,this.s=null,this.m=0,this.n=0,this.m=i.length,this.n=i[0].length;var a=Math.min(this.m,this.n);this.s=(function(Rt){for(var gt=[];Rt-- >0;)gt.push(0);return gt})(Math.min(this.m+1,this.n)),this.U=(function(Rt){var gt=s(function be(ct){if(ct.length==0)return 0;for(var xt=[],Ke=0;Ke0;)gt.push(0);return gt})(this.n),l=(function(Rt){for(var gt=[];Rt-- >0;)gt.push(0);return gt})(this.m),u=!0,h=!0,d=Math.min(this.m-1,this.n),f=Math.max(0,Math.min(this.n-2,this.m)),p=0;p=0;L--)if(this.s[L]!==0){for(var B=L+1;B=0;K--){if((function(Rt,gt){return Rt&>})(K0;){var we=void 0,Fe=void 0;for(we=A-2;we>=-1&&we!==-1;we--)if(Math.abs(o[we])<=Ee+de*(Math.abs(this.s[we])+Math.abs(this.s[we+1]))){o[we]=0;break}if(we===A-2)Fe=4;else{var Ye=void 0;for(Ye=A-1;Ye>=we&&Ye!==we;Ye--){var He=(Ye!==A?Math.abs(o[Ye]):0)+(Ye!==we+1?Math.abs(o[Ye-1]):0);if(Math.abs(this.s[Ye])<=Ee+de*He){this.s[Ye]=0;break}}Ye===we?Fe=3:Ye===A-1?Fe=1:(Fe=2,we=Ye)}switch(we++,Fe){case 1:{var ye=o[A-2];o[A-2]=0;for(var Be=A-2;Be>=we;Be--){var Ie=n.hypot(this.s[Be],ye),Ne=this.s[Be]/Ie,re=ye/Ie;if(this.s[Be]=Ie,Be!==we&&(ye=-re*o[Be-1],o[Be-1]=Ne*o[Be-1]),h)for(var ue=0;ue=this.s[we+1]);){var mt=this.s[we];if(this.s[we]=this.s[we+1],this.s[we+1]=mt,h&&weMath.abs(a)?(o=a/i,o=Math.abs(i)*Math.sqrt(1+o*o)):a!=0?(o=i/a,o=Math.abs(a)*Math.sqrt(1+o*o)):o=0,o},e.exports=n}),(function(e,t,r){"use strict";var n=(function(){function o(l,u){for(var h=0;h2&&arguments[2]!==void 0?arguments[2]:1,d=arguments.length>3&&arguments[3]!==void 0?arguments[3]:-1,f=arguments.length>4&&arguments[4]!==void 0?arguments[4]:-1;i(this,o),this.sequence1=l,this.sequence2=u,this.match_score=h,this.mismatch_penalty=d,this.gap_penalty=f,this.iMax=l.length+1,this.jMax=u.length+1,this.grid=new Array(this.iMax);for(var p=0;p=0;l--){var u=this.listeners[l];u.event===a&&u.callback===o&&this.listeners.splice(l,1)}},i.emit=function(a,o){for(var l=0;l{"use strict";s((function(t,r){typeof rw=="object"&&typeof Hj=="object"?Hj.exports=r(qj()):typeof define=="function"&&define.amd?define(["layout-base"],r):typeof rw=="object"?rw.coseBase=r(qj()):t.coseBase=r(t.layoutBase)}),"webpackUniversalModuleDefinition")(rw,function(e){return(()=>{"use strict";var t={45:((a,o,l)=>{var u={};u.layoutBase=l(551),u.CoSEConstants=l(806),u.CoSEEdge=l(767),u.CoSEGraph=l(880),u.CoSEGraphManager=l(578),u.CoSELayout=l(765),u.CoSENode=l(991),u.ConstraintHandler=l(902),a.exports=u}),806:((a,o,l)=>{var u=l(551).FDLayoutConstants;function h(){}s(h,"CoSEConstants");for(var d in u)h[d]=u[d];h.DEFAULT_USE_MULTI_LEVEL_SCALING=!1,h.DEFAULT_RADIAL_SEPARATION=u.DEFAULT_EDGE_LENGTH,h.DEFAULT_COMPONENT_SEPERATION=60,h.TILE=!0,h.TILING_PADDING_VERTICAL=10,h.TILING_PADDING_HORIZONTAL=10,h.TRANSFORM_ON_CONSTRAINT_HANDLING=!0,h.ENFORCE_CONSTRAINTS=!0,h.APPLY_LAYOUT=!0,h.RELAX_MOVEMENT_ON_CONSTRAINTS=!0,h.TREE_REDUCTION_ON_INCREMENTAL=!0,h.PURE_INCREMENTAL=h.DEFAULT_INCREMENTAL,a.exports=h}),767:((a,o,l)=>{var u=l(551).FDLayoutEdge;function h(f,p,m){u.call(this,f,p,m)}s(h,"CoSEEdge"),h.prototype=Object.create(u.prototype);for(var d in u)h[d]=u[d];a.exports=h}),880:((a,o,l)=>{var u=l(551).LGraph;function h(f,p,m){u.call(this,f,p,m)}s(h,"CoSEGraph"),h.prototype=Object.create(u.prototype);for(var d in u)h[d]=u[d];a.exports=h}),578:((a,o,l)=>{var u=l(551).LGraphManager;function h(f){u.call(this,f)}s(h,"CoSEGraphManager"),h.prototype=Object.create(u.prototype);for(var d in u)h[d]=u[d];a.exports=h}),765:((a,o,l)=>{var u=l(551).FDLayout,h=l(578),d=l(880),f=l(991),p=l(767),m=l(806),g=l(902),y=l(551).FDLayoutConstants,v=l(551).LayoutConstants,x=l(551).Point,b=l(551).PointD,T=l(551).DimensionD,w=l(551).Layout,C=l(551).Integer,S=l(551).IGeometry,k=l(551).LGraph,R=l(551).Transform,M=l(551).LinkedList;function P(){u.call(this),this.toBeTiled={},this.constraints={}}s(P,"CoSELayout"),P.prototype=Object.create(u.prototype);for(var N in u)P[N]=u[N];P.prototype.newGraphManager=function(){var _=new h(this);return this.graphManager=_,_},P.prototype.newGraph=function(_){return new d(null,this.graphManager,_)},P.prototype.newNode=function(_){return new f(this.graphManager,_)},P.prototype.newEdge=function(_){return new p(null,null,_)},P.prototype.initParameters=function(){u.prototype.initParameters.call(this,arguments),this.isSubLayout||(m.DEFAULT_EDGE_LENGTH<10?this.idealEdgeLength=10:this.idealEdgeLength=m.DEFAULT_EDGE_LENGTH,this.useSmartIdealEdgeLengthCalculation=m.DEFAULT_USE_SMART_IDEAL_EDGE_LENGTH_CALCULATION,this.gravityConstant=y.DEFAULT_GRAVITY_STRENGTH,this.compoundGravityConstant=y.DEFAULT_COMPOUND_GRAVITY_STRENGTH,this.gravityRangeFactor=y.DEFAULT_GRAVITY_RANGE_FACTOR,this.compoundGravityRangeFactor=y.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR,this.prunedNodesAll=[],this.growTreeIterations=0,this.afterGrowthIterations=0,this.isTreeGrowing=!1,this.isGrowthFinished=!1)},P.prototype.initSpringEmbedder=function(){u.prototype.initSpringEmbedder.call(this),this.coolingCycle=0,this.maxCoolingCycle=this.maxIterations/y.CONVERGENCE_CHECK_PERIOD,this.finalTemperature=.04,this.coolingAdjuster=1},P.prototype.layout=function(){var _=v.DEFAULT_CREATE_BENDS_AS_NEEDED;return _&&(this.createBendpoints(),this.graphManager.resetAllEdges()),this.level=0,this.classicLayout()},P.prototype.classicLayout=function(){if(this.nodesWithGravity=this.calculateNodesToApplyGravitationTo(),this.graphManager.setAllNodesToApplyGravitation(this.nodesWithGravity),this.calcNoOfChildrenForAllNodes(),this.graphManager.calcLowestCommonAncestors(),this.graphManager.calcInclusionTreeDepths(),this.graphManager.getRoot().calcEstimatedSize(),this.calcIdealEdgeLengths(),this.incremental){if(m.TREE_REDUCTION_ON_INCREMENTAL){this.reduceTrees(),this.graphManager.resetAllNodesToApplyGravitation();var A=new Set(this.getAllNodes()),I=this.nodesWithGravity.filter(function(B){return A.has(B)});this.graphManager.setAllNodesToApplyGravitation(I)}}else{var _=this.getFlatForest();if(_.length>0)this.positionNodesRadially(_);else{this.reduceTrees(),this.graphManager.resetAllNodesToApplyGravitation();var A=new Set(this.getAllNodes()),I=this.nodesWithGravity.filter(function(D){return A.has(D)});this.graphManager.setAllNodesToApplyGravitation(I),this.positionNodesRandomly()}}return Object.keys(this.constraints).length>0&&(g.handleConstraints(this),this.initConstraintVariables()),this.initSpringEmbedder(),m.APPLY_LAYOUT&&this.runSpringEmbedder(),!0},P.prototype.tick=function(){if(this.totalIterations++,this.totalIterations===this.maxIterations&&!this.isTreeGrowing&&!this.isGrowthFinished)if(this.prunedNodesAll.length>0)this.isTreeGrowing=!0;else return!0;if(this.totalIterations%y.CONVERGENCE_CHECK_PERIOD==0&&!this.isTreeGrowing&&!this.isGrowthFinished){if(this.isConverged())if(this.prunedNodesAll.length>0)this.isTreeGrowing=!0;else return!0;this.coolingCycle++,this.layoutQuality==0?this.coolingAdjuster=this.coolingCycle:this.layoutQuality==1&&(this.coolingAdjuster=this.coolingCycle/3),this.coolingFactor=Math.max(this.initialCoolingFactor-Math.pow(this.coolingCycle,Math.log(100*(this.initialCoolingFactor-this.finalTemperature))/Math.log(this.maxCoolingCycle))/100*this.coolingAdjuster,this.finalTemperature),this.animationPeriod=Math.ceil(this.initialAnimationPeriod*Math.sqrt(this.coolingFactor))}if(this.isTreeGrowing){if(this.growTreeIterations%10==0)if(this.prunedNodesAll.length>0){this.graphManager.updateBounds(),this.updateGrid(),this.growTree(this.prunedNodesAll),this.graphManager.resetAllNodesToApplyGravitation();var _=new Set(this.getAllNodes()),A=this.nodesWithGravity.filter(function(L){return _.has(L)});this.graphManager.setAllNodesToApplyGravitation(A),this.graphManager.updateBounds(),this.updateGrid(),m.PURE_INCREMENTAL?this.coolingFactor=y.DEFAULT_COOLING_FACTOR_INCREMENTAL/2:this.coolingFactor=y.DEFAULT_COOLING_FACTOR_INCREMENTAL}else this.isTreeGrowing=!1,this.isGrowthFinished=!0;this.growTreeIterations++}if(this.isGrowthFinished){if(this.isConverged())return!0;this.afterGrowthIterations%10==0&&(this.graphManager.updateBounds(),this.updateGrid()),m.PURE_INCREMENTAL?this.coolingFactor=y.DEFAULT_COOLING_FACTOR_INCREMENTAL/2*((100-this.afterGrowthIterations)/100):this.coolingFactor=y.DEFAULT_COOLING_FACTOR_INCREMENTAL*((100-this.afterGrowthIterations)/100),this.afterGrowthIterations++}var I=!this.isTreeGrowing&&!this.isGrowthFinished,D=this.growTreeIterations%10==1&&this.isTreeGrowing||this.afterGrowthIterations%10==1&&this.isGrowthFinished;return this.totalDisplacement=0,this.graphManager.updateBounds(),this.calcSpringForces(),this.calcRepulsionForces(I,D),this.calcGravitationalForces(),this.moveNodes(),this.animate(),!1},P.prototype.getPositionsData=function(){for(var _=this.graphManager.getAllNodes(),A={},I=0;I<_.length;I++){var D=_[I].rect,L=_[I].id;A[L]={id:L,x:D.getCenterX(),y:D.getCenterY(),w:D.width,h:D.height}}return A},P.prototype.runSpringEmbedder=function(){this.initialAnimationPeriod=25,this.animationPeriod=this.initialAnimationPeriod;var _=!1;if(y.ANIMATE==="during")this.emit("layoutstarted");else{for(;!_;)_=this.tick();this.graphManager.updateBounds()}},P.prototype.moveNodes=function(){for(var _=this.getAllNodes(),A,I=0;I<_.length;I++)A=_[I],A.calculateDisplacement();Object.keys(this.constraints).length>0&&this.updateDisplacements();for(var I=0;I<_.length;I++)A=_[I],A.move()},P.prototype.initConstraintVariables=function(){var _=this;this.idToNodeMap=new Map,this.fixedNodeSet=new Set;for(var A=this.graphManager.getAllNodes(),I=0;I0&&(D.fixedNodeWeight=B)}}if(this.constraints.relativePlacementConstraint){var O=new Map,$=new Map;if(this.dummyToNodeForVerticalAlignment=new Map,this.dummyToNodeForHorizontalAlignment=new Map,this.fixedNodesOnHorizontal=new Set,this.fixedNodesOnVertical=new Set,this.fixedNodeSet.forEach(function(Z){_.fixedNodesOnHorizontal.add(Z),_.fixedNodesOnVertical.add(Z)}),this.constraints.alignmentConstraint){if(this.constraints.alignmentConstraint.vertical)for(var W=this.constraints.alignmentConstraint.vertical,I=0;I=2*Z.length/3;fe--)pe=Math.floor(Math.random()*(fe+1)),te=Z[fe],Z[fe]=Z[pe],Z[pe]=te;return Z},this.nodesInRelativeHorizontal=[],this.nodesInRelativeVertical=[],this.nodeToRelativeConstraintMapHorizontal=new Map,this.nodeToRelativeConstraintMapVertical=new Map,this.nodeToTempPositionMapHorizontal=new Map,this.nodeToTempPositionMapVertical=new Map,this.constraints.relativePlacementConstraint.forEach(function(Z){if(Z.left){var pe=O.has(Z.left)?O.get(Z.left):Z.left,te=O.has(Z.right)?O.get(Z.right):Z.right;_.nodesInRelativeHorizontal.includes(pe)||(_.nodesInRelativeHorizontal.push(pe),_.nodeToRelativeConstraintMapHorizontal.set(pe,[]),_.dummyToNodeForVerticalAlignment.has(pe)?_.nodeToTempPositionMapHorizontal.set(pe,_.idToNodeMap.get(_.dummyToNodeForVerticalAlignment.get(pe)[0]).getCenterX()):_.nodeToTempPositionMapHorizontal.set(pe,_.idToNodeMap.get(pe).getCenterX())),_.nodesInRelativeHorizontal.includes(te)||(_.nodesInRelativeHorizontal.push(te),_.nodeToRelativeConstraintMapHorizontal.set(te,[]),_.dummyToNodeForVerticalAlignment.has(te)?_.nodeToTempPositionMapHorizontal.set(te,_.idToNodeMap.get(_.dummyToNodeForVerticalAlignment.get(te)[0]).getCenterX()):_.nodeToTempPositionMapHorizontal.set(te,_.idToNodeMap.get(te).getCenterX())),_.nodeToRelativeConstraintMapHorizontal.get(pe).push({right:te,gap:Z.gap}),_.nodeToRelativeConstraintMapHorizontal.get(te).push({left:pe,gap:Z.gap})}else{var fe=$.has(Z.top)?$.get(Z.top):Z.top,ce=$.has(Z.bottom)?$.get(Z.bottom):Z.bottom;_.nodesInRelativeVertical.includes(fe)||(_.nodesInRelativeVertical.push(fe),_.nodeToRelativeConstraintMapVertical.set(fe,[]),_.dummyToNodeForHorizontalAlignment.has(fe)?_.nodeToTempPositionMapVertical.set(fe,_.idToNodeMap.get(_.dummyToNodeForHorizontalAlignment.get(fe)[0]).getCenterY()):_.nodeToTempPositionMapVertical.set(fe,_.idToNodeMap.get(fe).getCenterY())),_.nodesInRelativeVertical.includes(ce)||(_.nodesInRelativeVertical.push(ce),_.nodeToRelativeConstraintMapVertical.set(ce,[]),_.dummyToNodeForHorizontalAlignment.has(ce)?_.nodeToTempPositionMapVertical.set(ce,_.idToNodeMap.get(_.dummyToNodeForHorizontalAlignment.get(ce)[0]).getCenterY()):_.nodeToTempPositionMapVertical.set(ce,_.idToNodeMap.get(ce).getCenterY())),_.nodeToRelativeConstraintMapVertical.get(fe).push({bottom:ce,gap:Z.gap}),_.nodeToRelativeConstraintMapVertical.get(ce).push({top:fe,gap:Z.gap})}});else{var z=new Map,H=new Map;this.constraints.relativePlacementConstraint.forEach(function(Z){if(Z.left){var pe=O.has(Z.left)?O.get(Z.left):Z.left,te=O.has(Z.right)?O.get(Z.right):Z.right;z.has(pe)?z.get(pe).push(te):z.set(pe,[te]),z.has(te)?z.get(te).push(pe):z.set(te,[pe])}else{var fe=$.has(Z.top)?$.get(Z.top):Z.top,ce=$.has(Z.bottom)?$.get(Z.bottom):Z.bottom;H.has(fe)?H.get(fe).push(ce):H.set(fe,[ce]),H.has(ce)?H.get(ce).push(fe):H.set(ce,[fe])}});var K=s(function(pe,te){var fe=[],ce=[],de=new M,Ee=new Set,we=0;return pe.forEach(function(Fe,Ye){if(!Ee.has(Ye)){fe[we]=[],ce[we]=!1;var He=Ye;for(de.push(He),Ee.add(He),fe[we].push(He);de.length!=0;){He=de.shift(),te.has(He)&&(ce[we]=!0);var ye=pe.get(He);ye.forEach(function(Be){Ee.has(Be)||(de.push(Be),Ee.add(Be),fe[we].push(Be))})}we++}}),{components:fe,isFixed:ce}},"constructComponents"),J=K(z,_.fixedNodesOnHorizontal);this.componentsOnHorizontal=J.components,this.fixedComponentsOnHorizontal=J.isFixed;var ne=K(H,_.fixedNodesOnVertical);this.componentsOnVertical=ne.components,this.fixedComponentsOnVertical=ne.isFixed}}},P.prototype.updateDisplacements=function(){var _=this;if(this.constraints.fixedNodeConstraint&&this.constraints.fixedNodeConstraint.forEach(function(ne){var Z=_.idToNodeMap.get(ne.nodeId);Z.displacementX=0,Z.displacementY=0}),this.constraints.alignmentConstraint){if(this.constraints.alignmentConstraint.vertical)for(var A=this.constraints.alignmentConstraint.vertical,I=0;I1){var $;for($=0;$D&&(D=Math.floor(O.y)),B=Math.floor(O.x+m.DEFAULT_COMPONENT_SEPERATION)}this.transform(new b(v.WORLD_CENTER_X-O.x/2,v.WORLD_CENTER_Y-O.y/2))},P.radialLayout=function(_,A,I){var D=Math.max(this.maxDiagonalInTree(_),m.DEFAULT_RADIAL_SEPARATION);P.branchRadialLayout(A,null,0,359,0,D);var L=k.calculateBounds(_),B=new R;B.setDeviceOrgX(L.getMinX()),B.setDeviceOrgY(L.getMinY()),B.setWorldOrgX(I.x),B.setWorldOrgY(I.y);for(var O=0;O<_.length;O++){var $=_[O];$.transform(B)}var W=new b(L.getMaxX(),L.getMaxY());return B.inverseTransformPoint(W)},P.branchRadialLayout=function(_,A,I,D,L,B){var O=(D-I+1)/2;O<0&&(O+=180);var $=(O+I)%360,W=$*S.TWO_PI/360,V=Math.cos(W),z=L*Math.cos(W),H=L*Math.sin(W);_.setCenter(z,H);var K=[];K=K.concat(_.getEdges());var J=K.length;A!=null&&J--;for(var ne=0,Z=K.length,pe,te=_.getEdgesBetween(A);te.length>1;){var fe=te[0];te.splice(0,1);var ce=K.indexOf(fe);ce>=0&&K.splice(ce,1),Z--,J--}A!=null?pe=(K.indexOf(te[0])+1)%Z:pe=0;for(var de=Math.abs(D-I)/J,Ee=pe;ne!=J;Ee=++Ee%Z){var we=K[Ee].getOtherEnd(_);if(we!=A){var Fe=(I+ne*de)%360,Ye=(Fe+de)%360;P.branchRadialLayout(we,_,Fe,Ye,L+B,B),ne++}}},P.maxDiagonalInTree=function(_){for(var A=C.MIN_VALUE,I=0;I<_.length;I++){var D=_[I],L=D.getDiagonal();L>A&&(A=L)}return A},P.prototype.calcRepulsionRange=function(){return 2*(this.level+1)*this.idealEdgeLength},P.prototype.groupZeroDegreeMembers=function(){var _=this,A={};this.memberGroups={},this.idToDummyNode={};for(var I=[],D=this.graphManager.getAllNodes(),L=0;L"u"&&(A[$]=[]),A[$]=A[$].concat(B)}Object.keys(A).forEach(function(W){if(A[W].length>1){var V="DummyCompound_"+W;_.memberGroups[V]=A[W];var z=A[W][0].getParent(),H=new f(_.graphManager);H.id=V,H.paddingLeft=z.paddingLeft||0,H.paddingRight=z.paddingRight||0,H.paddingBottom=z.paddingBottom||0,H.paddingTop=z.paddingTop||0,_.idToDummyNode[V]=H;var K=_.getGraphManager().add(_.newGraph(),H),J=z.getChild();J.add(H);for(var ne=0;neL?(D.rect.x-=(D.labelWidth-L)/2,D.setWidth(D.labelWidth),D.labelMarginLeft=(D.labelWidth-L)/2):D.labelPosHorizontal=="right"&&D.setWidth(L+D.labelWidth)),D.labelHeight&&(D.labelPosVertical=="top"?(D.rect.y-=D.labelHeight,D.setHeight(B+D.labelHeight),D.labelMarginTop=D.labelHeight):D.labelPosVertical=="center"&&D.labelHeight>B?(D.rect.y-=(D.labelHeight-B)/2,D.setHeight(D.labelHeight),D.labelMarginTop=(D.labelHeight-B)/2):D.labelPosVertical=="bottom"&&D.setHeight(B+D.labelHeight))}})},P.prototype.repopulateCompounds=function(){for(var _=this.compoundOrder.length-1;_>=0;_--){var A=this.compoundOrder[_],I=A.id,D=A.paddingLeft,L=A.paddingTop,B=A.labelMarginLeft,O=A.labelMarginTop;this.adjustLocations(this.tiledMemberPack[I],A.rect.x,A.rect.y,D,L,B,O)}},P.prototype.repopulateZeroDegreeMembers=function(){var _=this,A=this.tiledZeroDegreePack;Object.keys(A).forEach(function(I){var D=_.idToDummyNode[I],L=D.paddingLeft,B=D.paddingTop,O=D.labelMarginLeft,$=D.labelMarginTop;_.adjustLocations(A[I],D.rect.x,D.rect.y,L,B,O,$)})},P.prototype.getToBeTiled=function(_){var A=_.id;if(this.toBeTiled[A]!=null)return this.toBeTiled[A];var I=_.getChild();if(I==null)return this.toBeTiled[A]=!1,!1;for(var D=I.getNodes(),L=0;L0)return this.toBeTiled[A]=!1,!1;if(B.getChild()==null){this.toBeTiled[B.id]=!1;continue}if(!this.getToBeTiled(B))return this.toBeTiled[A]=!1,!1}return this.toBeTiled[A]=!0,!0},P.prototype.getNodeDegree=function(_){for(var A=_.id,I=_.getEdges(),D=0,L=0;Lz&&(z=K.rect.height)}I+=z+_.verticalPadding}},P.prototype.tileCompoundMembers=function(_,A){var I=this;this.tiledMemberPack=[],Object.keys(_).forEach(function(D){var L=A[D];if(I.tiledMemberPack[D]=I.tileNodes(_[D],L.paddingLeft+L.paddingRight),L.rect.width=I.tiledMemberPack[D].width,L.rect.height=I.tiledMemberPack[D].height,L.setCenter(I.tiledMemberPack[D].centerX,I.tiledMemberPack[D].centerY),L.labelMarginLeft=0,L.labelMarginTop=0,m.NODE_DIMENSIONS_INCLUDE_LABELS){var B=L.rect.width,O=L.rect.height;L.labelWidth&&(L.labelPosHorizontal=="left"?(L.rect.x-=L.labelWidth,L.setWidth(B+L.labelWidth),L.labelMarginLeft=L.labelWidth):L.labelPosHorizontal=="center"&&L.labelWidth>B?(L.rect.x-=(L.labelWidth-B)/2,L.setWidth(L.labelWidth),L.labelMarginLeft=(L.labelWidth-B)/2):L.labelPosHorizontal=="right"&&L.setWidth(B+L.labelWidth)),L.labelHeight&&(L.labelPosVertical=="top"?(L.rect.y-=L.labelHeight,L.setHeight(O+L.labelHeight),L.labelMarginTop=L.labelHeight):L.labelPosVertical=="center"&&L.labelHeight>O?(L.rect.y-=(L.labelHeight-O)/2,L.setHeight(L.labelHeight),L.labelMarginTop=(L.labelHeight-O)/2):L.labelPosVertical=="bottom"&&L.setHeight(O+L.labelHeight))}})},P.prototype.tileNodes=function(_,A){var I=this.tileNodesByFavoringDim(_,A,!0),D=this.tileNodesByFavoringDim(_,A,!1),L=this.getOrgRatio(I),B=this.getOrgRatio(D),O;return B$&&($=ne.getWidth())});var W=B/L,V=O/L,z=Math.pow(I-D,2)+4*(W+D)*(V+I)*L,H=(D-I+Math.sqrt(z))/(2*(W+D)),K;A?(K=Math.ceil(H),K==H&&K++):K=Math.floor(H);var J=K*(W+D)-D;return $>J&&(J=$),J+=D*2,J},P.prototype.tileNodesByFavoringDim=function(_,A,I){var D=m.TILING_PADDING_VERTICAL,L=m.TILING_PADDING_HORIZONTAL,B=m.TILING_COMPARE_BY,O={rows:[],rowWidth:[],rowHeight:[],width:0,height:A,verticalPadding:D,horizontalPadding:L,centerX:0,centerY:0};B&&(O.idealRowWidth=this.calcIdealRowWidth(_,I));var $=s(function(Z){return Z.rect.width*Z.rect.height},"getNodeArea"),W=s(function(Z,pe){return $(pe)-$(Z)},"areaCompareFcn");_.sort(function(ne,Z){var pe=W;return O.idealRowWidth?(pe=B,pe(ne.id,Z.id)):pe(ne,Z)});for(var V=0,z=0,H=0;H<_.length;H++){var K=_[H];V+=K.getCenterX(),z+=K.getCenterY()}O.centerX=V/_.length,O.centerY=z/_.length;for(var H=0;H<_.length;H++){var K=_[H];if(O.rows.length==0)this.insertNodeToRow(O,K,0,A);else if(this.canAddHorizontal(O,K.rect.width,K.rect.height)){var J=O.rows.length-1;O.idealRowWidth||(J=this.getShortestRowIndex(O)),this.insertNodeToRow(O,K,J,A)}else this.insertNodeToRow(O,K,O.rows.length,A);this.shiftToLastRow(O)}return O},P.prototype.insertNodeToRow=function(_,A,I,D){var L=D;if(I==_.rows.length){var B=[];_.rows.push(B),_.rowWidth.push(L),_.rowHeight.push(0)}var O=_.rowWidth[I]+A.rect.width;_.rows[I].length>0&&(O+=_.horizontalPadding),_.rowWidth[I]=O,_.width0&&($+=_.verticalPadding);var W=0;$>_.rowHeight[I]&&(W=_.rowHeight[I],_.rowHeight[I]=$,W=_.rowHeight[I]-W),_.height+=W,_.rows[I].push(A)},P.prototype.getShortestRowIndex=function(_){for(var A=-1,I=Number.MAX_VALUE,D=0;D<_.rows.length;D++)_.rowWidth[D]I&&(A=D,I=_.rowWidth[D]);return A},P.prototype.canAddHorizontal=function(_,A,I){if(_.idealRowWidth){var D=_.rows.length-1,L=_.rowWidth[D];return L+A+_.horizontalPadding<=_.idealRowWidth}var B=this.getShortestRowIndex(_);if(B<0)return!0;var O=_.rowWidth[B];if(O+_.horizontalPadding+A<=_.width)return!0;var $=0;_.rowHeight[B]0&&($=I+_.verticalPadding-_.rowHeight[B]);var W;_.width-O>=A+_.horizontalPadding?W=(_.height+$)/(O+A+_.horizontalPadding):W=(_.height+$)/_.width,$=I+_.verticalPadding;var V;return _.widthB&&A!=I){D.splice(-1,1),_.rows[I].push(L),_.rowWidth[A]=_.rowWidth[A]-B,_.rowWidth[I]=_.rowWidth[I]+B,_.width=_.rowWidth[instance.getLongestRowIndex(_)];for(var O=Number.MIN_VALUE,$=0;$O&&(O=D[$].height);A>0&&(O+=_.verticalPadding);var W=_.rowHeight[A]+_.rowHeight[I];_.rowHeight[A]=O,_.rowHeight[I]0)for(var J=L;J<=B;J++)K[0]+=this.grid[J][O-1].length+this.grid[J][O].length-1;if(B0)for(var J=O;J<=$;J++)K[3]+=this.grid[L-1][J].length+this.grid[L][J].length-1;for(var ne=C.MAX_VALUE,Z,pe,te=0;te{var u=l(551).FDLayoutNode,h=l(551).IMath;function d(p,m,g,y){u.call(this,p,m,g,y)}s(d,"CoSENode"),d.prototype=Object.create(u.prototype);for(var f in u)d[f]=u[f];d.prototype.calculateDisplacement=function(){var p=this.graphManager.getLayout();this.getChild()!=null&&this.fixedNodeWeight?(this.displacementX+=p.coolingFactor*(this.springForceX+this.repulsionForceX+this.gravitationForceX)/this.fixedNodeWeight,this.displacementY+=p.coolingFactor*(this.springForceY+this.repulsionForceY+this.gravitationForceY)/this.fixedNodeWeight):(this.displacementX+=p.coolingFactor*(this.springForceX+this.repulsionForceX+this.gravitationForceX)/this.noOfChildren,this.displacementY+=p.coolingFactor*(this.springForceY+this.repulsionForceY+this.gravitationForceY)/this.noOfChildren),Math.abs(this.displacementX)>p.coolingFactor*p.maxNodeDisplacement&&(this.displacementX=p.coolingFactor*p.maxNodeDisplacement*h.sign(this.displacementX)),Math.abs(this.displacementY)>p.coolingFactor*p.maxNodeDisplacement&&(this.displacementY=p.coolingFactor*p.maxNodeDisplacement*h.sign(this.displacementY)),this.child&&this.child.getNodes().length>0&&this.propogateDisplacementToChildren(this.displacementX,this.displacementY)},d.prototype.propogateDisplacementToChildren=function(p,m){for(var g=this.getChild().getNodes(),y,v=0;v{function u(g){if(Array.isArray(g)){for(var y=0,v=Array(g.length);y0){var lt=0;ve.forEach(function(mt){ie=="horizontal"?(Se.set(mt,x.has(mt)?b[x.get(mt)]:U.get(mt)),lt+=Se.get(mt)):(Se.set(mt,x.has(mt)?T[x.get(mt)]:U.get(mt)),lt+=Se.get(mt))}),lt=lt/ve.length,qe.forEach(function(mt){j.has(mt)||Se.set(mt,lt)})}else{var Oe=0;qe.forEach(function(mt){ie=="horizontal"?Oe+=x.has(mt)?b[x.get(mt)]:U.get(mt):Oe+=x.has(mt)?T[x.get(mt)]:U.get(mt)}),Oe=Oe/qe.length,qe.forEach(function(mt){Se.set(mt,Oe)})}});for(var _e=s(function(){var ve=Me.shift(),lt=q.get(ve);lt.forEach(function(Oe){if(Se.get(Oe.id)mt&&(mt=xt),KeAt&&(At=Ke)}}catch(Sr){Ct=!0,Rt=Sr}finally{try{!Gt&>.return&>.return()}finally{if(Ct)throw Rt}}var De=(lt+mt)/2-(Oe+At)/2,tt=!0,Ze=!1,dt=void 0;try{for(var Ut=qe[Symbol.iterator](),$t;!(tt=($t=Ut.next()).done);tt=!0){var Yt=$t.value;Se.set(Yt,Se.get(Yt)+De)}}catch(Sr){Ze=!0,dt=Sr}finally{try{!tt&&Ut.return&&Ut.return()}finally{if(Ze)throw dt}}})}return Se},"findAppropriatePositionForRelativePlacement"),N=s(function(q){var ie=0,j=0,U=0,Y=0;if(q.forEach(function(Ce){Ce.left?b[x.get(Ce.left)]-b[x.get(Ce.right)]>=0?ie++:j++:T[x.get(Ce.top)]-T[x.get(Ce.bottom)]>=0?U++:Y++}),ie>j&&U>Y)for(var Ae=0;Aej)for(var me=0;meY)for(var Se=0;Se1)y.fixedNodeConstraint.forEach(function(ee,q){D[q]=[ee.position.x,ee.position.y],L[q]=[b[x.get(ee.nodeId)],T[x.get(ee.nodeId)]]}),B=!0;else if(y.alignmentConstraint)(function(){var ee=0;if(y.alignmentConstraint.vertical){for(var q=y.alignmentConstraint.vertical,ie=s(function(Se){var Ce=new Set;q[Se].forEach(function(ze){Ce.add(ze)});var Me=new Set([].concat(u(Ce)).filter(function(ze){return $.has(ze)})),_e=void 0;Me.size>0?_e=b[x.get(Me.values().next().value)]:_e=M(Ce).x,q[Se].forEach(function(ze){D[ee]=[_e,T[x.get(ze)]],L[ee]=[b[x.get(ze)],T[x.get(ze)]],ee++})},"_loop2"),j=0;j0?_e=b[x.get(Me.values().next().value)]:_e=M(Ce).y,U[Se].forEach(function(ze){D[ee]=[b[x.get(ze)],_e],L[ee]=[b[x.get(ze)],T[x.get(ze)]],ee++})},"_loop3"),Ae=0;AeH&&(H=z[J].length,K=J);if(H0){var Ne={x:0,y:0};y.fixedNodeConstraint.forEach(function(ee,q){var ie={x:b[x.get(ee.nodeId)],y:T[x.get(ee.nodeId)]},j=ee.position,U=R(j,ie);Ne.x+=U.x,Ne.y+=U.y}),Ne.x/=y.fixedNodeConstraint.length,Ne.y/=y.fixedNodeConstraint.length,b.forEach(function(ee,q){b[q]+=Ne.x}),T.forEach(function(ee,q){T[q]+=Ne.y}),y.fixedNodeConstraint.forEach(function(ee){b[x.get(ee.nodeId)]=ee.position.x,T[x.get(ee.nodeId)]=ee.position.y})}if(y.alignmentConstraint){if(y.alignmentConstraint.vertical)for(var re=y.alignmentConstraint.vertical,ue=s(function(q){var ie=new Set;re[q].forEach(function(Y){ie.add(Y)});var j=new Set([].concat(u(ie)).filter(function(Y){return $.has(Y)})),U=void 0;j.size>0?U=b[x.get(j.values().next().value)]:U=M(ie).x,ie.forEach(function(Y){$.has(Y)||(b[x.get(Y)]=U)})},"_loop4"),le=0;le0?U=T[x.get(j.values().next().value)]:U=M(ie).y,ie.forEach(function(Y){$.has(Y)||(T[x.get(Y)]=U)})},"_loop5"),oe=0;oe{a.exports=e})},r={};function n(a){var o=r[a];if(o!==void 0)return o.exports;var l=r[a]={exports:{}};return t[a](l,l.exports,n),l.exports}s(n,"__webpack_require__");var i=n(45);return i})()})});var Y9e=As((nw,Yj)=>{"use strict";s((function(t,r){typeof nw=="object"&&typeof Yj=="object"?Yj.exports=r(Uj()):typeof define=="function"&&define.amd?define(["cose-base"],r):typeof nw=="object"?nw.cytoscapeFcose=r(Uj()):t.cytoscapeFcose=r(t.coseBase)}),"webpackUniversalModuleDefinition")(nw,function(e){return(()=>{"use strict";var t={658:(a=>{a.exports=Object.assign!=null?Object.assign.bind(Object):function(o){for(var l=arguments.length,u=Array(l>1?l-1:0),h=1;h{var u=(function(){function f(p,m){var g=[],y=!0,v=!1,x=void 0;try{for(var b=p[Symbol.iterator](),T;!(y=(T=b.next()).done)&&(g.push(T.value),!(m&&g.length===m));y=!0);}catch(w){v=!0,x=w}finally{try{!y&&b.return&&b.return()}finally{if(v)throw x}}return g}return s(f,"sliceIterator"),function(p,m){if(Array.isArray(p))return p;if(Symbol.iterator in Object(p))return f(p,m);throw new TypeError("Invalid attempt to destructure non-iterable instance")}})(),h=l(140).layoutBase.LinkedList,d={};d.getTopMostNodes=function(f){for(var p={},m=0;m0&&B.merge(V)});for(var O=0;O1){T=x[0],w=T.connectedEdges().length,x.forEach(function(L){L.connectedEdges().length0&&g.set("dummy"+(g.size+1),k),R},d.relocateComponent=function(f,p,m){if(!m.fixedNodeConstraint){var g=Number.POSITIVE_INFINITY,y=Number.NEGATIVE_INFINITY,v=Number.POSITIVE_INFINITY,x=Number.NEGATIVE_INFINITY;if(m.quality=="draft"){var b=!0,T=!1,w=void 0;try{for(var C=p.nodeIndexes[Symbol.iterator](),S;!(b=(S=C.next()).done);b=!0){var k=S.value,R=u(k,2),M=R[0],P=R[1],N=m.cy.getElementById(M);if(N){var _=N.boundingBox(),A=p.xCoords[P]-_.w/2,I=p.xCoords[P]+_.w/2,D=p.yCoords[P]-_.h/2,L=p.yCoords[P]+_.h/2;Ay&&(y=I),Dx&&(x=L)}}}catch(V){T=!0,w=V}finally{try{!b&&C.return&&C.return()}finally{if(T)throw w}}var B=f.x-(y+g)/2,O=f.y-(x+v)/2;p.xCoords=p.xCoords.map(function(V){return V+B}),p.yCoords=p.yCoords.map(function(V){return V+O})}else{Object.keys(p).forEach(function(V){var z=p[V],H=z.getRect().x,K=z.getRect().x+z.getRect().width,J=z.getRect().y,ne=z.getRect().y+z.getRect().height;Hy&&(y=K),Jx&&(x=ne)});var $=f.x-(y+g)/2,W=f.y-(x+v)/2;Object.keys(p).forEach(function(V){var z=p[V];z.setCenter(z.getCenterX()+$,z.getCenterY()+W)})}}},d.calcBoundingBox=function(f,p,m,g){for(var y=Number.MAX_SAFE_INTEGER,v=Number.MIN_SAFE_INTEGER,x=Number.MAX_SAFE_INTEGER,b=Number.MIN_SAFE_INTEGER,T=void 0,w=void 0,C=void 0,S=void 0,k=f.descendants().not(":parent"),R=k.length,M=0;MT&&(y=T),vC&&(x=C),b{var u=l(548),h=l(140).CoSELayout,d=l(140).CoSENode,f=l(140).layoutBase.PointD,p=l(140).layoutBase.DimensionD,m=l(140).layoutBase.LayoutConstants,g=l(140).layoutBase.FDLayoutConstants,y=l(140).CoSEConstants,v=s(function(b,T){var w=b.cy,C=b.eles,S=C.nodes(),k=C.edges(),R=void 0,M=void 0,P=void 0,N={};b.randomize&&(R=T.nodeIndexes,M=T.xCoords,P=T.yCoords);var _=s(function(V){return typeof V=="function"},"isFn"),A=s(function(V,z){return _(V)?V(z):V},"optFn"),I=u.calcParentsWithoutChildren(w,C),D=s(function W(V,z,H,K){for(var J=z.length,ne=0;ne0){var de=void 0;de=H.getGraphManager().add(H.newGraph(),te),W(de,pe,H,K)}}},"processChildrenList"),L=s(function(V,z,H){for(var K=0,J=0,ne=0;ne0?y.DEFAULT_EDGE_LENGTH=g.DEFAULT_EDGE_LENGTH=K/J:_(b.idealEdgeLength)?y.DEFAULT_EDGE_LENGTH=g.DEFAULT_EDGE_LENGTH=50:y.DEFAULT_EDGE_LENGTH=g.DEFAULT_EDGE_LENGTH=b.idealEdgeLength,y.MIN_REPULSION_DIST=g.MIN_REPULSION_DIST=g.DEFAULT_EDGE_LENGTH/10,y.DEFAULT_RADIAL_SEPARATION=g.DEFAULT_EDGE_LENGTH)},"processEdges"),B=s(function(V,z){z.fixedNodeConstraint&&(V.constraints.fixedNodeConstraint=z.fixedNodeConstraint),z.alignmentConstraint&&(V.constraints.alignmentConstraint=z.alignmentConstraint),z.relativePlacementConstraint&&(V.constraints.relativePlacementConstraint=z.relativePlacementConstraint)},"processConstraints");b.nestingFactor!=null&&(y.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=g.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=b.nestingFactor),b.gravity!=null&&(y.DEFAULT_GRAVITY_STRENGTH=g.DEFAULT_GRAVITY_STRENGTH=b.gravity),b.numIter!=null&&(y.MAX_ITERATIONS=g.MAX_ITERATIONS=b.numIter),b.gravityRange!=null&&(y.DEFAULT_GRAVITY_RANGE_FACTOR=g.DEFAULT_GRAVITY_RANGE_FACTOR=b.gravityRange),b.gravityCompound!=null&&(y.DEFAULT_COMPOUND_GRAVITY_STRENGTH=g.DEFAULT_COMPOUND_GRAVITY_STRENGTH=b.gravityCompound),b.gravityRangeCompound!=null&&(y.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=g.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=b.gravityRangeCompound),b.initialEnergyOnIncremental!=null&&(y.DEFAULT_COOLING_FACTOR_INCREMENTAL=g.DEFAULT_COOLING_FACTOR_INCREMENTAL=b.initialEnergyOnIncremental),b.tilingCompareBy!=null&&(y.TILING_COMPARE_BY=b.tilingCompareBy),b.quality=="proof"?m.QUALITY=2:m.QUALITY=0,y.NODE_DIMENSIONS_INCLUDE_LABELS=g.NODE_DIMENSIONS_INCLUDE_LABELS=m.NODE_DIMENSIONS_INCLUDE_LABELS=b.nodeDimensionsIncludeLabels,y.DEFAULT_INCREMENTAL=g.DEFAULT_INCREMENTAL=m.DEFAULT_INCREMENTAL=!b.randomize,y.ANIMATE=g.ANIMATE=m.ANIMATE=b.animate,y.TILE=b.tile,y.TILING_PADDING_VERTICAL=typeof b.tilingPaddingVertical=="function"?b.tilingPaddingVertical.call():b.tilingPaddingVertical,y.TILING_PADDING_HORIZONTAL=typeof b.tilingPaddingHorizontal=="function"?b.tilingPaddingHorizontal.call():b.tilingPaddingHorizontal,y.DEFAULT_INCREMENTAL=g.DEFAULT_INCREMENTAL=m.DEFAULT_INCREMENTAL=!0,y.PURE_INCREMENTAL=!b.randomize,m.DEFAULT_UNIFORM_LEAF_NODE_SIZES=b.uniformNodeDimensions,b.step=="transformed"&&(y.TRANSFORM_ON_CONSTRAINT_HANDLING=!0,y.ENFORCE_CONSTRAINTS=!1,y.APPLY_LAYOUT=!1),b.step=="enforced"&&(y.TRANSFORM_ON_CONSTRAINT_HANDLING=!1,y.ENFORCE_CONSTRAINTS=!0,y.APPLY_LAYOUT=!1),b.step=="cose"&&(y.TRANSFORM_ON_CONSTRAINT_HANDLING=!1,y.ENFORCE_CONSTRAINTS=!1,y.APPLY_LAYOUT=!0),b.step=="all"&&(b.randomize?y.TRANSFORM_ON_CONSTRAINT_HANDLING=!0:y.TRANSFORM_ON_CONSTRAINT_HANDLING=!1,y.ENFORCE_CONSTRAINTS=!0,y.APPLY_LAYOUT=!0),b.fixedNodeConstraint||b.alignmentConstraint||b.relativePlacementConstraint?y.TREE_REDUCTION_ON_INCREMENTAL=!1:y.TREE_REDUCTION_ON_INCREMENTAL=!0;var O=new h,$=O.newGraphManager();return D($.addRoot(),u.getTopMostNodes(S),O,b),L(O,$,k),B(O,b),O.runLayout(),N},"coseLayout");a.exports={coseLayout:v}}),212:((a,o,l)=>{var u=(function(){function b(T,w){for(var C=0;C0)if(L){var $=f.getTopMostNodes(C.eles.nodes());if(_=f.connectComponents(S,C.eles,$),_.forEach(function(He){var ye=He.boundingBox();A.push({x:ye.x1+ye.w/2,y:ye.y1+ye.h/2})}),C.randomize&&_.forEach(function(He){C.eles=He,R.push(m(C))}),C.quality=="default"||C.quality=="proof"){var W=S.collection();if(C.tile){var V=new Map,z=[],H=[],K=0,J={nodeIndexes:V,xCoords:z,yCoords:H},ne=[];if(_.forEach(function(He,ye){He.edges().length==0&&(He.nodes().forEach(function(Be,Ie){W.merge(He.nodes()[Ie]),Be.isParent()||(J.nodeIndexes.set(He.nodes()[Ie].id(),K++),J.xCoords.push(He.nodes()[0].position().x),J.yCoords.push(He.nodes()[0].position().y))}),ne.push(ye))}),W.length>1){var Z=W.boundingBox();A.push({x:Z.x1+Z.w/2,y:Z.y1+Z.h/2}),_.push(W),R.push(J);for(var pe=ne.length-1;pe>=0;pe--)_.splice(ne[pe],1),R.splice(ne[pe],1),A.splice(ne[pe],1)}}_.forEach(function(He,ye){C.eles=He,N.push(y(C,R[ye])),f.relocateComponent(A[ye],N[ye],C)})}else _.forEach(function(He,ye){f.relocateComponent(A[ye],R[ye],C)});var te=new Set;if(_.length>1){var fe=[],ce=k.filter(function(He){return He.css("display")=="none"});_.forEach(function(He,ye){var Be=void 0;if(C.quality=="draft"&&(Be=R[ye].nodeIndexes),He.nodes().not(ce).length>0){var Ie={};Ie.edges=[],Ie.nodes=[];var Ne=void 0;He.nodes().not(ce).forEach(function(re){if(C.quality=="draft")if(!re.isParent())Ne=Be.get(re.id()),Ie.nodes.push({x:R[ye].xCoords[Ne]-re.boundingbox().w/2,y:R[ye].yCoords[Ne]-re.boundingbox().h/2,width:re.boundingbox().w,height:re.boundingbox().h});else{var ue=f.calcBoundingBox(re,R[ye].xCoords,R[ye].yCoords,Be);Ie.nodes.push({x:ue.topLeftX,y:ue.topLeftY,width:ue.width,height:ue.height})}else N[ye][re.id()]&&Ie.nodes.push({x:N[ye][re.id()].getLeft(),y:N[ye][re.id()].getTop(),width:N[ye][re.id()].getWidth(),height:N[ye][re.id()].getHeight()})}),He.edges().forEach(function(re){var ue=re.source(),le=re.target();if(ue.css("display")!="none"&&le.css("display")!="none")if(C.quality=="draft"){var he=Be.get(ue.id()),Te=Be.get(le.id()),oe=[],G=[];if(ue.isParent()){var X=f.calcBoundingBox(ue,R[ye].xCoords,R[ye].yCoords,Be);oe.push(X.topLeftX+X.width/2),oe.push(X.topLeftY+X.height/2)}else oe.push(R[ye].xCoords[he]),oe.push(R[ye].yCoords[he]);if(le.isParent()){var ee=f.calcBoundingBox(le,R[ye].xCoords,R[ye].yCoords,Be);G.push(ee.topLeftX+ee.width/2),G.push(ee.topLeftY+ee.height/2)}else G.push(R[ye].xCoords[Te]),G.push(R[ye].yCoords[Te]);Ie.edges.push({startX:oe[0],startY:oe[1],endX:G[0],endY:G[1]})}else N[ye][ue.id()]&&N[ye][le.id()]&&Ie.edges.push({startX:N[ye][ue.id()].getCenterX(),startY:N[ye][ue.id()].getCenterY(),endX:N[ye][le.id()].getCenterX(),endY:N[ye][le.id()].getCenterY()})}),Ie.nodes.length>0&&(fe.push(Ie),te.add(ye))}});var de=D.packComponents(fe,C.randomize).shifts;if(C.quality=="draft")R.forEach(function(He,ye){var Be=He.xCoords.map(function(Ne){return Ne+de[ye].dx}),Ie=He.yCoords.map(function(Ne){return Ne+de[ye].dy});He.xCoords=Be,He.yCoords=Ie});else{var Ee=0;te.forEach(function(He){Object.keys(N[He]).forEach(function(ye){var Be=N[He][ye];Be.setCenter(Be.getCenterX()+de[Ee].dx,Be.getCenterY()+de[Ee].dy)}),Ee++})}}}else{var B=C.eles.boundingBox();if(A.push({x:B.x1+B.w/2,y:B.y1+B.h/2}),C.randomize){var O=m(C);R.push(O)}C.quality=="default"||C.quality=="proof"?(N.push(y(C,R[0])),f.relocateComponent(A[0],N[0],C)):f.relocateComponent(A[0],R[0],C)}var we=s(function(ye,Be){if(C.quality=="default"||C.quality=="proof"){typeof ye=="number"&&(ye=Be);var Ie=void 0,Ne=void 0,re=ye.data("id");return N.forEach(function(le){re in le&&(Ie={x:le[re].getRect().getCenterX(),y:le[re].getRect().getCenterY()},Ne=le[re])}),C.nodeDimensionsIncludeLabels&&(Ne.labelWidth&&(Ne.labelPosHorizontal=="left"?Ie.x+=Ne.labelWidth/2:Ne.labelPosHorizontal=="right"&&(Ie.x-=Ne.labelWidth/2)),Ne.labelHeight&&(Ne.labelPosVertical=="top"?Ie.y+=Ne.labelHeight/2:Ne.labelPosVertical=="bottom"&&(Ie.y-=Ne.labelHeight/2))),Ie==null&&(Ie={x:ye.position("x"),y:ye.position("y")}),{x:Ie.x,y:Ie.y}}else{var ue=void 0;return R.forEach(function(le){var he=le.nodeIndexes.get(ye.id());he!=null&&(ue={x:le.xCoords[he],y:le.yCoords[he]})}),ue==null&&(ue={x:ye.position("x"),y:ye.position("y")}),{x:ue.x,y:ue.y}}},"getPositions");if(C.quality=="default"||C.quality=="proof"||C.randomize){var Fe=f.calcParentsWithoutChildren(S,k),Ye=k.filter(function(He){return He.css("display")=="none"});C.eles=k.not(Ye),k.nodes().not(":parent").not(Ye).layoutPositions(w,C,we),Fe.length>0&&Fe.forEach(function(He){He.position(we(He))})}else console.log("If randomize option is set to false, then quality option must be 'default' or 'proof'.")},"run")}]),b})();a.exports=x}),657:((a,o,l)=>{var u=l(548),h=l(140).layoutBase.Matrix,d=l(140).layoutBase.SVD,f=s(function(m){var g=m.cy,y=m.eles,v=y.nodes(),x=y.nodes(":parent"),b=new Map,T=new Map,w=new Map,C=[],S=[],k=[],R=[],M=[],P=[],N=[],_=[],A=void 0,I=void 0,D=1e8,L=1e-9,B=m.piTol,O=m.samplingType,$=m.nodeSeparation,W=void 0,V=s(function(){for(var ie=0,j=0,U=!1;j=Ae;){Se=Y[Ae++];for(var qe=C[Se],ve=0;ve_e&&(_e=M[Oe],ze=Oe)}return ze},"BFS"),H=s(function(ie){var j=void 0;if(ie){j=Math.floor(Math.random()*I),A=j;for(var Y=0;Y=1)break;_e=Me}for(var qe=0;qe=1)break;_e=Me}for(var lt=0;lt0&&(j.isParent()?C[ie].push(w.get(j.id())):C[ie].push(j.id()))})});var Fe=s(function(ie){var j=T.get(ie),U=void 0;b.get(ie).forEach(function(Y){g.getElementById(Y).isParent()?U=w.get(Y):U=Y,C[j].push(U),C[T.get(U)].push(ie)})},"_loop"),Ye=!0,He=!1,ye=void 0;try{for(var Be=b.keys()[Symbol.iterator](),Ie;!(Ye=(Ie=Be.next()).done);Ye=!0){var Ne=Ie.value;Fe(Ne)}}catch(q){He=!0,ye=q}finally{try{!Ye&&Be.return&&Be.return()}finally{if(He)throw ye}}I=T.size;var re=void 0;if(I>2){W=I{var u=l(212),h=s(function(f){f&&f("layout","fcose",u)},"register");typeof cytoscape<"u"&&h(cytoscape),a.exports=h}),140:(a=>{a.exports=e})},r={};function n(a){var o=r[a];if(o!==void 0)return o.exports;var l=r[a]={exports:{}};return t[a](l,l.exports,n),l.exports}s(n,"__webpack_require__");var i=n(579);return i})()})});function jj(e,t){if(e===0)return t();let r=Math.random,n=e>>>0;Math.random=function(){n=n+1831565813>>>0;let i=n;return i=Math.imul(i^i>>>15,i|1),i^=i+Math.imul(i^i>>>7,i|61),((i^i>>>14)>>>0)/4294967296};try{return t()}finally{Math.random=r}}var j9e=F(()=>{"use strict";s(jj,"withSeededRandom")});var fx,I0,Xj=F(()=>{"use strict";Hl();fx=s(e=>`${e}`,"wrapIcon"),I0={prefix:"mermaid-architecture",height:80,width:80,icons:{database:{body:fx('')},server:{body:fx('')},disk:{body:fx('')},internet:{body:fx('')},cloud:{body:fx('')},unknown:U7,blank:{body:fx("")}}}});var X9e,K9e,Z9e,Q9e,J9e=F(()=>{"use strict";Jt();qo();Hl();Yr();Xj();FL();er();X9e=s(async function(e,t,r,n){let i=r.getConfigField("padding"),a=r.getConfigField("iconSize"),o=a/2,l=a/6,u=l/2;await Promise.all(t.edges().map(async h=>{let{source:d,sourceDir:f,sourceArrow:p,sourceGroup:m,target:g,targetDir:y,targetArrow:v,targetGroup:x,label:b}=$L(h),{x:T,y:w}=h[0].sourceEndpoint(),{x:C,y:S}=h[0].midpoint(),{x:k,y:R}=h[0].targetEndpoint(),M=i+4;if(m&&(ks(f)?T+=f==="L"?-M:M:w+=f==="T"?-M:M+18),x&&(ks(y)?k+=y==="L"?-M:M:R+=y==="T"?-M:M+18),!m&&r.getNode(d)?.type==="junction"&&(ks(f)?T+=f==="L"?o:-o:w+=f==="T"?o:-o),!x&&r.getNode(g)?.type==="junction"&&(ks(y)?k+=y==="L"?o:-o:R+=y==="T"?o:-o),h[0]._private.rscratch){let P=e.insert("g");if(P.insert("path").attr("d",`M ${T},${w} L ${C},${S} L${k},${R} `).attr("class","edge").attr("id",`${n}-${Jc(d,g,{prefix:"L"})}`),p){let N=ks(f)?QS[f](T,l):T-u,_=Ku(f)?QS[f](w,l):w-u;P.insert("polygon").attr("points",Fj[f](l)).attr("transform",`translate(${N},${_})`).attr("class","arrow")}if(v){let N=ks(y)?QS[y](k,l):k-u,_=Ku(y)?QS[y](R,l):R-u;P.insert("polygon").attr("points",Fj[y](l)).attr("transform",`translate(${N},${_})`).attr("class","arrow")}if(b){let N=JS(f,y)?"XY":ks(f)?"X":"Y",_=0;N==="X"?_=Math.abs(T-k):N==="Y"?_=Math.abs(w-R)/1.5:_=Math.abs(T-k)/2;let A=P.append("g");if(await li(A,b,{useHtmlLabels:!1,width:_,classes:"architecture-service-label"},Pe()),A.attr("dy","1em").attr("alignment-baseline","middle").attr("dominant-baseline","middle").attr("text-anchor","middle"),N==="X")A.attr("transform","translate("+C+", "+S+")");else if(N==="Y")A.attr("transform","translate("+C+", "+S+") rotate(-90)");else if(N==="XY"){let I=ew(f,y);if(I&&$9e(I)){let D=A.node().getBoundingClientRect(),[L,B]=G9e(I);A.attr("dominant-baseline","auto").attr("transform",`rotate(${-1*L*B*45})`);let O=A.node().getBoundingClientRect();A.attr("transform",` + translate(${C}, ${S-D.height/2}) + translate(${L*O.width/2}, ${B*O.height/2}) + rotate(${-1*L*B*45}, 0, ${D.height/2}) + `)}}}}}))},"drawEdges"),K9e=s(async function(e,t,r,n){let a=r.getConfigField("padding")*.75,o=r.getConfigField("fontSize"),u=r.getConfigField("iconSize")/2;await Promise.all(t.nodes().map(async h=>{let d=Np(h);if(d.type==="group"){let{h:f,w:p,x1:m,y1:g}=h.boundingBox(),y=e.append("rect");y.attr("id",`${n}-group-${d.id}`).attr("x",m+u).attr("y",g+u).attr("width",p).attr("height",f).attr("class","node-bkg");let v=e.append("g"),x=m,b=g;if(d.icon){let T=v.append("g");T.html(`${await ps(d.icon,{height:a,width:a,fallbackPrefix:I0.prefix})}`),T.attr("transform","translate("+(x+u+1)+", "+(b+u+1)+")"),x+=a,b+=o/2-1-2}if(d.label){let T=v.append("g");await li(T,d.label,{useHtmlLabels:!1,width:p,classes:"architecture-service-label"},Pe()),T.attr("dy","1em").attr("alignment-baseline","middle").attr("dominant-baseline","start").attr("text-anchor","start"),T.attr("transform","translate("+(x+u+4)+", "+(b+u+2)+")")}r.setElementForId(d.id,y)}}))},"drawGroups"),Z9e=s(async function(e,t,r,n){let i=Pe();for(let a of r){let o=t.append("g"),l=e.getConfigField("iconSize");if(a.title){let f=o.append("g");await li(f,a.title,{useHtmlLabels:!1,width:l*1.5,classes:"architecture-service-label"},i),f.attr("dy","1em").attr("alignment-baseline","middle").attr("dominant-baseline","middle").attr("text-anchor","middle"),f.attr("transform","translate("+l/2+", "+l+")")}let u=o.append("g");if(a.icon)u.html(`${await ps(a.icon,{height:l,width:l,fallbackPrefix:I0.prefix})}`);else if(a.iconText){u.html(`${await ps("blank",{height:l,width:l,fallbackPrefix:I0.prefix})}`);let m=u.append("g").append("foreignObject").attr("width",l).attr("height",l).append("div").attr("class","node-icon-text").attr("style",`height: ${l}px;`).append("div").html(xr(a.iconText,i)),g=parseInt(window.getComputedStyle(m.node(),null).getPropertyValue("font-size").replace(/\D/g,""))??16;m.attr("style",`-webkit-line-clamp: ${Math.floor((l-2)/g)};`)}else u.append("path").attr("class","node-bkg").attr("id",`${n}-node-${a.id}`).attr("d",`M0,${l} V5 Q0,0 5,0 H${l-5} Q${l},0 ${l},5 V${l} Z`);o.attr("id",`${n}-service-${a.id}`).attr("class","architecture-service");let{width:h,height:d}=o.node().getBBox();a.width=h,a.height=d,e.setElementForId(a.id,o)}return 0},"drawServices"),Q9e=s(function(e,t,r,n){r.forEach(i=>{let a=t.append("g"),o=e.getConfigField("iconSize");a.append("g").append("rect").attr("id",`${n}-node-${i.id}`).attr("fill-opacity","0").attr("width",o).attr("height",o),a.attr("class","architecture-junction");let{width:u,height:h}=a._groups[0][0].getBBox();a.width=u,a.height=h,e.setElementForId(i.id,a)})},"drawJunctions")});function PDt(e,t,r){e.forEach(n=>{t.add({group:"nodes",data:{type:"service",id:n.id,icon:n.icon,label:n.title,parent:n.in,width:r.getConfigField("iconSize"),height:r.getConfigField("iconSize")},classes:"node-service"})})}function ODt(e,t,r){e.forEach(n=>{t.add({group:"nodes",data:{type:"junction",id:n.id,parent:n.in,width:r.getConfigField("iconSize"),height:r.getConfigField("iconSize")},classes:"node-junction"})})}function BDt(e,t){t.nodes().map(r=>{let n=Np(r);if(n.type==="group")return;n.x=r.position().x,n.y=r.position().y,e.getElementById(n.id).attr("transform","translate("+(n.x||0)+","+(n.y||0)+")")})}function $Dt(e,t){e.forEach(r=>{t.add({group:"nodes",data:{type:"group",id:r.id,icon:r.icon,label:r.title,parent:r.in},classes:"node-group"})})}function FDt(e,t){e.forEach(r=>{let{lhsId:n,rhsId:i,lhsInto:a,lhsGroup:o,rhsInto:l,lhsDir:u,rhsDir:h,rhsGroup:d,title:f}=r,p=JS(r.lhsDir,r.rhsDir)?"segments":"straight",m={id:`${n}-${i}`,label:f,source:n,sourceDir:u,sourceArrow:a,sourceGroup:o,sourceEndpoint:u==="L"?"0 50%":u==="R"?"100% 50%":u==="T"?"50% 0":"50% 100%",target:i,targetDir:h,targetArrow:l,targetGroup:d,targetEndpoint:h==="L"?"0 50%":h==="R"?"100% 50%":h==="T"?"50% 0":"50% 100%"};t.add({group:"edges",data:m,classes:p})})}function GDt(e,t,r,n=[]){let i=s((p,m)=>{let g=new Map;for(let[y,v]of p.entries()){let x=`${y}`,b=0,T=[...v.entries()];if(T.length===1){g.set(x,T[0][1]);continue}for(let w=0;w{let m=new Map,g=new Map;return p.forEach(([y,v],x)=>{let b=e.getNode(x)?.in??"default",T=m.get(v)??new Map;m.has(v)||m.set(v,T);let w=g.get(y)??new Map;g.has(y)||g.set(y,w);for(let C of[T,w]){let S=C.get(b)??[];C.has(b)||C.set(b,S),S.push(x)}}),{horiz:[...i(m,"horizontal").values()].filter(y=>y.length>1),vert:[...i(g,"vertical").values()].filter(y=>y.length>1)}}),[o,l]=a.reduce(([p,m],{horiz:g,vert:y})=>[[...p,...g],[...m,...y]],[[],[]]),u=new Set;n.forEach(p=>p.members.forEach(m=>u.add(m)));let h=s(p=>p.filter(m=>!m.some(g=>u.has(g))),"dropOverlapping"),d=h(o),f=h(l);return n.forEach(p=>{p.members.length<2||(p.direction==="row"?d.push([...p.members]):f.push([...p.members]))}),{horizontal:d,vertical:f}}function zDt(e,t,r=[]){let n=[],i=t.getConfigField("iconSize"),a=t.getConfigField("idealEdgeLengthMultiplier"),o=a*i,l=new Set;r.forEach(d=>{for(let f=0;f`${d[0]},${d[1]}`,"posToStr"),h=s(d=>d.split(",").map(f=>parseInt(f)),"strToPos");return e.forEach(d=>{let f=new Map([...d.entries()].map(([y,v])=>[u(v),y])),p=[u([0,0])],m={},g={L:[-1,0],R:[1,0],T:[0,1],B:[0,-1]};for(;p.length>0;){let y=p.shift();if(y){m[y]=1;let v=f.get(y);if(v){let x=h(y);Object.entries(g).forEach(([b,T])=>{let w=u([x[0]+T[0],x[1]+T[1]]),C=f.get(w);if(C&&!m[w]){if(p.push(w),l.has(`${v}|${C}`))return;n.push({[$j[b]]:C,[$j[B9e(b)]]:v,gap:a*i})}})}}}}),n}function VDt(e,t,r,n,i,{spatialMaps:a,groupAlignments:o}){return new Promise(l=>{let u=ut("body").append("div").attr("id","cy").attr("style","display:none"),h=Ll({container:document.getElementById("cy"),style:[{selector:"edge",style:{"curve-style":"straight","source-endpoint":"data(sourceEndpoint)","target-endpoint":"data(targetEndpoint)"}},{selector:"edge[label]",style:{label:"data(label)"}},{selector:"edge.segments",style:{"curve-style":"segments","segment-weights":"0","segment-distances":[.5],"edge-distances":"endpoints","source-endpoint":"data(sourceEndpoint)","target-endpoint":"data(targetEndpoint)"}},{selector:"node",style:{"compound-sizing-wrt-labels":"include"}},{selector:"node[label]",style:{"text-valign":"bottom","text-halign":"center","font-size":`${i.getConfigField("fontSize")}px`}},{selector:".node-service",style:{label:"data(label)",width:"data(width)",height:"data(height)"}},{selector:".node-junction",style:{width:"data(width)",height:"data(height)"}},{selector:".node-group",style:{padding:`${i.getConfigField("padding")}px`}}],layout:{name:"grid",boundingBox:{x1:0,x2:100,y1:0,y2:100}}});u.remove(),$Dt(r,h),PDt(e,h,i),ODt(t,h,i),FDt(n,h);let d=i.getLayoutHints(),f=GDt(i,a,o,d),p=zDt(a,i,d),m=i.getConfigField("iconSize"),g=i.getConfigField("idealEdgeLengthMultiplier")*m,y=.5*m,v=i.getConfigField("edgeElasticity"),x=i.getConfigField("seed"),b=h.layout({name:"fcose",quality:"proof",randomize:i.getConfigField("randomize"),nodeSeparation:i.getConfigField("nodeSeparation"),numIter:i.getConfigField("numIter"),styleEnabled:!1,animate:!1,nodeDimensionsIncludeLabels:!1,idealEdgeLength(T){let[w,C]=T.connectedNodes(),{parent:S}=Np(w),{parent:k}=Np(C);return S===k?g:y},edgeElasticity(T){let[w,C]=T.connectedNodes(),{parent:S}=Np(w),{parent:k}=Np(C);return S===k?v:.001},alignmentConstraint:f,relativePlacementConstraint:p});b.one("layoutstop",()=>{function T(w,C,S,k){let R,M,{x:P,y:N}=w,{x:_,y:A}=C;M=(k-N+(P-S)*(N-A)/(P-_))/Math.sqrt(1+Math.pow((N-A)/(P-_),2)),R=Math.sqrt(Math.pow(k-N,2)+Math.pow(S-P,2)-Math.pow(M,2));let I=Math.sqrt(Math.pow(_-P,2)+Math.pow(A-N,2));R=R/I;let D=(_-P)*(k-N)-(A-N)*(S-P);switch(!0){case D>=0:D=1;break;case D<0:D=-1;break}let L=(_-P)*(S-P)+(A-N)*(k-N);switch(!0){case L>=0:L=1;break;case L<0:L=-1;break}return M=Math.abs(M)*D,R=R*L,{distances:M,weights:R}}s(T,"getSegmentWeights"),h.startBatch();for(let w of Object.values(h.edges()))if(w.data?.()){let{x:C,y:S}=w.source().position(),{x:k,y:R}=w.target().position();if(C!==k&&S!==R){let M=w.sourceEndpoint(),P=w.targetEndpoint(),{sourceDir:N}=$L(w),[_,A]=Ku(N)?[M.x,P.y]:[P.x,M.y],{weights:I,distances:D}=T(M,P,_,A);w.style("segment-distances",D),w.style("segment-weights",I)}}h.endBatch(),jj(x,()=>b.run())});try{jj(x,()=>b.run())}catch(T){throw T instanceof RangeError&&T.message.includes("Invalid array length")?new Error("Architecture layout failed: a declared `align row|column` directive likely contradicts the edge directions, or two declared alignments overlap on a shared node. Check that the order of members in each `align` chain is consistent with the edges between them, and that no node appears in two `align` directives along the same axis."):T}h.ready(T=>{se.info("Ready",T),l(h)})})}var eBe,WDt,tBe,rBe=F(()=>{"use strict";jF();eBe=Rs(Y9e(),1);j9e();Wr();St();Hl();cs();Vn();Xj();FL();J9e();ey([{name:I0.prefix,icons:I0}]);Ll.use(eBe.default);s(PDt,"addServices");s(ODt,"addJunctions");s(BDt,"positionNodes");s($Dt,"addGroups");s(FDt,"addEdges");s(GDt,"getAlignments");s(zDt,"getRelativeConstraints");s(VDt,"layoutArchitecture");WDt=s(async(e,t,r,n)=>{let i=n.db;i.setDiagramId(t);let a=i.getServices(),o=i.getJunctions(),l=i.getGroups(),u=i.getEdges(),h=i.getDataStructures(),d=wn(t),f=d.append("g");f.attr("class","architecture-edges");let p=d.append("g");p.attr("class","architecture-services");let m=d.append("g");m.attr("class","architecture-groups"),await Z9e(i,p,a,t),Q9e(i,p,o,t);let g=await VDt(a,o,l,u,i,h);await X9e(f,g,i,t),await K9e(m,g,i,t),BDt(i,g),ml(void 0,d,i.getConfigField("padding"),i.getConfigField("useMaxWidth"))},"draw"),tBe={draw:WDt}});var nBe={};sr(nBe,{diagram:()=>qDt});var qDt,iBe=F(()=>{"use strict";q9e();zj();U9e();rBe();qDt={parser:Vj,get db(){return new dx},renderer:tBe,styles:H9e}});var Kj,Zj,GL,Qj,oBe=F(()=>{"use strict";Kj="position frame",Zj="frame positioned",GL="position relation",Qj="relation positioned"});function ZDt(){tX={}}function e7t(){let e=r7t,{ast:t}=tX,r=cBe();if(!t)throw new Error("No data for EventModel");return t.frames.forEach((n,i)=>{let a=l7t(n,t.dataEntities,r);e=eX(e,{$kind:Kj,index:i,frame:n,textProps:a});let o;p7t(n)?(se.debug("source frame",n.sourceFrames),o=t.frames.filter(l=>n.sourceFrames.some(u=>u.$refText===l.name)),o.forEach(l=>{e=eX(e,{$kind:GL,index:i,frame:n,sourceFrame:l})})):e=eX(e,{$kind:GL,index:i,frame:n})}),e={...e,sortedSwimlanesArray:uBe(e.swimlanes)},e}function t7t(e){tX.ast=e}function cBe(){return kn}function n7t(e){let t=e.split(".");if(t.length===2)return t[0]}function i7t(e){let t=e.split(".");return t.length===2?t[1]:e}function a7t(e,t){if(!(!t||t.length===0))return Object.values(e).find(r=>r.namespace===t)}function Jj(e,t,r){return Math.max(t,...Object.keys(e).filter(n=>{let i=Number.parseInt(n);return i>t&&iNumber.parseInt(n)))+1}function s7t(e,t){let r=n7t(e.entityIdentifier),n=a7t(t,r);switch(e.modelEntityType){case"ui":case"pcr":case"processor":return n?{index:n.index,label:n.namespace||kn.labelUiAutomation}:r?{index:Jj(t,0,100),label:kn.labelUiAutomationPrefix+r}:{index:0,label:kn.labelUiAutomation};case"rmo":case"readmodel":case"cmd":case"command":return n?{index:n.index,label:n.namespace||kn.labelCommandReadModel}:r?{index:Jj(t,100,200),label:kn.labelCommandReadModelPrefix+r}:{index:100,label:kn.labelCommandReadModel};case"evt":case"event":default:return n?{index:n.index,label:n.namespace||kn.labelEvents}:r?{index:Jj(t,200,300),label:kn.labelEventsPrefix+r}:{index:200,label:kn.labelEvents}}}function o7t(e){let{themeVariables:t}=Lt();switch(e.modelEntityType){case"ui":return{fill:t.emUiFill??"white",stroke:t.emUiStroke??"#dbdada"};case"pcr":case"processor":return{fill:t.emProcessorFill??"#edb3f6",stroke:t.emProcessorStroke??"#b88cbf"};case"rmo":case"readmodel":return{fill:t.emReadModelFill??"#d3f1a2",stroke:t.emReadModelStroke??"#a3b732"};case"cmd":case"command":return{fill:t.emCommandFill??"#bcd6fe",stroke:t.emCommandStroke??"#679ac3"};case"evt":case"event":return{fill:t.emEventFill??"#ffb778",stroke:t.emEventStroke??"#c19a0f"};default:return{fill:"red",stroke:"black"}}}function l7t(e,t,r){let n=Lt(),i=xr(i7t(e.entityIdentifier)??"",n),a,o={fontSize:16,fontWeight:700,fontFamily:'"trebuchet ms", verdana, arial, sans-serif',joinWith:"
    "},u=`${Tm(i,r.textMaxWidth,o)}`;if(e.dataInlineValue&&(a=e.dataInlineValue,a=a.substring(a.indexOf("{")+1),a=a.substring(0,a.lastIndexOf("}")-1),a=xr(a,n),a=Tm(a,r.textMaxWidth,o),a=a.replaceAll(" "," ")),e.dataReference){let g=t.find(y=>y.name===e.dataReference?.$refText);g&&(a=g.dataBlockValue,a=a.substring(a.indexOf(`{ +`)+2),a=a.substring(0,a.lastIndexOf("}")-1),a=xr(a,n),a=Tm(a,r.textMaxWidth,o),a=a.replaceAll(" "," "),a+="
    ")}let h=a!==void 0;h&&(u+=`

    ${a}`);let d={fontSize:o.fontSize,fontWeight:o.fontWeight,fontFamily:o.fontFamily},f=y2(u,d),p=h?f.width/3:f.width,m={content:u,width:p,height:f.height};return se.debug(`[${e.name}] ${e.entityIdentifier} text`,m),m}function c7t(e,t){let r=t,n=o7t(r.frame),i={width:r.textProps.width+2*kn.boxTextPadding,height:r.textProps.height+2*kn.boxTextPadding};return[{$kind:Zj,frame:r.frame,index:r.index,visual:n,dimension:i,textProps:r.textProps}]}function u7t(e,t,r){return t===void 0?kn.contentStartX:t.index===e.index&&e.r?e.r+kn.boxPadding:r===void 0?kn.contentStartX:r.r-kn.boxOverlap+kn.boxPadding}function h7t(e,t){let r=[...e.map(n=>n.r),t];return Math.max(...r)}function uBe(e){return Object.values(e).sort((t,r)=>t.index-r.index)}function d7t(e,t){let r=t,n=s7t(r.frame,e.swimlanes),i;n.index in e.swimlanes?i=e.swimlanes[n.index]:i={index:n.index,label:n.label,r:0,y:n.index*kn.swimlaneMinHeight+kn.swimlaneGap,height:kn.swimlaneMinHeight,maxHeight:kn.swimlaneMinHeight};let a=e.boxes.length>0?e.boxes[e.boxes.length-1]:void 0,o=e.previousSwimlaneNumber!==void 0?e.swimlanes[e.previousSwimlaneNumber]:void 0,l={width:Math.max(kn.boxMinWidth,Math.min(kn.boxMaxWidth,r.dimension.width))+2*kn.boxPadding,height:Math.max(kn.boxMinHeight,Math.min(kn.boxMaxHeight,r.dimension.height))+2*kn.boxPadding},u=u7t(i,o,a),h=u+l.width+kn.boxPadding,d=h7t(Object.values(e.swimlanes),h);i.r=u+l.width,i.maxHeight=Math.max(i.maxHeight,l.height),i.height=Math.max(kn.swimlaneMinHeight,i.maxHeight)+2*kn.swimlanePadding;let f={x:u,y:kn.swimlanePadding+i.y,r:h,dimension:l,leftSibling:!1,swimlane:i,visual:r.visual,text:r.textProps.content,frame:r.frame,index:r.index},p={...e,boxes:[...e.boxes,f],swimlanes:{...e.swimlanes,[`${i.index}`]:i},previousSwimlaneNumber:n.index,previousFrame:r.frame,maxR:d},m=uBe(p.swimlanes);m.length>0&&(m[0].y=0);for(let g=1;g0}function lBe(e,t){if(t!=null)return e.find(r=>r.frame.name===t.name)}function m7t(e,t,r){if(!(r<0))for(let n=r;n>=0;n--){let i=e[n];if(i.swimlane.index!==t)return i}}function g7t(e,t){let r=t;if(c_(r.frame)||f7t(r.index,r.frame))return[];let n=lBe(e.boxes,r.frame);if(n===void 0)throw new Error(`Target box not found for frame ${r.frame.name}`);let i;return r.sourceFrame?i=lBe(e.boxes,r.sourceFrame):i=m7t(e.boxes,n.swimlane.index,r.index-1),i===void 0?[]:[{$kind:Qj,frame:r.frame,index:r.index,sourceBox:i,targetBox:n}]}function y7t(e,t){let r=t,n={visual:{fill:"none",stroke:"#000"},source:{x:r.sourceBox.x,y:r.sourceBox.y},target:{x:r.targetBox.x,y:r.targetBox.y},sourceBox:r.sourceBox,targetBox:r.targetBox};return{...e,relations:[...e.relations,n]}}function b7t(e,t){let r=v7t[t.$kind];if(r==null)return[];let n=r(e,t);return se.debug("decided events",n),n}function T7t(e,t){let r=t.reduce((n,i)=>{let a=x7t[i.$kind];return a==null?n:a(n,i)},e);return se.debug("evolve events",{state:e,newState:r,events:t}),r}function eX(e,t){let r=b7t(e,t);return T7t(e,r)}var jDt,XDt,KDt,QDt,JDt,tX,kn,r7t,v7t,x7t,iw,rX=F(()=>{"use strict";St();er();er();br();$n();Yr();Qi();ls();oBe();jDt=s(function(e){se.debug("options str",e)},"setOptions"),XDt=s(function(){return{}},"getOptions"),KDt=s(function(){ZDt(),Tr()},"clear");s(ZDt,"reset");QDt=mr.eventmodeling,JDt=s(()=>Xr({...QDt,...Lt().eventmodeling}),"getConfig"),tX={};s(e7t,"getState");s(t7t,"setAst");kn={swimlaneMinHeight:70,swimlanePadding:15,swimlaneGap:10,boxPadding:10,boxOverlap:90,boxDefaultY:0,boxMinWidth:80,boxMaxWidth:450,boxMinHeight:80,boxMaxHeight:750,contentStartX:250,textMaxWidth:430,boxTextFontWeight:"bold",boxTextPadding:10,swimlaneTextFontWeight:"bold",labelUiAutomation:"UI/Automation",labelUiAutomationPrefix:"UI/A: ",labelCommandReadModel:"Command/Read Model",labelCommandReadModelPrefix:"C/RM: ",labelEvents:"Events",labelEventsPrefix:"Stream: "};s(cBe,"getDiagramProps");r7t={boxes:[],swimlanes:{},relations:[],maxR:0,sortedSwimlanesArray:[]};s(n7t,"extractNamespace");s(i7t,"extractName");s(a7t,"findSwimlaneByNamespace");s(Jj,"findNextAvailableIndex");s(s7t,"calculateSwimlaneProps");s(o7t,"calculateEntityVisualProps");s(l7t,"calculateTextProps");s(c7t,"decidePositionFrame");s(u7t,"calculateX");s(h7t,"calculateMaxRight");s(uBe,"sortedSwimlanesArray");s(d7t,"evolveFramePositioned");s(f7t,"isFirstFrame");s(p7t,"hasSourceFrame");s(lBe,"findBoxByFrame");s(m7t,"findBoxByLineIndex");s(g7t,"decidePositionRelation");s(y7t,"evolveRelationPositioned");v7t={[Kj]:c7t,[GL]:g7t},x7t={[Zj]:d7t,[Qj]:y7t};s(b7t,"decide");s(T7t,"evolve");s(eX,"dispatch");iw={getConfig:JDt,setOptions:jDt,getOptions:XDt,clear:KDt,setAccTitle:Lr,getAccTitle:Ir,getAccDescription:Nr,setAccDescription:Mr,setDiagramTitle:Gr,getDiagramTitle:Pr,setAst:t7t,getDiagramProps:cBe,getState:e7t}});var hBe,dBe=F(()=>{"use strict";ls();St();to();rX();hBe={parse:s(async e=>{let t=await Li("eventmodeling",e);se.debug(t),iw.setAst(t),Hn(t,iw)},"parse")}});function w7t(e,t){return r=>{let n=r.swimlane.y+t.swimlanePadding,i=e.append("g").attr("class","em-box");i.append("rect").attr("x",r.x).attr("y",n).attr("rx","3").attr("width",r.dimension.width).attr("height",r.dimension.height).attr("stroke",r.visual.stroke).attr("fill",r.visual.fill),i.append("foreignObject").attr("x",r.x+t.boxPadding).attr("y",n+10).attr("width",r.dimension.width-2*t.boxPadding).attr("height",r.dimension.height-2*t.boxPadding).append("xhtml:div").style("display","table").style("height","100%").style("width","100%").append("span").style("display","table-cell").style("text-align","center").style("vertical-align","middle").html(r.text)}}function k7t(e,t){return e>t}function E7t(e,t,r,n){return i=>{let a=i.sourceBox.swimlane.y+t.swimlanePadding,o=i.targetBox.swimlane.y+t.swimlanePadding,l=k7t(a,o),u=i.sourceBox.x+i.sourceBox.dimension.width*2/3,h=i.targetBox.x+i.targetBox.dimension.width/3,d,f;se.debug(`rendering relation up=${l} for `,{sourceBox:i.sourceBox,targetBox:i.targetBox}),l?(d=a,f=o+i.targetBox.dimension.height):(d=a+i.sourceBox.dimension.height,f=o);let p=n.emRelationStroke??i.visual.stroke;e.append("path").attr("class","em-relation").attr("fill",i.visual.fill).attr("stroke",p).attr("stroke-width","1").attr("marker-end",`url(#${r})`).attr("d",`M${u} ${d} L${h} ${f}`)}}function A7t(e,t,r,n){return i=>{let a=e.append("g").attr("class","em-swimlane"),o=n.emSwimlaneBackgroundOdd??"rgb(250,250,250)",l=n.emSwimlaneBackgroundStroke??"rgb(240,240,240)";a.append("rect").attr("x",0).attr("y",i.y).attr("rx","3").attr("width",t+r.swimlanePadding).attr("height",i.height).attr("fill",o).attr("stroke",l),a.append("text").attr("font-weight",r.swimlaneTextFontWeight).attr("x",30).attr("y",i.y+30).text(i.label)}}var C7t,S7t,R7t,fBe,pBe=F(()=>{"use strict";Wr();Jt();St();C7t=Pe(),S7t=C7t?.eventmodeling;s(w7t,"renderD3Box");s(k7t,"dirUpwards");s(E7t,"renderD3Relation");s(A7t,"renderD3Swimlane");R7t=s(function(e,t,r,n){if(se.debug("in eventmodeling renderer",e+` +`,"id:",t,r),!S7t)throw new Error("EventModeling config not found");let i=n.db,{themeVariables:a,eventmodeling:o}=Pe(),l=ut(`[id="${t}"]`),u=i.getDiagramProps(),h=i.getState(),d=`em-arrowhead-${t}`,f=a.emArrowhead??"#000000";h.sortedSwimlanesArray.forEach(A7t(l,h.maxR,u,a)),h.boxes.forEach(w7t(l,u)),h.relations.forEach(E7t(l,u,d,a)),l.append("defs").append("marker").attr("id",d).attr("markerWidth","10").attr("markerHeight","7").attr("refX","10").attr("refY","3.5").attr("orient","auto").append("polygon").attr("points","0 0, 10 3.5, 0 7").attr("fill",f),sb(void 0,l,o?.padding??30,o?.useMaxWidth)},"draw"),fBe={draw:R7t}});var _7t,mBe,gBe=F(()=>{"use strict";_7t=s(e=>"","getStyles"),mBe=_7t});var yBe={};sr(yBe,{diagram:()=>L7t});var L7t,vBe=F(()=>{"use strict";dBe();rX();pBe();gBe();L7t={parser:hBe,db:iw,renderer:fBe,styles:mBe}});var nX,TBe,CBe=F(()=>{"use strict";nX=(function(){var e=s(function(x,b,T,w){for(T=T||{},w=x.length;w--;T[x[w]]=b);return T},"o"),t=[1,4],r=[1,14],n=[1,12],i=[1,13],a=[6,7,8],o=[1,20],l=[1,18],u=[1,19],h=[6,7,11],d=[1,6,13,14],f=[1,23],p=[1,24],m=[1,6,7,11,13,14],g={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,ishikawa:4,spaceLines:5,SPACELINE:6,NL:7,ISHIKAWA:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,TEXT:14,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"ISHIKAWA",11:"EOF",13:"SPACELIST",14:"TEXT"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,2],[12,1],[12,1],[12,1]],performAction:s(function(b,T,w,C,S,k,R){var M=k.length-1;switch(S){case 6:case 7:return C;case 15:C.addNode(k[M-1].length,k[M].trim());break;case 16:C.addNode(0,k[M].trim());break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:t},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:t},{6:r,7:[1,10],9:9,12:11,13:n,14:i},e(a,[2,3]),{1:[2,2]},e(a,[2,4]),e(a,[2,5]),{1:[2,6],6:r,12:15,13:n,14:i},{6:r,9:16,12:11,13:n,14:i},{6:o,7:l,10:17,11:u},e(h,[2,18],{14:[1,21]}),e(h,[2,16]),e(h,[2,17]),{6:o,7:l,10:22,11:u},{1:[2,7],6:r,12:15,13:n,14:i},e(d,[2,14],{7:f,11:p}),e(m,[2,8]),e(m,[2,9]),e(m,[2,10]),e(h,[2,15]),e(d,[2,13],{7:f,11:p}),e(m,[2,11]),e(m,[2,12])],defaultActions:{2:[2,1],6:[2,2]},parseError:s(function(b,T){if(T.recoverable)this.trace(b);else{var w=new Error(b);throw w.hash=T,w}},"parseError"),parse:s(function(b){var T=this,w=[0],C=[],S=[null],k=[],R=this.table,M="",P=0,N=0,_=0,A=2,I=1,D=k.slice.call(arguments,1),L=Object.create(this.lexer),B={yy:{}};for(var O in this.yy)Object.prototype.hasOwnProperty.call(this.yy,O)&&(B.yy[O]=this.yy[O]);L.setInput(b,B.yy),B.yy.lexer=L,B.yy.parser=this,typeof L.yylloc>"u"&&(L.yylloc={});var $=L.yylloc;k.push($);var W=L.options&&L.options.ranges;typeof B.yy.parseError=="function"?this.parseError=B.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function V(Fe){w.length=w.length-2*Fe,S.length=S.length-Fe,k.length=k.length-Fe}s(V,"popStack");function z(){var Fe;return Fe=C.pop()||L.lex()||I,typeof Fe!="number"&&(Fe instanceof Array&&(C=Fe,Fe=C.pop()),Fe=T.symbols_[Fe]||Fe),Fe}s(z,"lex");for(var H,K,J,ne,Z,pe,te={},fe,ce,de,Ee;;){if(J=w[w.length-1],this.defaultActions[J]?ne=this.defaultActions[J]:((H===null||typeof H>"u")&&(H=z()),ne=R[J]&&R[J][H]),typeof ne>"u"||!ne.length||!ne[0]){var we="";Ee=[];for(fe in R[J])this.terminals_[fe]&&fe>A&&Ee.push("'"+this.terminals_[fe]+"'");L.showPosition?we="Parse error on line "+(P+1)+`: +`+L.showPosition()+` +Expecting `+Ee.join(", ")+", got '"+(this.terminals_[H]||H)+"'":we="Parse error on line "+(P+1)+": Unexpected "+(H==I?"end of input":"'"+(this.terminals_[H]||H)+"'"),this.parseError(we,{text:L.match,token:this.terminals_[H]||H,line:L.yylineno,loc:$,expected:Ee})}if(ne[0]instanceof Array&&ne.length>1)throw new Error("Parse Error: multiple actions possible at state: "+J+", token: "+H);switch(ne[0]){case 1:w.push(H),S.push(L.yytext),k.push(L.yylloc),w.push(ne[1]),H=null,K?(H=K,K=null):(N=L.yyleng,M=L.yytext,P=L.yylineno,$=L.yylloc,_>0&&_--);break;case 2:if(ce=this.productions_[ne[1]][1],te.$=S[S.length-ce],te._$={first_line:k[k.length-(ce||1)].first_line,last_line:k[k.length-1].last_line,first_column:k[k.length-(ce||1)].first_column,last_column:k[k.length-1].last_column},W&&(te._$.range=[k[k.length-(ce||1)].range[0],k[k.length-1].range[1]]),pe=this.performAction.apply(te,[M,N,P,B.yy,ne[1],S,k].concat(D)),typeof pe<"u")return pe;ce&&(w=w.slice(0,-1*ce*2),S=S.slice(0,-1*ce),k=k.slice(0,-1*ce)),w.push(this.productions_[ne[1]][0]),S.push(te.$),k.push(te._$),de=R[w[w.length-2]][w[w.length-1]],w.push(de);break;case 3:return!0}}return!0},"parse")},y=(function(){var x={EOF:1,parseError:s(function(T,w){if(this.yy.parser)this.yy.parser.parseError(T,w);else throw new Error(T)},"parseError"),setInput:s(function(b,T){return this.yy=T||this.yy||{},this._input=b,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var b=this._input[0];this.yytext+=b,this.yyleng++,this.offset++,this.match+=b,this.matched+=b;var T=b.match(/(?:\r\n?|\n).*/g);return T?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),b},"input"),unput:s(function(b){var T=b.length,w=b.split(/(?:\r\n?|\n)/g);this._input=b+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-T),this.offset-=T;var C=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),w.length-1&&(this.yylineno-=w.length-1);var S=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:w?(w.length===C.length?this.yylloc.first_column:0)+C[C.length-w.length].length-w[0].length:this.yylloc.first_column-T},this.options.ranges&&(this.yylloc.range=[S[0],S[0]+this.yyleng-T]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(b){this.unput(this.match.slice(b))},"less"),pastInput:s(function(){var b=this.matched.substr(0,this.matched.length-this.match.length);return(b.length>20?"...":"")+b.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var b=this.match;return b.length<20&&(b+=this._input.substr(0,20-b.length)),(b.substr(0,20)+(b.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var b=this.pastInput(),T=new Array(b.length+1).join("-");return b+this.upcomingInput()+` +`+T+"^"},"showPosition"),test_match:s(function(b,T){var w,C,S;if(this.options.backtrack_lexer&&(S={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(S.yylloc.range=this.yylloc.range.slice(0))),C=b[0].match(/(?:\r\n?|\n).*/g),C&&(this.yylineno+=C.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:C?C[C.length-1].length-C[C.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+b[0].length},this.yytext+=b[0],this.match+=b[0],this.matches=b,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(b[0].length),this.matched+=b[0],w=this.performAction.call(this,this.yy,this,T,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),w)return w;if(this._backtrack){for(var k in S)this[k]=S[k];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var b,T,w,C;this._more||(this.yytext="",this.match="");for(var S=this._currentRules(),k=0;kT[0].length)){if(T=w,C=k,this.options.backtrack_lexer){if(b=this.test_match(w,S[k]),b!==!1)return b;if(this._backtrack){T=!1;continue}else return!1}else if(!this.options.flex)break}return T?(b=this.test_match(T,S[C]),b!==!1?b:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var T=this.next();return T||this.lex()},"lex"),begin:s(function(T){this.conditionStack.push(T)},"begin"),popState:s(function(){var T=this.conditionStack.length-1;return T>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(T){return T=this.conditionStack.length-1-Math.abs(T||0),T>=0?this.conditionStack[T]:"INITIAL"},"topState"),pushState:s(function(T){this.begin(T)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(T,w,C,S){var k=S;switch(C){case 0:return 6;case 1:return 8;case 2:return 8;case 3:return 6;case 4:return 7;case 5:return 13;case 6:return 14;case 7:return 11}},"anonymous"),rules:[/^(?:\s*%%.*)/i,/^(?:ishikawa-beta\b)/i,/^(?:ishikawa\b)/i,/^(?:[\s]+[\n])/i,/^(?:[\n]+)/i,/^(?:[\s]+)/i,/^(?:[^\n]+)/i,/^(?:$)/i],conditions:{INITIAL:{rules:[0,1,2,3,4,5,6,7],inclusive:!0}}};return x})();g.lexer=y;function v(){this.yy={}}return s(v,"Parser"),v.prototype=g,g.Parser=v,new v})();nX.parser=nX;TBe=nX});var zL,SBe=F(()=>{"use strict";Jt();Yr();$n();zL=class{constructor(){this.stack=[];this.clear=this.clear.bind(this),this.addNode=this.addNode.bind(this),this.getRoot=this.getRoot.bind(this)}static{s(this,"IshikawaDB")}clear(){this.root=void 0,this.stack=[],this.baseLevel=void 0,Tr()}getRoot(){return this.root}addNode(t,r){let n=Tt.sanitizeText(r,Pe());if(!this.root){this.root={text:n,children:[]},this.stack=[{level:0,node:this.root}],Gr(n);return}this.baseLevel??=t;let i=t-this.baseLevel+1;for(i<=0&&(i=1);this.stack.length>1&&this.stack[this.stack.length-1].level>=i;)this.stack.pop();let a=this.stack[this.stack.length-1].node,o={text:n,children:[]};a.children.push(o),this.stack.push({level:i,node:o})}getAccTitle(){return Ir()}setAccTitle(t){Lr(t)}getAccDescription(){return Nr()}setAccDescription(t){Mr(t)}getDiagramTitle(){return Pr()}setDiagramTitle(t){Gr(t)}}});var N7t,px,P7t,O7t,B7t,_Be,wBe,kBe,EBe,$7t,ABe,F7t,G7t,z7t,iX,V7t,W7t,LBe,VL,RBe,mx,DBe,IBe=F(()=>{"use strict";Jt();cs();Vn();er();Kt();N7t=14,px=250,P7t=30,O7t=60,B7t=5,_Be=82*Math.PI/180,wBe=Math.cos(_Be),kBe=Math.sin(_Be),EBe=s((e,t,r)=>{let n=e.node().getBBox(),i=n.width+t*2,a=n.height+t*2;jr(e,a,i,r),e.attr("viewBox",`${n.x-t} ${n.y-t} ${i} ${a}`)},"applyPaddedViewBox"),$7t=s((e,t,r,n)=>{let a=n.db.getRoot();if(!a)return;let o=Pe(),{look:l,handDrawnSeed:u,themeVariables:h}=o,d=Bs(o.fontSize)[0]??N7t,f=l==="handDrawn",p=a.children??[],m=o.ishikawa?.diagramPadding??20,g=o.ishikawa?.useMaxWidth??!1,y=wn(t),v=y.append("g").attr("class","ishikawa"),x=f?ot.svg(y.node()):void 0,b=x?{roughSvg:x,seed:u??0,lineColor:h?.lineColor??"#333",fillColor:h?.mainBkg??"#fff"}:void 0,T=`ishikawa-arrow-${t}`;f||v.append("defs").append("marker").attr("id",T).attr("viewBox","0 0 10 10").attr("refX",0).attr("refY",5).attr("markerWidth",6).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M 10 0 L 0 5 L 10 10 Z").attr("class","ishikawa-arrow");let w=0,C=px,S=f?void 0:mx(v,w,C,w,C,"ishikawa-spine");if(F7t(v,w,C,a.text,d,b),!p.length){f&&mx(v,w,C,w,C,"ishikawa-spine",b),EBe(y,m,g);return}w-=20;let k=p.filter((L,B)=>B%2===0),R=p.filter((L,B)=>B%2===1),M=ABe(k),P=ABe(R),N=M.total+P.total,_=px,A=px;if(N>0){let L=px*2,B=px*.3;_=Math.max(B,L*(M.total/N)),A=Math.max(B,L*(P.total/N))}let I=d*2;_=Math.max(_,M.max*I),A=Math.max(A,P.max*I),C=Math.max(_,px),S&&S.attr("y1",C).attr("y2",C),v.select(".ishikawa-head-group").attr("transform",`translate(0,${C})`);let D=Math.ceil(p.length/2);for(let L=0;LMath.min(O,$.getBBox().x),1/0)}if(f)mx(v,w,C,0,C,"ishikawa-spine",b);else{S.attr("x1",w);let L=`url(#${T})`;v.selectAll("line.ishikawa-branch, line.ishikawa-sub-branch").attr("marker-start",L)}EBe(y,m,g)},"draw"),ABe=s(e=>{let t=s(r=>r.children.reduce((n,i)=>n+1+t(i),0),"countDescendants");return e.reduce((r,n)=>{let i=t(n);return r.total+=i,r.max=Math.max(r.max,i),r},{total:0,max:0})},"sideStats"),F7t=s((e,t,r,n,i,a)=>{let o=Math.max(6,Math.floor(110/(i*.6))),l=e.append("g").attr("class","ishikawa-head-group").attr("transform",`translate(${t},${r})`),u=VL(l,LBe(n,o),0,0,"ishikawa-head-label","start",i),h=u.node().getBBox(),d=Math.max(60,h.width+6),f=Math.max(40,h.height*2+40),p=`M 0 ${-f/2} L 0 ${f/2} Q ${d*2.4} 0 0 ${-f/2} Z`;if(a){let m=a.roughSvg.path(p,{roughness:1.5,seed:a.seed,fill:a.fillColor,fillStyle:"hachure",fillWeight:2.5,hachureGap:5,stroke:a.lineColor,strokeWidth:2});l.insert(()=>m,":first-child").attr("class","ishikawa-head")}else l.insert("path",":first-child").attr("class","ishikawa-head").attr("d",p);u.attr("transform",`translate(${(d-h.width)/2-h.x+3},${-h.y-h.height/2})`)},"drawHead"),G7t=s((e,t)=>{let r=[],n=[],i=s((a,o,l)=>{let u=t===-1?[...a].reverse():a;for(let h of u){let d=r.length,f=h.children??[];r.push({depth:l,text:LBe(h.text,15),parentIndex:o,childCount:f.length}),l%2===0?(n.push(d),f.length&&i(f,d,l+1)):(f.length&&i(f,d,l+1),n.push(d))}},"walk");return i(e,-1,2),{entries:r,yOrder:n}},"flattenTree"),z7t=s((e,t,r,n,i,a,o)=>{let l=e.append("g").attr("class","ishikawa-label-group"),h=VL(l,t,r,n+11*i,"ishikawa-label cause","middle",a).node().getBBox();if(o){let d=o.roughSvg.rectangle(h.x-20,h.y-2,h.width+40,h.height+4,{roughness:1.5,seed:o.seed,fill:o.fillColor,fillStyle:"hachure",fillWeight:2.5,hachureGap:5,stroke:o.lineColor,strokeWidth:2});l.insert(()=>d,":first-child").attr("class","ishikawa-label-box")}else l.insert("rect",":first-child").attr("class","ishikawa-label-box").attr("x",h.x-20).attr("y",h.y-2).attr("width",h.width+40).attr("height",h.height+4)},"drawCauseLabel"),iX=s((e,t,r,n,i,a)=>{let o=Math.sqrt(n*n+i*i);if(o===0)return;let l=n/o,u=i/o,h=6,d=-u*h,f=l*h,p=t,m=r,g=`M ${p} ${m} L ${p-l*h*2+d} ${m-u*h*2+f} L ${p-l*h*2-d} ${m-u*h*2-f} Z`,y=a.roughSvg.path(g,{roughness:1,seed:a.seed,fill:a.lineColor,fillStyle:"solid",stroke:a.lineColor,strokeWidth:1});e.append(()=>y)},"drawArrowMarker"),V7t=s((e,t,r,n,i,a,o,l)=>{let u=t.children??[],h=a*(u.length?1:.2),d=-wBe*h,f=kBe*h*i,p=r+d,m=n+f;if(mx(e,r,n,p,m,"ishikawa-branch",l),l&&iX(e,r,n,r-p,n-m,l),z7t(e,t.text,p,m,i,o,l),!u.length)return;let{entries:g,yOrder:y}=G7t(u,i),v=g.length,x=new Array(v);for(let[S,k]of y.entries())x[k]=n+f*((S+1)/(v+1));let b=new Map;b.set(-1,{x0:r,y0:n,x1:p,y1:m,childCount:u.length,childrenDrawn:0});let T=-wBe,w=kBe*i,C=i<0?"ishikawa-label up":"ishikawa-label down";for(let[S,k]of g.entries()){let R=x[S],M=b.get(k.parentIndex),P=e.append("g").attr("class","ishikawa-sub-group"),N=0,_=0,A=0;if(k.depth%2===0){let I=M.y1-M.y0;N=RBe(M.x0,M.x1,I?(R-M.y0)/I:.5),_=R,A=N-(k.childCount>0?O7t+k.childCount*B7t:P7t),mx(P,N,R,A,R,"ishikawa-sub-branch",l),l&&iX(P,N,R,1,0,l),VL(P,k.text,A,R,"ishikawa-label align","end",o)}else{let I=M.childrenDrawn++;N=RBe(M.x0,M.x1,(M.childCount-I)/(M.childCount+1)),_=M.y0,A=N+T*((R-_)/w),mx(P,N,_,A,R,"ishikawa-sub-branch",l),l&&iX(P,N,_,N-A,_-R,l),VL(P,k.text,A,R,C,"end",o)}k.childCount>0&&b.set(S,{x0:N,y0:_,x1:A,y1:R,childCount:k.childCount,childrenDrawn:0})}},"drawBranch"),W7t=s(e=>e.split(/|\n/),"splitLines"),LBe=s((e,t)=>{if(e.length<=t)return e;let r=[];for(let n of e.split(/\s+/)){let i=r.length-1;i>=0&&r[i].length+1+n.length<=t?r[i]+=" "+n:r.push(n)}return r.join(` +`)},"wrapText"),VL=s((e,t,r,n,i,a,o)=>{let l=W7t(t),u=o*1.05,h=e.append("text").attr("class",i).attr("text-anchor",a).attr("x",r).attr("y",n-(l.length-1)*u/2);for(let[d,f]of l.entries())h.append("tspan").attr("x",r).attr("dy",d===0?0:u).text(f);return h},"drawMultilineText"),RBe=s((e,t,r)=>e+(t-e)*r,"lerp"),mx=s((e,t,r,n,i,a,o)=>{if(o){let l=o.roughSvg.line(t,r,n,i,{roughness:1.5,seed:o.seed,stroke:o.lineColor,strokeWidth:2});e.append(()=>l).attr("class",a);return}return e.append("line").attr("class",a).attr("x1",t).attr("y1",r).attr("x2",n).attr("y2",i)},"drawLine"),DBe={draw:$7t}});var q7t,MBe,NBe=F(()=>{"use strict";q7t=s(e=>` +.ishikawa .ishikawa-spine, +.ishikawa .ishikawa-branch, +.ishikawa .ishikawa-sub-branch { + stroke: ${e.lineColor}; + stroke-width: 2; + fill: none; +} + +.ishikawa .ishikawa-sub-branch { + stroke-width: 1; +} + +.ishikawa .ishikawa-arrow { + fill: ${e.lineColor}; +} + +.ishikawa .ishikawa-head { + fill: ${e.mainBkg}; + stroke: ${e.lineColor}; + stroke-width: 2; +} + +.ishikawa .ishikawa-label-box { + fill: ${e.mainBkg}; + stroke: ${e.lineColor}; + stroke-width: 2; +} + +.ishikawa text { + font-family: ${e.fontFamily}; + font-size: ${e.fontSize}; + fill: ${e.textColor}; +} + +.ishikawa .ishikawa-head-label { + font-weight: 600; + text-anchor: middle; + dominant-baseline: middle; + font-size: 14px; +} + +.ishikawa .ishikawa-label { + text-anchor: end; +} + +.ishikawa .ishikawa-label.cause { + text-anchor: middle; + dominant-baseline: middle; +} + +.ishikawa .ishikawa-label.align { + text-anchor: end; + dominant-baseline: middle; +} + +.ishikawa .ishikawa-label.up { + dominant-baseline: baseline; +} + +.ishikawa .ishikawa-label.down { + dominant-baseline: hanging; +} +`,"getStyles"),MBe=q7t});var PBe={};sr(PBe,{diagram:()=>H7t});var H7t,OBe=F(()=>{"use strict";CBe();SBe();IBe();NBe();H7t={parser:TBe,get db(){return new zL},renderer:DBe,styles:MBe}});var aX,FBe,GBe=F(()=>{"use strict";aX=(function(){var e=s(function(b,T,w,C){for(w=w||{},C=b.length;C--;w[b[C]]=T);return w},"o"),t=[5,8],r=[7,8,11,12,17,19,22,24],n=[1,17],i=[1,18],a=[7,8,11,12,14,15,16,17,19,20,21,22,24,27],o=[1,31],l=[1,39],u=[7,8,11,12,17,19,22,24,27],h=[1,57],d=[1,56],f=[1,58],p=[1,59],m=[1,60],g=[7,8,11,12,16,17,19,20,22,24,27,31,32,33],y={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,optNewlines:4,VENN:5,document:6,EOF:7,NEWLINE:8,line:9,statement:10,TITLE:11,SET:12,identifier:13,BRACKET_LABEL:14,COLON:15,NUMERIC:16,UNION:17,identifierList:18,TEXT:19,IDENTIFIER:20,STRING:21,INDENT_TEXT:22,indentedTextTail:23,STYLE:24,stylesOpt:25,styleField:26,COMMA:27,styleValue:28,valueTokens:29,valueToken:30,HEXCOLOR:31,RGBCOLOR:32,RGBACOLOR:33,$accept:0,$end:1},terminals_:{2:"error",5:"VENN",7:"EOF",8:"NEWLINE",11:"TITLE",12:"SET",14:"BRACKET_LABEL",15:"COLON",16:"NUMERIC",17:"UNION",19:"TEXT",20:"IDENTIFIER",21:"STRING",22:"INDENT_TEXT",24:"STYLE",27:"COMMA",31:"HEXCOLOR",32:"RGBCOLOR",33:"RGBACOLOR"},productions_:[0,[3,4],[4,0],[4,2],[6,0],[6,2],[9,1],[9,1],[10,1],[10,2],[10,3],[10,4],[10,5],[10,2],[10,3],[10,4],[10,5],[10,3],[10,3],[10,3],[10,4],[10,4],[10,2],[10,3],[23,1],[23,1],[23,1],[23,2],[23,2],[25,1],[25,3],[26,3],[28,1],[28,1],[29,1],[29,2],[30,1],[30,1],[30,1],[30,1],[30,1],[18,1],[18,3],[13,1],[13,1]],performAction:s(function(T,w,C,S,k,R,M){var P=R.length-1;switch(k){case 1:return R[P-1];case 2:case 3:case 4:this.$=[];break;case 5:R[P-1].push(R[P]),this.$=R[P-1];break;case 6:this.$=[];break;case 7:case 22:case 32:case 36:case 37:case 38:case 39:case 40:this.$=R[P];break;case 8:S.setDiagramTitle(R[P].substr(6)),this.$=R[P].substr(6);break;case 9:S.addSubsetData([R[P]],void 0,void 0),S.setIndentMode&&S.setIndentMode(!0);break;case 10:S.addSubsetData([R[P-1]],R[P],void 0),S.setIndentMode&&S.setIndentMode(!0);break;case 11:S.addSubsetData([R[P-2]],void 0,parseFloat(R[P])),S.setIndentMode&&S.setIndentMode(!0);break;case 12:S.addSubsetData([R[P-3]],R[P-2],parseFloat(R[P])),S.setIndentMode&&S.setIndentMode(!0);break;case 13:if(R[P].length<2)throw new Error("union requires multiple identifiers");S.validateUnionIdentifiers&&S.validateUnionIdentifiers(R[P]),S.addSubsetData(R[P],void 0,void 0),S.setIndentMode&&S.setIndentMode(!0);break;case 14:if(R[P-1].length<2)throw new Error("union requires multiple identifiers");S.validateUnionIdentifiers&&S.validateUnionIdentifiers(R[P-1]),S.addSubsetData(R[P-1],R[P],void 0),S.setIndentMode&&S.setIndentMode(!0);break;case 15:if(R[P-2].length<2)throw new Error("union requires multiple identifiers");S.validateUnionIdentifiers&&S.validateUnionIdentifiers(R[P-2]),S.addSubsetData(R[P-2],void 0,parseFloat(R[P])),S.setIndentMode&&S.setIndentMode(!0);break;case 16:if(R[P-3].length<2)throw new Error("union requires multiple identifiers");S.validateUnionIdentifiers&&S.validateUnionIdentifiers(R[P-3]),S.addSubsetData(R[P-3],R[P-2],parseFloat(R[P])),S.setIndentMode&&S.setIndentMode(!0);break;case 17:case 18:case 19:S.addTextData(R[P-1],R[P],void 0);break;case 20:case 21:S.addTextData(R[P-2],R[P-1],R[P]);break;case 23:S.addStyleData(R[P-1],R[P]);break;case 24:case 25:case 26:var N=S.getCurrentSets();if(!N)throw new Error("text requires set");S.addTextData(N,R[P],void 0);break;case 27:case 28:var N=S.getCurrentSets();if(!N)throw new Error("text requires set");S.addTextData(N,R[P-1],R[P]);break;case 29:case 41:this.$=[R[P]];break;case 30:case 42:this.$=[...R[P-2],R[P]];break;case 31:this.$=[R[P-2],R[P]];break;case 33:this.$=R[P].join(" ");break;case 34:this.$=[R[P]];break;case 35:R[P-1].push(R[P]),this.$=R[P-1];break;case 43:case 44:this.$=R[P];break}},"anonymous"),table:[e(t,[2,2],{3:1,4:2}),{1:[3]},{5:[1,3],8:[1,4]},e(r,[2,4],{6:5}),e(t,[2,3]),{7:[1,6],8:[1,8],9:7,10:9,11:[1,10],12:[1,11],17:[1,12],19:[1,13],22:[1,14],24:[1,15]},{1:[2,1]},e(r,[2,5]),e(r,[2,6]),e(r,[2,7]),e(r,[2,8]),{13:16,20:n,21:i},{13:20,18:19,20:n,21:i},{13:20,18:21,20:n,21:i},{16:[1,25],20:[1,23],21:[1,24],23:22},{13:20,18:26,20:n,21:i},e(r,[2,9],{14:[1,27],15:[1,28]}),e(a,[2,43]),e(a,[2,44]),e(r,[2,13],{14:[1,29],15:[1,30],27:o}),e(a,[2,41]),{16:[1,34],20:[1,32],21:[1,33],27:o},e(r,[2,22]),e(r,[2,24],{14:[1,35]}),e(r,[2,25],{14:[1,36]}),e(r,[2,26]),{20:l,25:37,26:38,27:o},e(r,[2,10],{15:[1,40]}),{16:[1,41]},e(r,[2,14],{15:[1,42]}),{16:[1,43]},{13:44,20:n,21:i},e(r,[2,17],{14:[1,45]}),e(r,[2,18],{14:[1,46]}),e(r,[2,19]),e(r,[2,27]),e(r,[2,28]),e(r,[2,23],{27:[1,47]}),e(u,[2,29]),{15:[1,48]},{16:[1,49]},e(r,[2,11]),{16:[1,50]},e(r,[2,15]),e(a,[2,42]),e(r,[2,20]),e(r,[2,21]),{20:l,26:51},{16:h,20:d,21:[1,53],28:52,29:54,30:55,31:f,32:p,33:m},e(r,[2,12]),e(r,[2,16]),e(u,[2,30]),e(u,[2,31]),e(u,[2,32]),e(u,[2,33],{30:61,16:h,20:d,31:f,32:p,33:m}),e(g,[2,34]),e(g,[2,36]),e(g,[2,37]),e(g,[2,38]),e(g,[2,39]),e(g,[2,40]),e(g,[2,35])],defaultActions:{6:[2,1]},parseError:s(function(T,w){if(w.recoverable)this.trace(T);else{var C=new Error(T);throw C.hash=w,C}},"parseError"),parse:s(function(T){var w=this,C=[0],S=[],k=[null],R=[],M=this.table,P="",N=0,_=0,A=0,I=2,D=1,L=R.slice.call(arguments,1),B=Object.create(this.lexer),O={yy:{}};for(var $ in this.yy)Object.prototype.hasOwnProperty.call(this.yy,$)&&(O.yy[$]=this.yy[$]);B.setInput(T,O.yy),O.yy.lexer=B,O.yy.parser=this,typeof B.yylloc>"u"&&(B.yylloc={});var W=B.yylloc;R.push(W);var V=B.options&&B.options.ranges;typeof O.yy.parseError=="function"?this.parseError=O.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function z(Ye){C.length=C.length-2*Ye,k.length=k.length-Ye,R.length=R.length-Ye}s(z,"popStack");function H(){var Ye;return Ye=S.pop()||B.lex()||D,typeof Ye!="number"&&(Ye instanceof Array&&(S=Ye,Ye=S.pop()),Ye=w.symbols_[Ye]||Ye),Ye}s(H,"lex");for(var K,J,ne,Z,pe,te,fe={},ce,de,Ee,we;;){if(ne=C[C.length-1],this.defaultActions[ne]?Z=this.defaultActions[ne]:((K===null||typeof K>"u")&&(K=H()),Z=M[ne]&&M[ne][K]),typeof Z>"u"||!Z.length||!Z[0]){var Fe="";we=[];for(ce in M[ne])this.terminals_[ce]&&ce>I&&we.push("'"+this.terminals_[ce]+"'");B.showPosition?Fe="Parse error on line "+(N+1)+`: +`+B.showPosition()+` +Expecting `+we.join(", ")+", got '"+(this.terminals_[K]||K)+"'":Fe="Parse error on line "+(N+1)+": Unexpected "+(K==D?"end of input":"'"+(this.terminals_[K]||K)+"'"),this.parseError(Fe,{text:B.match,token:this.terminals_[K]||K,line:B.yylineno,loc:W,expected:we})}if(Z[0]instanceof Array&&Z.length>1)throw new Error("Parse Error: multiple actions possible at state: "+ne+", token: "+K);switch(Z[0]){case 1:C.push(K),k.push(B.yytext),R.push(B.yylloc),C.push(Z[1]),K=null,J?(K=J,J=null):(_=B.yyleng,P=B.yytext,N=B.yylineno,W=B.yylloc,A>0&&A--);break;case 2:if(de=this.productions_[Z[1]][1],fe.$=k[k.length-de],fe._$={first_line:R[R.length-(de||1)].first_line,last_line:R[R.length-1].last_line,first_column:R[R.length-(de||1)].first_column,last_column:R[R.length-1].last_column},V&&(fe._$.range=[R[R.length-(de||1)].range[0],R[R.length-1].range[1]]),te=this.performAction.apply(fe,[P,_,N,O.yy,Z[1],k,R].concat(L)),typeof te<"u")return te;de&&(C=C.slice(0,-1*de*2),k=k.slice(0,-1*de),R=R.slice(0,-1*de)),C.push(this.productions_[Z[1]][0]),k.push(fe.$),R.push(fe._$),Ee=M[C[C.length-2]][C[C.length-1]],C.push(Ee);break;case 3:return!0}}return!0},"parse")},v=(function(){var b={EOF:1,parseError:s(function(w,C){if(this.yy.parser)this.yy.parser.parseError(w,C);else throw new Error(w)},"parseError"),setInput:s(function(T,w){return this.yy=w||this.yy||{},this._input=T,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var T=this._input[0];this.yytext+=T,this.yyleng++,this.offset++,this.match+=T,this.matched+=T;var w=T.match(/(?:\r\n?|\n).*/g);return w?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),T},"input"),unput:s(function(T){var w=T.length,C=T.split(/(?:\r\n?|\n)/g);this._input=T+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-w),this.offset-=w;var S=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),C.length-1&&(this.yylineno-=C.length-1);var k=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:C?(C.length===S.length?this.yylloc.first_column:0)+S[S.length-C.length].length-C[0].length:this.yylloc.first_column-w},this.options.ranges&&(this.yylloc.range=[k[0],k[0]+this.yyleng-w]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(T){this.unput(this.match.slice(T))},"less"),pastInput:s(function(){var T=this.matched.substr(0,this.matched.length-this.match.length);return(T.length>20?"...":"")+T.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var T=this.match;return T.length<20&&(T+=this._input.substr(0,20-T.length)),(T.substr(0,20)+(T.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var T=this.pastInput(),w=new Array(T.length+1).join("-");return T+this.upcomingInput()+` +`+w+"^"},"showPosition"),test_match:s(function(T,w){var C,S,k;if(this.options.backtrack_lexer&&(k={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(k.yylloc.range=this.yylloc.range.slice(0))),S=T[0].match(/(?:\r\n?|\n).*/g),S&&(this.yylineno+=S.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:S?S[S.length-1].length-S[S.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+T[0].length},this.yytext+=T[0],this.match+=T[0],this.matches=T,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(T[0].length),this.matched+=T[0],C=this.performAction.call(this,this.yy,this,w,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),C)return C;if(this._backtrack){for(var R in k)this[R]=k[R];return!1}return!1},"test_match"),next:s(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var T,w,C,S;this._more||(this.yytext="",this.match="");for(var k=this._currentRules(),R=0;Rw[0].length)){if(w=C,S=R,this.options.backtrack_lexer){if(T=this.test_match(C,k[R]),T!==!1)return T;if(this._backtrack){w=!1;continue}else return!1}else if(!this.options.flex)break}return w?(T=this.test_match(w,k[S]),T!==!1?T:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:s(function(){var w=this.next();return w||this.lex()},"lex"),begin:s(function(w){this.conditionStack.push(w)},"begin"),popState:s(function(){var w=this.conditionStack.length-1;return w>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:s(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:s(function(w){return w=this.conditionStack.length-1-Math.abs(w||0),w>=0?this.conditionStack[w]:"INITIAL"},"topState"),pushState:s(function(w){this.begin(w)},"pushState"),stateStackSize:s(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:s(function(w,C,S,k){var R=k;switch(S){case 0:break;case 1:break;case 2:break;case 3:if(w.getIndentMode&&w.getIndentMode())return w.consumeIndentText=!0,this.begin("INITIAL"),22;break;case 4:break;case 5:w.setIndentMode&&w.setIndentMode(!1),this.begin("INITIAL"),this.unput(C.yytext);break;case 6:return this.begin("bol"),8;break;case 7:break;case 8:break;case 9:return 7;case 10:return 11;case 11:return 5;case 12:return 12;case 13:return 17;case 14:if(w.consumeIndentText)w.consumeIndentText=!1;else return 19;break;case 15:return 24;case 16:return C.yytext=C.yytext.slice(2,-2),14;break;case 17:return C.yytext=C.yytext.slice(1,-1).trim(),14;break;case 18:return 16;case 19:return 31;case 20:return 33;case 21:return 32;case 22:return 20;case 23:return 21;case 24:return 27;case 25:return 15}},"anonymous"),rules:[/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[ \t]+(?=[\n\r]))/i,/^(?:[ \t]+(?=text\b))/i,/^(?:[ \t]+)/i,/^(?:[^ \t\n\r])/i,/^(?:[\n\r]+)/i,/^(?:%%[^\n]*)/i,/^(?:[ \t]+)/i,/^(?:$)/i,/^(?:title\s[^#\n;]+)/i,/^(?:venn-beta\b)/i,/^(?:set\b)/i,/^(?:union\b)/i,/^(?:text\b)/i,/^(?:style\b)/i,/^(?:\["[^\"]*"\])/i,/^(?:\[[^\]\"]+\])/i,/^(?:[+-]?(\d+(\.\d+)?|\.\d+))/i,/^(?:#[0-9a-fA-F]{3,8})/i,/^(?:rgba\(\s*[0-9.]+\s*[,]\s*[0-9.]+\s*[,]\s*[0-9.]+\s*[,]\s*[0-9.]+\s*\))/i,/^(?:rgb\(\s*[0-9.]+\s*[,]\s*[0-9.]+\s*[,]\s*[0-9.]+\s*\))/i,/^(?:[A-Za-z_][A-Za-z0-9\-_]*)/i,/^(?:"[^\"]*")/i,/^(?:,)/i,/^(?::)/i],conditions:{bol:{rules:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25],inclusive:!0},INITIAL:{rules:[0,1,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25],inclusive:!0}}};return b})();y.lexer=v;function x(){this.yy={}}return s(x,"Parser"),x.prototype=y,y.Parser=x,new x})();aX.parser=aX;FBe=aX});function sIt(){return Xr(aIt,Lt().venn)}var sX,oX,lX,cX,uX,hX,j7t,X7t,aw,K7t,Z7t,Q7t,J7t,WL,eIt,tIt,rIt,nIt,iIt,aIt,oIt,zBe,VBe=F(()=>{"use strict";er();br();$n();Qi();sX=[],oX=[],lX=[],cX=new Set,hX=!1,j7t=s((e,t,r)=>{let n=WL(e).sort(),i=r??10/Math.pow(e.length,2);uX=n,n.length===1&&cX.add(n[0]),sX.push({sets:n,size:i,label:t?aw(t):void 0})},"addSubsetData"),X7t=s(()=>sX,"getSubsetData"),aw=s(e=>{let t=e.trim();return t.length>=2&&t.startsWith('"')&&t.endsWith('"')?t.slice(1,-1):t},"normalizeText"),K7t=s(e=>e&&aw(e),"normalizeStyleValue"),Z7t=s((e,t,r)=>{let n=aw(t);oX.push({sets:WL(e).sort(),id:n,label:r?aw(r):void 0})},"addTextData"),Q7t=s((e,t)=>{let r=WL(e).sort(),n={};for(let[i,a]of t)n[i]=K7t(a)??a;lX.push({targets:r,styles:n})},"addStyleData"),J7t=s(()=>lX,"getStyleData"),WL=s(e=>e.map(t=>aw(t)),"normalizeIdentifierList"),eIt=s(e=>{let r=WL(e).filter(n=>!cX.has(n));if(r.length>0)throw new Error(`unknown set identifier: ${r.join(", ")}`)},"validateUnionIdentifiers"),tIt=s(()=>oX,"getTextData"),rIt=s(()=>uX,"getCurrentSets"),nIt=s(()=>hX,"getIndentMode"),iIt=s(e=>{hX=e},"setIndentMode"),aIt=mr.venn;s(sIt,"getConfig");oIt=s(()=>{Tr(),sX.length=0,oX.length=0,lX.length=0,cX.clear(),uX=void 0,hX=!1},"customClear"),zBe={getConfig:sIt,clear:oIt,setAccTitle:Lr,getAccTitle:Ir,setDiagramTitle:Gr,getDiagramTitle:Pr,getAccDescription:Nr,setAccDescription:Mr,addSubsetData:j7t,getSubsetData:X7t,addTextData:Z7t,addStyleData:Q7t,validateUnionIdentifiers:eIt,getTextData:tIt,getStyleData:J7t,getCurrentSets:rIt,getIndentMode:nIt,setIndentMode:iIt}});var lIt,WBe,qBe=F(()=>{"use strict";lIt=s(e=>` + .venn-title { + font-size: 32px; + fill: ${e.vennTitleTextColor}; + font-family: ${e.fontFamily}; + } + + .venn-circle text { + font-size: 48px; + font-family: ${e.fontFamily}; + } + + .venn-intersection text { + font-size: 48px; + fill: ${e.vennSetTextColor}; + font-family: ${e.fontFamily}; + } + + .venn-text-node { + font-family: ${e.fontFamily}; + color: ${e.vennSetTextColor}; + } +`,"getStyles"),WBe=lIt});function qL(e,t){let r=uIt(e),n=r.filter(l=>cIt(l,e)),i=0,a=0,o=[];if(n.length>1){let l=jBe(n);for(let h=0;hd.angle-h.angle);let u=n[n.length-1];for(let h=0;hg.radius*2&&(T=g.radius*2),(p==null||p.width>T)&&(p={circle:g,width:T,p1:d,p2:u,large:T>g.radius,sweep:!0})}p!=null&&(o.push(p),i+=pX(p.circle.radius,p.width),u=d)}}else{let l=e[0];for(let h=1;hMath.abs(l.radius-e[h].radius)){u=!0;break}u?i=a=0:(i=l.radius*l.radius*Math.PI,o.push({circle:l,p1:{x:l.x,y:l.y+l.radius},p2:{x:l.x-1e-10,y:l.y+l.radius},width:l.radius*2,large:!0,sweep:!0}))}return a/=2,t&&(t.area=i+a,t.arcArea=i,t.polygonArea=a,t.arcs=o,t.innerPoints=n,t.intersectionPoints=r),i+a}function cIt(e,t){return t.every(r=>Do(e,r)=e+t)return 0;if(r<=Math.abs(e-t))return Math.PI*Math.min(e,t)*Math.min(e,t);let n=e-(r*r-t*t+e*e)/(2*r),i=t-(r*r-e*e+t*t)/(2*r);return pX(e,n)+pX(t,i)}function YBe(e,t){let r=Do(e,t),n=e.radius,i=t.radius;if(r>=n+i||r<=Math.abs(n-i))return[];let a=(n*n-i*i+r*r)/(2*r),o=Math.sqrt(n*n-a*a),l=e.x+a*(t.x-e.x)/r,u=e.y+a*(t.y-e.y)/r,h=-(t.y-e.y)*(o/r),d=-(t.x-e.x)*(o/r);return[{x:l+h,y:u-d},{x:l-h,y:u+d}]}function jBe(e){let t={x:0,y:0};for(let r of e)t.x+=r.x,t.y+=r.y;return t.x/=e.length,t.y/=e.length,t}function hIt(e,t,r,n){n=n||{};let i=n.maxIterations||100,a=n.tolerance||1e-10,o=e(t),l=e(r),u=r-t;if(o*l>0)throw"Initial bisect points must have opposite signs";if(o===0)return t;if(l===0)return r;for(let h=0;h=0&&(t=d),Math.abs(u)mX(t))}function gx(e,t){let r=0;for(let n=0;nC.fx-S.fx,"sortOrder"),x=t.slice(),b=t.slice(),T=t.slice(),w=t.slice();for(let C=0;C{let M=R.slice();return M.fx=R.fx,M.id=R.id,M});k.sort((R,M)=>R.id-M.id),r.history.push({x:g[0].slice(),fx:g[0].fx,simplex:k})}p=0;for(let k=0;k=g[m-1].fx){let k=!1;if(b.fx>S.fx?(yd(T,1+d,x,-d,S),T.fx=e(T),T.fx=1)break;for(let R=1;Rl+a*i*u||h>=v)y=i;else{if(Math.abs(f)<=-o*u)return i;f*(y-g)>=0&&(y=g),g=i,v=h}return 0}s(m,"zoom");for(let g=0;g<10;++g){if(yd(n.x,1,r.x,i,t),h=n.fx=e(n.x,n.fxprime),f=gx(n.fxprime,t),h>l+a*i*u||g&&h>=d)return m(p,i,d);if(Math.abs(f)<=-o*u)return i;if(f>=0)return m(i,p,h);d=h,p=i,i*=2}return i}function fIt(e,t,r){let n={x:t.slice(),fx:0,fxprime:t.slice()},i={x:t.slice(),fx:0,fxprime:t.slice()},a=t.slice(),o,l,u=1,h;r=r||{},h=r.maxIterations||t.length*20,n.fx=e(n.x,n.fxprime),o=n.fxprime.slice(),yX(o,n.fxprime,-1);for(let d=0;d{let f={};for(let p=0;pbX(e,t,n)-r,0,e+t)}function pIt(e,t={}){let r=t.distinct,n=e.map(l=>Object.assign({},l));function i(l){return l.join(";")}if(s(i,"toKey"),r){let l=new Map;for(let u of n)for(let h=0;hl===u?0:la.sets.length===2).forEach(a=>{let o=r[a.sets[0]],l=r[a.sets[1]],u=Math.sqrt(t[o].size/Math.PI),h=Math.sqrt(t[l].size/Math.PI),d=vX(u,h,a.size);n[o][l]=n[l][o]=d;let f=0;a.size+1e-10>=Math.min(t[o].size,t[l].size)?f=1:a.size<=1e-10&&(f=-1),i[o][l]=i[l][o]=f}),{distances:n,constraints:i}}function gIt(e,t,r,n){for(let a=0;a0&&g<=f||p<0&&g>=f||(i+=2*y*y,t[2*a]+=4*y*(o-h),t[2*a+1]+=4*y*(l-d),t[2*u]+=4*y*(h-o),t[2*u+1]+=4*y*(d-l))}}return i}function yIt(e,t={}){let r=xIt(e,t),n=t.lossFunction||yx;if(e.length>=8){let i=vIt(e,t),a=n(i,e),o=n(r,e);a+1e-8p.map(m=>m/l));let u=s((p,m)=>gIt(p,m,a,o),"obj"),h=null;for(let p=0;pf.sets.length===2);for(let f of e){let p=f.weight!=null?f.weight:1,m=f.sets[0],g=f.sets[1];f.size+ZBe>=Math.min(n[m].size,n[g].size)&&(p=0),i[m].push({set:g,size:f.size,weight:p}),i[g].push({set:m,size:f.size,weight:p})}let a=[];Object.keys(i).forEach(f=>{let p=0;for(let m=0;me[o]));let a=n.weight!=null?n.weight:1;r+=a*(i-n.size)*(i-n.size)}return r}function QBe(e,t){let r=0;for(let n of t){if(n.sets.length===1)continue;let i;if(n.sets.length===2){let l=e[n.sets[0]],u=e[n.sets[1]];i=bX(l.radius,u.radius,Do(l,u))}else i=qL(n.sets.map(l=>e[l]));let a=n.weight!=null?n.weight:1,o=Math.log((i+1)/(n.size+1));r+=a*o*o}return r}function bIt(e,t,r){if(r==null?e.sort((i,a)=>a.radius-i.radius):e.sort(r),e.length>0){let i=e[0].x,a=e[0].y;for(let o of e)o.x-=i,o.y-=a}if(e.length===2&&Do(e[0],e[1])1){let i=Math.atan2(e[1].x,e[1].y)-t,a=Math.cos(i),o=Math.sin(i);for(let l of e){let u=l.x,h=l.y;l.x=a*u-o*h,l.y=o*u+a*h}}if(e.length>2){let i=Math.atan2(e[2].x,e[2].y)-t;for(;i<0;)i+=2*Math.PI;for(;i>2*Math.PI;)i-=2*Math.PI;if(i>Math.PI){let a=e[1].y/(1e-10+e[1].x);for(let o of e){var n=(o.x+a*o.y)/(1+a*a);o.x=2*n-o.x,o.y=2*n*a-o.y}}}}function TIt(e){e.forEach(i=>{i.parent=i});function t(i){return i.parent!==i&&(i.parent=t(i.parent)),i.parent}s(t,"find");function r(i,a){let o=t(i),l=t(a);o.parent=l}s(r,"union");for(let i=0;i{delete i.parent}),Array.from(n.values())}function xX(e){let t=s(r=>{let n=e.reduce((a,o)=>Math.max(a,o[r]+o.radius),Number.NEGATIVE_INFINITY),i=e.reduce((a,o)=>Math.min(a,o[r]-o.radius),Number.POSITIVE_INFINITY);return{max:n,min:i}},"minMax");return{xRange:t("x"),yRange:t("y")}}function JBe(e,t,r){t==null&&(t=Math.PI/2);let n=r$e(e).map(h=>Object.assign({},h)),i=TIt(n);for(let h of i){bIt(h,t,r);let d=xX(h);h.size=(d.xRange.max-d.xRange.min)*(d.yRange.max-d.yRange.min),h.bounds=d}i.sort((h,d)=>d.size-h.size),n=i[0];let a=n.bounds,o=(a.xRange.max-a.xRange.min)/50;function l(h,d,f){if(!h)return;let p=h.bounds,m,g;if(d)m=a.xRange.max-p.xRange.min+o;else{m=a.xRange.max-p.xRange.max;let y=(p.xRange.max-p.xRange.min)/2-(a.xRange.max-a.xRange.min)/2;y<0&&(m+=y)}if(f)g=a.yRange.max-p.yRange.min+o;else{g=a.yRange.max-p.yRange.max;let y=(p.yRange.max-p.yRange.min)/2-(a.yRange.max-a.yRange.min)/2;y<0&&(g+=y)}for(let y of h)y.x+=m,y.y+=g,n.push(y)}s(l,"addCluster");let u=1;for(;u({radius:d*m.radius,x:n+f+(m.x-o.min)*d,y:n+p+(m.y-l.min)*d,setid:m.setid})))}function t$e(e){let t={};for(let r of e)t[r.setid]=r;return t}function r$e(e){return Object.keys(e).map(r=>Object.assign(e[r],{setid:r}))}function n$e(e={}){let t=!1,r=600,n=350,i=15,a=1e3,o=Math.PI/2,l=!0,u=null,h=!0,d=!0,f=null,p=null,m=!1,g=null,y=e&&e.symmetricalTextCentre?e.symmetricalTextCentre:!1,v={},x=e&&e.colourScheme?e.colourScheme:e&&e.colorScheme?e.colorScheme:["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"],b=0,T=s(function(k){if(k in v)return v[k];var R=v[k]=x[b];return b+=1,b>=x.length&&(b=0),R},"colours"),w=KBe,C=yx;function S(k){let R=k.datum(),M=new Set;R.forEach(Z=>{Z.size==0&&Z.sets.length==1&&M.add(Z.sets[0])}),R=R.filter(Z=>!Z.sets.some(pe=>M.has(pe)));let P={},N={};if(R.length>0){let Z=w(R,{lossFunction:C,distinct:m});l&&(Z=JBe(Z,o,p)),P=e$e(Z,r,n,i,u),N=a$e(P,R,y)}let _={};R.forEach(Z=>{Z.label&&(_[Z.sets]=Z.label)});function A(Z){if(Z.sets in _)return _[Z.sets];if(Z.sets.length==1)return""+Z.sets[0]}s(A,"label"),k.selectAll("svg").data([P]).enter().append("svg");let I=k.select("svg");t?I.attr("viewBox",`0 0 ${r} ${n}`):I.attr("width",r).attr("height",n);let D={},L=!1;I.selectAll(".venn-area path").each(function(Z){let pe=this.getAttribute("d");Z.sets.length==1&&pe&&!m&&(L=!0,D[Z.sets[0]]=wIt(pe))});function B(Z){return pe=>{let te=Z.sets.map(fe=>{let ce=D[fe],de=P[fe];return ce||(ce={x:r/2,y:n/2,radius:1}),de||(de={x:r/2,y:n/2,radius:1}),{x:ce.x*(1-pe)+de.x*pe,y:ce.y*(1-pe)+de.y*pe,radius:ce.radius*(1-pe)+de.radius*pe}});return UBe(te,g)}}s(B,"pathTween");let O=I.selectAll(".venn-area").data(R,Z=>Z.sets),$=O.enter().append("g").attr("class",Z=>`venn-area venn-${Z.sets.length==1?"circle":"intersection"}${Z.colour||Z.color?" venn-coloured":""}`).attr("data-venn-sets",Z=>Z.sets.join("_")),W=$.append("path"),V=$.append("text").attr("class","label").text(Z=>A(Z)).attr("text-anchor","middle").attr("dy",".35em").attr("x",r/2).attr("y",n/2);d&&(W.style("fill-opacity","0").filter(Z=>Z.sets.length==1).style("fill",Z=>Z.colour?Z.colour:Z.color?Z.color:T(Z.sets)).style("fill-opacity",".25"),V.style("fill",Z=>Z.colour||Z.color?"#FFF":e.textFill?e.textFill:Z.sets.length==1?T(Z.sets):"#444"));function z(Z){return typeof Z.transition=="function"?Z.transition("venn").duration(a):Z}s(z,"asTransition");let H=k;L&&typeof H.transition=="function"?(H=z(k),H.selectAll("path").attrTween("d",B)):H.selectAll("path").attr("d",Z=>UBe(Z.sets.map(pe=>P[pe])),g);let K=H.selectAll("text").filter(Z=>Z.sets in N).text(Z=>A(Z)).attr("x",Z=>Math.floor(N[Z.sets].x)).attr("y",Z=>Math.floor(N[Z.sets].y));h&&(L?"on"in K?K.on("end",dX(P,A)):K.each("end",dX(P,A)):K.each(dX(P,A)));let J=z(O.exit()).remove();typeof O.transition=="function"&&J.selectAll("path").attrTween("d",B);let ne=J.selectAll("text").attr("x",r/2).attr("y",n/2);return f!==null&&(V.style("font-size","0px"),K.style("font-size",f),ne.style("font-size","0px")),{circles:P,textCentres:N,nodes:O,enter:$,update:H,exit:J}}return s(S,"chart"),S.wrap=function(k){return arguments.length?(h=k,S):h},S.useViewBox=function(){return t=!0,S},S.width=function(k){return arguments.length?(r=k,S):r},S.height=function(k){return arguments.length?(n=k,S):n},S.padding=function(k){return arguments.length?(i=k,S):i},S.distinct=function(k){return arguments.length?(m=k,S):m},S.colours=function(k){return arguments.length?(T=k,S):T},S.colors=function(k){return arguments.length?(T=k,S):T},S.fontSize=function(k){return arguments.length?(f=k,S):f},S.round=function(k){return arguments.length?(g=k,S):g},S.duration=function(k){return arguments.length?(a=k,S):a},S.layoutFunction=function(k){return arguments.length?(w=k,S):w},S.normalize=function(k){return arguments.length?(l=k,S):l},S.scaleToFit=function(k){return arguments.length?(u=k,S):u},S.styled=function(k){return arguments.length?(d=k,S):d},S.orientation=function(k){return arguments.length?(o=k,S):o},S.orientationOrder=function(k){return arguments.length?(p=k,S):p},S.lossFunction=function(k){return arguments.length?(C=k==="default"?yx:k==="logRatio"?QBe:k,S):C},S}function dX(e,t){return function(r){let n=this,i=e[r.sets[0]].radius||50,a=t(r)||"",o=a.split(/\s+/).reverse(),u=(a.length+o.length)/3,h=o.pop(),d=[h],f=0,p=1.1;n.textContent=null;let m=[];function g(T){let w=n.ownerDocument.createElementNS(n.namespaceURI,"tspan");return w.textContent=T,m.push(w),n.append(w),w}s(g,"append");let y=g(h);for(;h=o.pop(),!!h;){d.push(h);let T=d.join(" ");y.textContent=T,T.length>u&&y.getComputedTextLength()>i&&(d.pop(),y.textContent=d.join(" "),d=[h],y=g(h),f++)}let v=.35-f*p/2,x=n.getAttribute("x"),b=n.getAttribute("y");m.forEach((T,w)=>{T.setAttribute("x",x),T.setAttribute("y",b),T.setAttribute("dy",`${v+w*p}em`)})}}function fX(e,t,r){let n=t[0].radius-Do(t[0],e);for(let i=1;i=a&&(i=n[d],a=f)}let o=XBe(d=>-1*fX({x:d[0],y:d[1]},e,t),[i.x,i.y],{maxIterations:500,minErrorDelta:1e-10}).x,l={x:r?0:o[0],y:o[1]},u=!0;for(let d of e)if(Do(l,d)>d.radius){u=!1;break}for(let d of t)if(Do(l,d)d.p1))}function CIt(e){let t={},r=Object.keys(e);for(let n of r)t[n]=[];for(let n=0;n0&&console.log("WARNING: area "+o+" not represented on screen")}return n}function SIt(e,t,r){let n=[];return n.push(` +M`,e,t),n.push(` +m`,-r,0),n.push(` +a`,r,r,0,1,0,r*2,0),n.push(` +a`,r,r,0,1,0,-r*2,0),n.join(" ")}function wIt(e){let t=e.split(" ");return{x:Number.parseFloat(t[1]),y:Number.parseFloat(t[2]),radius:-Number.parseFloat(t[4])}}function s$e(e){if(e.length===0)return[];let t={};return qL(e,t),t.arcs}function o$e(e,t){if(e.length===0)return"M 0 0";let r=Math.pow(10,t||0),n=t!=null?a=>Math.round(a*r)/r:a=>a;if(e.length==1){let a=e[0].circle;return SIt(n(a.x),n(a.y),n(a.radius))}let i=[` +M`,n(e[0].p2.x),n(e[0].p2.y)];for(let a of e){let o=n(a.circle.radius);i.push(` +A`,o,o,0,a.large?1:0,a.sweep?1:0,n(a.p1.x),n(a.p1.y))}return i.join(" ")}function UBe(e,t){return o$e(s$e(e),t)}function l$e(e,t={}){let{lossFunction:r,layoutFunction:n=KBe,normalize:i=!0,orientation:a=Math.PI/2,orientationOrder:o,width:l=600,height:u=350,padding:h=15,scaleToFit:d=!1,symmetricalTextCentre:f=!1,distinct:p,round:m=2}=t,g=n(e,{lossFunction:r==="default"||!r?yx:r==="logRatio"?QBe:r,distinct:p});i&&(g=JBe(g,a,o));let y=e$e(g,l,u,h,d),v=a$e(y,e,f),x=new Map(Object.keys(y).map(w=>[w,{set:w,x:y[w].x,y:y[w].y,radius:y[w].radius}])),b=e.map(w=>{let C=w.sets.map(R=>x.get(R)),S=s$e(C),k=o$e(S,m);return{circles:C,arcs:S,path:k,area:w,has:new Set(w.sets)}});function T(w){let C="";for(let S of b)S.has.size>w.length&&w.every(k=>S.has.has(k))&&(C+=" "+S.path);return C}return s(T,"genDistinctPath"),b.map(({circles:w,arcs:C,path:S,area:k})=>({data:k,text:v[k.sets],circles:w,arcs:C,path:S,distinctPath:S+T(k.sets)}))}var ZBe,c$e=F(()=>{"use strict";s(qL,"intersectionArea");s(cIt,"containedInCircles");s(uIt,"getIntersectionPoints");s(pX,"circleArea");s(Do,"distance");s(bX,"circleOverlap");s(YBe,"circleCircleIntersection");s(jBe,"getCenter");s(hIt,"bisect");s(mX,"zeros");s(HBe,"zerosM");s(gx,"dot");s(gX,"norm2");s(yX,"scale");s(yd,"weightedSum");s(XBe,"nelderMead");s(dIt,"wolfeLineSearch");s(fIt,"conjugateGradient");s(KBe,"venn");ZBe=1e-10;s(vX,"distanceFromIntersectArea");s(pIt,"addMissingAreas");s(mIt,"getDistanceMatrices");s(gIt,"constrainedMDSGradient");s(yIt,"bestInitialLayout");s(vIt,"constrainedMDSLayout");s(xIt,"greedyLayout");s(yx,"lossFunction");s(QBe,"logRatioLossFunction");s(bIt,"orientateCircles");s(TIt,"disjointCluster");s(xX,"getBoundingBox");s(JBe,"normalizeSolution");s(e$e,"scaleSolution");s(t$e,"toObjectNotation");s(r$e,"fromObjectNotation");s(n$e,"VennDiagram");s(dX,"wrapText");s(fX,"circleMargin");s(i$e,"computeTextCentre");s(CIt,"getOverlappingCircles");s(a$e,"computeTextCentres");s(SIt,"circlePath");s(wIt,"circleFromPath");s(s$e,"intersectionAreaArcs");s(o$e,"arcsToPath");s(UBe,"intersectionAreaPath");s(l$e,"layout")});function EIt(e){let t=new Map;for(let r of e){let n=r.targets.join("|"),i=t.get(n);i?Object.assign(i,r.styles):t.set(n,{...r.styles})}return t}function M0(e){return e.join("|")}function RIt(e,t,r,n,i,a){let o=e?.useDebugLayout??!1,u=r.select("svg").append("g").attr("class","venn-text-nodes"),h=new Map;for(let d of n){let f=M0(d.sets),p=h.get(f);p?p.push(d):h.set(f,[d])}for(let[d,f]of h.entries()){let p=t.get(d);if(!p?.text)continue;let m=p.text.x,g=p.text.y,y=Math.min(...p.circles.map(I=>I.radius)),v=Math.min(...p.circles.map(I=>I.radius-Math.hypot(m-I.x,g-I.y))),x=Number.isFinite(v)?Math.max(0,v):0;x===0&&Number.isFinite(y)&&(x=y*.6);let b=u.append("g").attr("class","venn-text-area").attr("font-size",`${40*i}px`);o&&b.append("circle").attr("class","venn-text-debug-circle").attr("cx",m).attr("cy",g).attr("r",x).attr("fill","none").attr("stroke","purple").attr("stroke-width",1.5*i).attr("stroke-dasharray",`${6*i} ${4*i}`);let T=Math.max(80*i,x*2*.95),w=Math.max(60*i,x*2*.95),k=(p.data.label&&p.data.label.length>0?Math.min(32*i,x*.25):0)+(f.length<=2?30*i:0),R=m-T/2,M=g-w/2+k,P=Math.max(1,Math.ceil(Math.sqrt(f.length))),N=Math.max(1,Math.ceil(f.length/P)),_=T/P,A=w/N;for(let[I,D]of f.entries()){let L=I%P,B=Math.floor(I/P),O=R+_*(L+.5),$=M+A*(B+.5);o&&b.append("rect").attr("class","venn-text-debug-cell").attr("x",R+_*L).attr("y",M+A*B).attr("width",_).attr("height",A).attr("fill","none").attr("stroke","teal").attr("stroke-width",1*i).attr("stroke-dasharray",`${4*i} ${3*i}`);let W=_*.9,V=A*.9,z=b.append("foreignObject").attr("class","venn-text-node-fo").attr("width",W).attr("height",V).attr("x",O-W/2).attr("y",$-V/2).attr("overflow","visible"),H=a.get(D.id)?.color,K=z.append("xhtml:span").attr("class","venn-text-node").style("display","flex").style("width","100%").style("height","100%").style("white-space","normal").style("align-items","center").style("justify-content","center").style("text-align","center").style("overflow-wrap","normal").style("word-break","normal").text(D.label??D.id);H&&K.style("color",H)}}}function _It(e){let t=new Set(e.map(i=>[...i.sets].sort().join("|"))),r=new Map(e.filter(i=>i.sets.length===1&&i.size!==void 0).map(i=>[i.sets[0],i.size])),n=[];for(let i of e){if(i.sets.length<3)continue;let a=[...i.sets].sort();for(let o=0;o0?[...e,...n]:e}var AIt,u$e,h$e=F(()=>{"use strict";Wr();Xi();br();cs();c$e();Vn();Kt();s(EIt,"buildStyleByKey");AIt=s((e,t,r,n)=>{let i=n.db,a=i.getConfig?.(),{themeVariables:o,look:l,handDrawnSeed:u}=Lt(),h=l==="handDrawn",d=[o.venn1,o.venn2,o.venn3,o.venn4,o.venn5,o.venn6,o.venn7,o.venn8].filter(Boolean),f=i.getDiagramTitle?.(),p=i.getSubsetData(),m=i.getTextData(),g=EIt(i.getStyleData()),y=_It(p),v=a?.width??800,x=a?.height??450,T=v/1600,w=f?48*T:0,C=o.primaryTextColor??o.textColor,S=wn(t);S.attr("viewBox",`0 0 ${v} ${x}`),f&&S.append("text").text(f).attr("class","venn-title").attr("font-size",`${32*T}px`).attr("text-anchor","middle").attr("dominant-baseline","middle").attr("x","50%").attr("y",32*T).style("fill",o.vennTitleTextColor||o.titleColor);let k=ut(document.createElement("div")),R=n$e().width(v).height(x-w);k.datum(y).call(R);let M=h?ot.svg(k.select("svg").node()):void 0,P=l$e(y,{width:v,height:x-w,padding:a?.padding??15}),N=new Map;for(let D of P){let L=M0([...D.data.sets].sort());N.set(L,D)}m.length>0&&RIt(a,N,k,m,T,g);let _=En(o.background||"#f4f4f4");k.selectAll(".venn-circle").each(function(D,L){let B=ut(this),$=M0([...D.sets].sort()),W=g.get($),V=W?.fill||d[L%d.length]||o.primaryColor;B.classed(`venn-set-${L%8}`,!0);let z=W?.["fill-opacity"]??.1,H=W?.stroke||V,K=W?.["stroke-width"]||`${5*T}`;if(h&&M){let ne=N.get($);if(ne&&ne.circles.length>0){let Z=ne.circles[0],pe=M.circle(Z.x,Z.y,Z.radius*2,{roughness:.7,seed:u,fill:Tw(V,.7),fillStyle:"hachure",fillWeight:2,hachureGap:8,hachureAngle:-41+L*60,stroke:H,strokeWidth:parseFloat(String(K))});B.select("path").remove(),B.node()?.insertBefore(pe,B.select("text").node())}}else B.select("path").style("fill",V).style("fill-opacity",z).style("stroke",H).style("stroke-width",K).style("stroke-opacity",.95);let J=W?.color||(_?Qe(V,30):Je(V,30));B.select("text").style("font-size",`${48*T}px`).style("fill",J)}),h&&M?k.selectAll(".venn-intersection").each(function(D){let L=ut(this),O=M0([...D.sets].sort()),$=g.get(O),W=$?.fill;if(W){let V=L.select("path"),z=V.attr("d");if(z){let H=M.path(z,{roughness:.7,seed:u,fill:Tw(W,.3),fillStyle:"cross-hatch",fillWeight:2,hachureGap:6,hachureAngle:60,stroke:"none"}),K=V.node();K?.parentNode?.insertBefore(H,K),V.remove()}}else L.select("path").style("fill-opacity",0);L.select("text").style("font-size",`${48*T}px`).style("fill",$?.color??o.vennSetTextColor??C)}):(k.selectAll(".venn-intersection text").style("font-size",`${48*T}px`).style("fill",D=>{let B=M0([...D.sets].sort());return g.get(B)?.color??o.vennSetTextColor??C}),k.selectAll(".venn-intersection path").style("fill-opacity",D=>{let B=M0([...D.sets].sort());return g.get(B)?.fill?1:0}).style("fill",D=>{let B=M0([...D.sets].sort());return g.get(B)?.fill??"transparent"}));let A=S.append("g").attr("transform",`translate(0, ${w})`),I=k.select("svg").node();if(I&&"childNodes"in I)for(let D of[...I.childNodes])A.node()?.appendChild(D);jr(S,x,v,a?.useMaxWidth??!0)},"draw");s(M0,"stableSetsKey");s(RIt,"renderTextNodes");s(_It,"ensurePairwiseSubsets");u$e={draw:AIt}});var d$e={};sr(d$e,{diagram:()=>LIt});var LIt,f$e=F(()=>{"use strict";GBe();VBe();qBe();h$e();LIt={parser:FBe,db:zBe,renderer:u$e,styles:WBe}});var vx,TX=F(()=>{"use strict";Qi();br();er();Xt();$n();vx=class{constructor(){this.nodes=[];this.levels=new Map;this.outerNodes=[];this.classes=new Map;this.setAccTitle=Lr;this.getAccTitle=Ir;this.setDiagramTitle=Gr;this.getDiagramTitle=Pr;this.getAccDescription=Nr;this.setAccDescription=Mr}static{s(this,"TreeMapDB")}getNodes(){return this.nodes}getConfig(){let t=mr,r=Lt();return Xr({...t.treemap,...r.treemap??{}})}addNode(t,r){this.nodes.push(t),this.levels.set(t,r),r===0&&(this.outerNodes.push(t),this.root??=t)}getRoot(){return{name:"",children:this.outerNodes}}addClass(t,r){let n=this.classes.get(t)??{id:t,styles:[],textStyles:[]},i=r.replace(/\\,/g,"\xA7\xA7\xA7").replace(/,/g,";").replace(/§§§/g,",").split(";");i&&i.forEach(a=>{S2(a)&&(n?.textStyles?n.textStyles.push(a):n.textStyles=[a]),n?.styles?n.styles.push(a):n.styles=[a]}),this.classes.set(t,n)}getClasses(){return this.classes}getStylesForClass(t){return this.classes.get(t)?.styles??[]}clear(){Tr(),this.nodes=[],this.levels=new Map,this.outerNodes=[],this.classes=new Map,this.root=void 0}}});function g$e(e){if(!e.length)return[];let t=[],r=[];return e.forEach(n=>{let i={name:n.name,children:n.type==="Leaf"?void 0:[]};for(i.classSelector=n?.classSelector,n?.cssCompiledStyles&&(i.cssCompiledStyles=n.cssCompiledStyles),n.type==="Leaf"&&n.value!==void 0&&(i.value=n.value);r.length>0&&r[r.length-1].level>=n.level;)r.pop();if(r.length===0)t.push(i);else{let a=r[r.length-1].node;a.children?a.children.push(i):a.children=[i]}n.type!=="Leaf"&&r.push({node:i,level:n.level})}),t}var y$e=F(()=>{"use strict";s(g$e,"buildHierarchy")});var NIt,PIt,CX,v$e=F(()=>{"use strict";ls();St();to();y$e();TX();NIt=s((e,t)=>{Hn(e,t);let r=[];for(let a of e.TreemapRows??[])a.$type==="ClassDefStatement"&&t.addClass(a.className??"",a.styleText??"");for(let a of e.TreemapRows??[]){let o=a.item;if(!o)continue;let l=a.indent?parseInt(a.indent):0,u=PIt(o),h=o.classSelector?t.getStylesForClass(o.classSelector):[],d=h.length>0?h:void 0,f={level:l,name:u,type:o.$type,value:o.value,classSelector:o.classSelector,cssCompiledStyles:d};r.push(f)}let n=g$e(r),i=s((a,o)=>{for(let l of a)t.addNode(l,o),l.children&&l.children.length>0&&i(l.children,o+1)},"addNodesRecursively");i(n,0)},"populate"),PIt=s(e=>e.name?String(e.name):"","getItemName"),CX={parser:{yy:void 0},parse:s(async e=>{try{let r=await Li("treemap",e);se.debug("Treemap AST:",r);let n=CX.parser?.yy;if(!(n instanceof vx))throw new Error("parser.parser?.yy was not a TreemapDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");NIt(r,n)}catch(t){throw se.error("Error parsing treemap:",t),t}},"parse")}});var OIt,xx,sw,BIt,$It,x$e,b$e=F(()=>{"use strict";cs();tp();Vn();Wr();Xt();br();St();OIt=10,xx=10,sw=25,BIt=s((e,t,r,n)=>{let i=n.db,a=i.getConfig(),o=a.padding??OIt,l=i.getDiagramTitle(),u=i.getRoot(),{themeVariables:h}=Lt();if(!u)return;let d=l?30:0,f=wn(t),p=a.nodeWidth?a.nodeWidth*xx:960,m=a.nodeHeight?a.nodeHeight*xx:500,g=p,y=m+d;f.attr("viewBox",`0 0 ${g} ${y}`),jr(f,y,g,a.useMaxWidth);let v;try{let z=a.valueFormat||",";if(z==="$0,0")v=s(H=>"$"+Yc(",")(H),"valueFormat");else if(z.startsWith("$")&&z.includes(",")){let H=/\.\d+/.exec(z),K=H?H[0]:"";v=s(J=>"$"+Yc(","+K)(J),"valueFormat")}else if(z.startsWith("$")){let H=z.substring(1);v=s(K=>"$"+Yc(H||"")(K),"valueFormat")}else v=Yc(z)}catch(z){se.error("Error creating format function:",z),v=Yc(",")}let x=Go().range(["transparent",h.cScale0,h.cScale1,h.cScale2,h.cScale3,h.cScale4,h.cScale5,h.cScale6,h.cScale7,h.cScale8,h.cScale9,h.cScale10,h.cScale11]),b=Go().range(["transparent",h.cScalePeer0,h.cScalePeer1,h.cScalePeer2,h.cScalePeer3,h.cScalePeer4,h.cScalePeer5,h.cScalePeer6,h.cScalePeer7,h.cScalePeer8,h.cScalePeer9,h.cScalePeer10,h.cScalePeer11]),T=Go().range([h.cScaleLabel0,h.cScaleLabel1,h.cScaleLabel2,h.cScaleLabel3,h.cScaleLabel4,h.cScaleLabel5,h.cScaleLabel6,h.cScaleLabel7,h.cScaleLabel8,h.cScaleLabel9,h.cScaleLabel10,h.cScaleLabel11]);l&&f.append("text").attr("x",g/2).attr("y",d/2).attr("class","treemapTitle").attr("text-anchor","middle").attr("dominant-baseline","middle").text(l);let w=f.append("g").attr("transform",`translate(0, ${d})`).attr("class","treemapContainer"),C=yy(u).sum(z=>z.value??0).sort((z,H)=>(H.value??0)-(z.value??0)),k=aE().size([p,m]).paddingTop(z=>z.children&&z.children.length>0?sw+xx:0).paddingInner(o).paddingLeft(z=>z.children&&z.children.length>0?xx:0).paddingRight(z=>z.children&&z.children.length>0?xx:0).paddingBottom(z=>z.children&&z.children.length>0?xx:0).round(!0)(C),R=k.descendants().filter(z=>z.children&&z.children.length>0),M=w.selectAll(".treemapSection").data(R).enter().append("g").attr("class","treemapSection").attr("transform",z=>`translate(${z.x0},${z.y0})`);M.append("rect").attr("width",z=>z.x1-z.x0).attr("height",sw).attr("class","treemapSectionHeader").attr("fill","none").attr("fill-opacity",.6).attr("stroke-width",.6).attr("style",z=>z.depth===0?"display: none;":""),M.append("clipPath").attr("id",(z,H)=>`clip-section-${t}-${H}`).append("rect").attr("width",z=>Math.max(0,z.x1-z.x0-12)).attr("height",sw),M.append("rect").attr("width",z=>z.x1-z.x0).attr("height",z=>z.y1-z.y0).attr("class",(z,H)=>`treemapSection section${H}`).attr("fill",z=>x(z.data.name)).attr("fill-opacity",.6).attr("stroke",z=>b(z.data.name)).attr("stroke-width",2).attr("stroke-opacity",.4).attr("style",z=>{if(z.depth===0)return"display: none;";let H=st({cssCompiledStyles:z.data.cssCompiledStyles});return H.nodeStyles+";"+H.borderStyles.join(";")}),M.append("text").attr("class","treemapSectionLabel").attr("x",6).attr("y",sw/2).attr("dominant-baseline","middle").text(z=>z.depth===0?"":z.data.name).attr("font-weight","bold").attr("clip-path",(z,H)=>`url(#clip-section-${t}-${H})`).attr("style",z=>{if(z.depth===0)return"display: none;";let H="dominant-baseline: middle; font-size: 12px; fill:"+T(z.data.name)+"; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",K=st({cssCompiledStyles:z.data.cssCompiledStyles});return H+K.labelStyles.replace("color:","fill:")}).each(function(z){if(z.depth===0)return;let H=ut(this),K=z.data.name;H.text(K);let J=z.x1-z.x0,ne=6,Z;a.showValues!==!1&&z.value?Z=J-10-30-10-ne:Z=J-ne-6;let te=Math.max(15,Z),fe=H.node();if(fe.getComputedTextLength()>te){let Ee=K;for(;Ee.length>0;){if(Ee=K.substring(0,Ee.length-1),Ee.length===0){H.text("..."),fe.getComputedTextLength()>te&&H.text("");break}if(H.text(Ee+"..."),fe.getComputedTextLength()<=te)break}}}),a.showValues!==!1&&M.append("text").attr("class","treemapSectionValue").attr("x",z=>z.x1-z.x0-10).attr("y",sw/2).attr("text-anchor","end").attr("dominant-baseline","middle").text(z=>z.value?v(z.value):"").attr("font-style","italic").attr("style",z=>{if(z.depth===0)return"display: none;";let H="text-anchor: end; dominant-baseline: middle; font-size: 10px; fill:"+T(z.data.name)+"; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",K=st({cssCompiledStyles:z.data.cssCompiledStyles});return H+K.labelStyles.replace("color:","fill:")});let P=k.leaves(),N=P.length>20,_=N?16:38,A=N?14:28,I=N?4:8,D=N?4:6,L=N?2:4,B=N?8:10,O=N?1:2,$=w.selectAll(".treemapLeafGroup").data(P).enter().append("g").attr("class",(z,H)=>`treemapNode treemapLeafGroup leaf${H}${z.data.classSelector?` ${z.data.classSelector}`:""}x`).attr("transform",z=>`translate(${z.x0},${z.y0})`);$.append("rect").attr("width",z=>z.x1-z.x0).attr("height",z=>z.y1-z.y0).attr("class","treemapLeaf").attr("fill",z=>z.parent?x(z.parent.data.name):x(z.data.name)).attr("style",z=>st({cssCompiledStyles:z.data.cssCompiledStyles}).nodeStyles).attr("fill-opacity",.3).attr("stroke",z=>z.parent?x(z.parent.data.name):x(z.data.name)).attr("stroke-width",3),$.append("clipPath").attr("id",(z,H)=>`clip-${t}-${H}`).append("rect").attr("width",z=>Math.max(0,z.x1-z.x0-4)).attr("height",z=>Math.max(0,z.y1-z.y0-4)),$.append("text").attr("class","treemapLabel").attr("x",z=>(z.x1-z.x0)/2).attr("y",z=>(z.y1-z.y0)/2).attr("style",z=>{let H=`text-anchor: middle; dominant-baseline: middle; font-size: ${_}px;fill:`+T(z.data.name)+";",K=st({cssCompiledStyles:z.data.cssCompiledStyles});return H+K.labelStyles.replace("color:","fill:")}).attr("clip-path",(z,H)=>`url(#clip-${t}-${H})`).text(z=>z.data.name).each(function(z){let H=ut(this),K=z.x1-z.x0,J=z.y1-z.y0,ne=H.node(),Z=K-2*L,pe=J-2*L;if(ZZ&&te>I;)te--,H.style("font-size",`${te}px`);let ce=Math.max(D,Math.min(A,Math.round(te*fe))),de=te+O+ce;for(;de>pe&&te>I&&(te--,ce=Math.max(D,Math.min(A,Math.round(te*fe))),!(cepe;H.style("font-size",`${te}px`),N?(teZ||te(H.x1-H.x0)/2).attr("y",function(H){return(H.y1-H.y0)/2}).attr("style",H=>{let K=`text-anchor: middle; dominant-baseline: hanging; font-size: ${A}px;fill:`+T(H.data.name)+";",J=st({cssCompiledStyles:H.data.cssCompiledStyles});return K+J.labelStyles.replace("color:","fill:")}).attr("clip-path",(H,K)=>`url(#clip-${t}-${K})`).text(H=>H.value?v(H.value):"").each(function(H){let K=ut(this),J=this.parentNode;if(!J){K.style("display","none");return}let ne=ut(J).select(".treemapLabel");if(ne.empty()||ne.style("display")==="none"){K.style("display","none");return}let Z=parseFloat(ne.style("font-size")),te=Math.max(D,Math.min(A,Math.round(Z*.6)));K.style("font-size",`${te}px`);let ce=(H.y1-H.y0)/2+Z/2+O;K.attr("y",ce);let de=H.x1-H.x0,Fe=H.y1-H.y0-4,Ye=de-2*L;K.node().getComputedTextLength()>Ye||ce+te>Fe||te{"use strict";er();Mc();br();FIt={sectionStrokeColor:"black",sectionStrokeWidth:"1",sectionFillColor:"#efefef",leafStrokeColor:"black",leafStrokeWidth:"1",leafFillColor:"#efefef",labelFontSize:"12px",valueFontSize:"10px",titleFontSize:"14px"},GIt=s(({treemap:e}={})=>{let t=ka(),r=Lt(),n=Xr(t,r.themeVariables),i=Xr(FIt,e),a=i.titleColor??n.titleColor,o=i.labelColor??n.textColor,l=i.valueColor??n.textColor;return` + .treemapNode.section { + stroke: ${i.sectionStrokeColor}; + stroke-width: ${i.sectionStrokeWidth}; + fill: ${i.sectionFillColor}; + } + .treemapNode.leaf { + stroke: ${i.leafStrokeColor}; + stroke-width: ${i.leafStrokeWidth}; + fill: ${i.leafFillColor}; + } + .treemapLabel { + fill: ${o}; + font-size: ${i.labelFontSize}; + } + .treemapValue { + fill: ${l}; + font-size: ${i.valueFontSize}; + } + .treemapTitle { + fill: ${a}; + font-size: ${i.titleFontSize}; + } + `},"getStyles"),T$e=GIt});var S$e={};sr(S$e,{diagram:()=>zIt});var zIt,w$e=F(()=>{"use strict";TX();v$e();b$e();C$e();zIt={parser:CX,get db(){return new vx},renderer:x$e,styles:T$e}});var HL,N0,A$e,qIt,HIt,SX,R$e=F(()=>{"use strict";ls();St();to();HL=s((e,t)=>{let r=e<=1?e*100:e;if(r<0||r>100)throw new Error(`${t} must be between 0-1 (decimal) or 0-100 (percentage). Received: ${e}`);return r},"toPercent"),N0=s((e,t,r)=>({x:HL(t,`${r} evolution`),y:HL(e,`${r} visibility`)}),"toCoordinates"),A$e=s(e=>{if(e){if(e==="+<>")return"bidirectional";if(e==="+<")return"backward";if(e==="+>")return"forward"}},"getFlowFromPort"),qIt=s(e=>{if(!e?.startsWith("+"))return{};let r=/^\+'([^']*)'/.exec(e)?.[1];return e.includes("<>")?{flow:"bidirectional",label:r}:e.includes("<")?{flow:"backward",label:r}:e.includes(">")?{flow:"forward",label:r}:{label:r}},"extractFlowFromArrow"),HIt=s((e,t)=>{if(Hn(e,t),e.size&&t.setSize(e.size.width,e.size.height),e.evolution){let r=e.evolution.stages.map(i=>i.secondName?`${i.name.trim()} / ${i.secondName.trim()}`:i.name.trim()),n=e.evolution.stages.filter(i=>i.boundary!==void 0).map(i=>i.boundary);t.updateAxes({stages:r,stageBoundaries:n})}if(e.anchors.forEach(r=>{let n=N0(r.visibility,r.evolution,`Anchor "${r.name}"`);t.addNode(r.name,r.name,n.x,n.y,"anchor")}),e.components.forEach(r=>{let n=N0(r.visibility,r.evolution,`Component "${r.name}"`),i=r.label?(r.label.negX?-1:1)*r.label.offsetX:void 0,a=r.label?(r.label.negY?-1:1)*r.label.offsetY:void 0,o=r.decorator?.strategy;t.addNode(r.name,r.name,n.x,n.y,"component",i,a,r.inertia,o)}),e.notes.forEach(r=>{let n=N0(r.visibility,r.evolution,`Note "${r.text}"`);t.addNote(r.text,n.x,n.y)}),e.pipelines.forEach(r=>{let n=t.getNode(r.parent);if(!n||typeof n.y!="number")throw new Error(`Pipeline "${r.parent}" must reference an existing component with coordinates.`);let i=n.y;t.startPipeline(r.parent),r.components.forEach(a=>{let o=`${r.parent}_${a.name}`,l=a.label?(a.label.negX?-1:1)*a.label.offsetX:void 0,u=a.label?(a.label.negY?-1:1)*a.label.offsetY:void 0,h=HL(a.evolution,`Pipeline component "${a.name}" evolution`);t.addNode(o,a.name,h,i,"pipeline-component",l,u),t.addPipelineComponent(r.parent,o)})}),e.links.forEach(r=>{let n=!!r.arrow&&(r.arrow.includes("-.->")||r.arrow.includes(".-.")),i=A$e(r.fromPort)??A$e(r.toPort),{flow:a,label:o}=qIt(r.arrow);!i&&a&&(i=a);let l=r.linkLabel,u=o??l;t.addLink(t.resolveNodeId(r.from),t.resolveNodeId(r.to),n,u,i)}),e.evolves.forEach(r=>{let n=t.getNode(r.component);if(n?.y!==void 0){let i=HL(r.target,`Evolve target for "${r.component}"`);t.addTrend(r.component,i,n.y)}}),e.annotations.length>0){let r=e.annotations[0],n=N0(r.x,r.y,"Annotations box");t.setAnnotationsBox(n.x,n.y)}e.annotation.forEach(r=>{let n=N0(r.x,r.y,`Annotation ${r.number}`);t.addAnnotation(r.number,[{x:n.x,y:n.y}],r.text)}),e.accelerators.forEach(r=>{let n=N0(r.x,r.y,`Accelerator "${r.name}"`);t.addAccelerator(r.name,n.x,n.y)}),e.deaccelerators.forEach(r=>{let n=N0(r.x,r.y,`Deaccelerator "${r.name}"`);t.addDeaccelerator(r.name,n.x,n.y)})},"populateDb"),SX={parser:{yy:void 0},parse:s(async e=>{let t=await Li("wardley",e);se.debug(t);let r=SX.parser?.yy;if(!r||typeof r.addNode!="function")throw new Error("parser.parser?.yy was not a WardleyDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");HIt(t,r)},"parse")}});var UL,_$e=F(()=>{"use strict";UL=class{constructor(){this.nodes=new Map;this.links=[];this.trends=new Map;this.pipelines=new Map;this.annotations=[];this.notes=[];this.accelerators=[];this.deaccelerators=[];this.axes={}}static{s(this,"WardleyBuilder")}addNode(t){let r=this.nodes.get(t.id)??{id:t.id,label:t.label},n={...r,...t,className:t.className??r.className,labelOffsetX:t.labelOffsetX??r.labelOffsetX,labelOffsetY:t.labelOffsetY??r.labelOffsetY};this.nodes.set(t.id,n)}addLink(t){this.links.push(t)}addTrend(t){this.trends.set(t.nodeId,t)}startPipeline(t){this.pipelines.set(t,{nodeId:t,componentIds:[]});let r=this.nodes.get(t);r&&(r.isPipelineParent=!0)}addPipelineComponent(t,r){let n=this.pipelines.get(t);n&&n.componentIds.push(r);let i=this.nodes.get(r);i&&(i.inPipeline=!0)}addAnnotation(t){this.annotations.push(t)}addNote(t){this.notes.push(t)}addAccelerator(t){this.accelerators.push(t)}addDeaccelerator(t){this.deaccelerators.push(t)}setAnnotationsBox(t,r){this.annotationsBox={x:t,y:r}}setAxes(t){this.axes={...this.axes,...t}}setSize(t,r){this.size={width:t,height:r}}getNode(t){return this.nodes.get(t)}resolveNodeId(t){if(this.nodes.has(t))return t;for(let[r,n]of this.nodes)if(n.label===t)return r;return t}build(){let t=[];for(let r of this.nodes.values()){if(typeof r.x!="number"||typeof r.y!="number")throw new Error(`Node "${r.label}" is missing coordinates`);t.push(r)}return{nodes:t,links:[...this.links],trends:[...this.trends.values()],pipelines:[...this.pipelines.values()],annotations:[...this.annotations],notes:[...this.notes],accelerators:[...this.accelerators],deaccelerators:[...this.deaccelerators],annotationsBox:this.annotationsBox,axes:{...this.axes},size:this.size}}clear(){this.nodes.clear(),this.links=[],this.trends.clear(),this.pipelines.clear(),this.annotations=[],this.notes=[],this.accelerators=[],this.deaccelerators=[],this.annotationsBox=void 0,this.axes={},this.size=void 0}}});function UIt(){return Pe()["wardley-beta"]}function YIt(e,t,r,n,i,a,o,l,u){Es.addNode({id:e,label:t,x:r,y:n,className:i,labelOffsetX:a,labelOffsetY:o,inertia:l,sourceStrategy:u})}function jIt(e,t,r=!1,n,i){Es.addLink({source:e,target:t,dashed:r,label:n,flow:i})}function XIt(e,t,r){Es.addTrend({nodeId:e,targetX:t,targetY:r})}function KIt(e,t,r){Es.addAnnotation({number:e,coordinates:t,text:r})}function ZIt(e,t,r){Es.addNote({text:e,x:t,y:r})}function QIt(e,t,r){Es.addAccelerator({name:e,x:t,y:r})}function JIt(e,t,r){Es.addDeaccelerator({name:e,x:t,y:r})}function e8t(e,t){Es.setAnnotationsBox(e,t)}function t8t(e,t){Es.setSize(e,t)}function r8t(e){Es.startPipeline(e)}function n8t(e,t){Es.addPipelineComponent(e,t)}function i8t(e){Es.setAxes(e)}function a8t(e){return Es.getNode(e)}function s8t(e){return Es.resolveNodeId(e)}function o8t(){return Es.build()}function l8t(){Es.clear(),Tr()}var Es,L$e,D$e=F(()=>{"use strict";Jt();$n();_$e();Es=new UL;s(UIt,"getConfig");s(YIt,"addNode");s(jIt,"addLink");s(XIt,"addTrend");s(KIt,"addAnnotation");s(ZIt,"addNote");s(QIt,"addAccelerator");s(JIt,"addDeaccelerator");s(e8t,"setAnnotationsBox");s(t8t,"setSize");s(r8t,"startPipeline");s(n8t,"addPipelineComponent");s(i8t,"updateAxes");s(a8t,"getNode");s(s8t,"resolveNodeId");s(o8t,"getWardleyData");s(l8t,"clear");L$e={getConfig:UIt,addNode:YIt,addLink:jIt,addTrend:XIt,addAnnotation:KIt,addNote:ZIt,addAccelerator:QIt,addDeaccelerator:JIt,setAnnotationsBox:e8t,setSize:t8t,startPipeline:r8t,addPipelineComponent:n8t,updateAxes:i8t,getNode:a8t,resolveNodeId:s8t,getWardleyData:o8t,clear:l8t,setAccTitle:Lr,getAccTitle:Ir,setDiagramTitle:Gr,getDiagramTitle:Pr,getAccDescription:Nr,setAccDescription:Mr}});var c8t,u8t,h8t,d8t,I$e,M$e=F(()=>{"use strict";Jt();St();cs();Vn();c8t=["Genesis","Custom Built","Product","Commodity"],u8t=s(()=>{let{themeVariables:e}=Pe();return{backgroundColor:e.wardley?.backgroundColor??e.background??"#fff",axisColor:e.wardley?.axisColor??"#000",axisTextColor:e.wardley?.axisTextColor??e.primaryTextColor??"#222",gridColor:e.wardley?.gridColor??"rgba(100, 100, 100, 0.2)",componentFill:e.wardley?.componentFill??"#fff",componentStroke:e.wardley?.componentStroke??"#000",componentLabelColor:e.wardley?.componentLabelColor??e.primaryTextColor??"#222",linkStroke:e.wardley?.linkStroke??"#000",evolutionStroke:e.wardley?.evolutionStroke??"#dc3545",annotationStroke:e.wardley?.annotationStroke??"#000",annotationTextColor:e.wardley?.annotationTextColor??e.primaryTextColor??"#222",annotationFill:e.wardley?.annotationFill??e.background??"#fff"}},"getTheme"),h8t=s(()=>{let e=Pe()["wardley-beta"];return{width:e?.width??900,height:e?.height??600,padding:e?.padding??48,nodeRadius:e?.nodeRadius??6,nodeLabelOffset:e?.nodeLabelOffset??8,axisFontSize:e?.axisFontSize??12,labelFontSize:e?.labelFontSize??10,showGrid:e?.showGrid??!1,useMaxWidth:e?.useMaxWidth??!0}},"getConfigValues"),d8t=s((e,t,r,n)=>{se.debug(`Rendering Wardley map +`+e);let i=h8t(),a=u8t(),o=i.nodeRadius*1.6,l=n.db,u=l.getWardleyData(),h=l.getDiagramTitle(),d=u.size?.width??i.width,f=u.size?.height??i.height,p=wn(t);p.selectAll("*").remove(),jr(p,f,d,i.useMaxWidth),p.attr("viewBox",`0 0 ${d} ${f}`);let m=p.append("g").attr("class","wardley-map"),g=p.append("defs");g.append("marker").attr("id",`arrow-${t}`).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerWidth",6).attr("markerHeight",6).attr("orient","auto-start-reverse").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").attr("fill",a.evolutionStroke).attr("stroke","none"),g.append("marker").attr("id",`link-arrow-end-${t}`).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerWidth",5).attr("markerHeight",5).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").attr("fill",a.linkStroke).attr("stroke","none"),g.append("marker").attr("id",`link-arrow-start-${t}`).attr("viewBox","0 0 10 10").attr("refX",1).attr("refY",5).attr("markerWidth",5).attr("markerHeight",5).attr("orient","auto").append("path").attr("d","M 10 0 L 0 5 L 10 10 z").attr("fill",a.linkStroke).attr("stroke","none"),m.append("rect").attr("class","wardley-background").attr("width",d).attr("height",f).attr("fill",a.backgroundColor);let y=d-i.padding*2,v=f-i.padding*2;h&&m.append("text").attr("class","wardley-title").attr("x",d/2).attr("y",i.padding/2).attr("fill",a.axisTextColor).attr("font-size",i.axisFontSize*1.05).attr("font-weight","bold").attr("text-anchor","middle").attr("dominant-baseline","middle").text(h);let x=s(O=>i.padding+O/100*y,"projectX"),b=s(O=>f-i.padding-O/100*v,"projectY"),T=m.append("g").attr("class","wardley-axes");T.append("line").attr("x1",i.padding).attr("x2",d-i.padding).attr("y1",f-i.padding).attr("y2",f-i.padding).attr("stroke",a.axisColor).attr("stroke-width",1),T.append("line").attr("x1",i.padding).attr("x2",i.padding).attr("y1",i.padding).attr("y2",f-i.padding).attr("stroke",a.axisColor).attr("stroke-width",1);let w=u.axes.xLabel??"Evolution",C=u.axes.yLabel??"Visibility";T.append("text").attr("class","wardley-axis-label wardley-axis-label-x").attr("x",i.padding+y/2).attr("y",f-i.padding/4).attr("fill",a.axisTextColor).attr("font-size",i.axisFontSize).attr("font-weight","bold").attr("text-anchor","middle").text(w),T.append("text").attr("class","wardley-axis-label wardley-axis-label-y").attr("x",i.padding/3).attr("y",i.padding+v/2).attr("fill",a.axisTextColor).attr("font-size",i.axisFontSize).attr("font-weight","bold").attr("text-anchor","middle").attr("transform",`rotate(-90 ${i.padding/3} ${i.padding+v/2})`).text(C);let S=u.axes.stages&&u.axes.stages.length>0?u.axes.stages:c8t;if(S.length>0){let O=m.append("g").attr("class","wardley-stages"),$=u.axes.stageBoundaries,W=[];if($&&$.length===S.length){let V=0;$.forEach(z=>{W.push({start:V,end:z}),V=z})}else{let V=1/S.length;S.forEach((z,H)=>{W.push({start:H*V,end:(H+1)*V})})}S.forEach((V,z)=>{let H=W[z],K=i.padding+H.start*y,J=i.padding+H.end*y,ne=(K+J)/2;z>0&&O.append("line").attr("x1",K).attr("x2",K).attr("y1",i.padding).attr("y2",f-i.padding).attr("stroke","#000").attr("stroke-width",1).attr("stroke-dasharray","5 5").attr("opacity",.8),O.append("text").attr("class","wardley-stage-label").attr("x",ne).attr("y",f-i.padding/1.5).attr("fill",a.axisTextColor).attr("font-size",i.axisFontSize-2).attr("text-anchor","middle").text(V)})}if(i.showGrid){let O=m.append("g").attr("class","wardley-grid");for(let $=1;$<4;$++){let W=$/4,V=i.padding+y*W;O.append("line").attr("x1",V).attr("x2",V).attr("y1",i.padding).attr("y2",f-i.padding).attr("stroke",a.gridColor).attr("stroke-dasharray","2 6"),O.append("line").attr("x1",i.padding).attr("x2",d-i.padding).attr("y1",f-i.padding-v*W).attr("y2",f-i.padding-v*W).attr("stroke",a.gridColor).attr("stroke-dasharray","2 6")}}let k=new Map;if(u.nodes.forEach(O=>{k.set(O.id,{x:x(O.x),y:b(O.y),node:O})}),u.pipelines.length>0){let O=m.append("g").attr("class","wardley-pipelines"),$=m.append("g").attr("class","wardley-pipeline-links");u.pipelines.forEach(W=>{if(W.componentIds.length===0)return;let V=W.componentIds.map(J=>({id:J,pos:k.get(J),node:u.nodes.find(ne=>ne.id===J)})).filter(J=>J.pos&&J.node).sort((J,ne)=>J.node.x-ne.node.x);for(let J=0;J{let ne=k.get(J);ne&&(z=Math.min(z,ne.x),H=Math.max(H,ne.x),K=ne.y)}),z!==1/0&&H!==-1/0){let ne=i.nodeRadius*4,Z=K-ne/2,pe=k.get(W.nodeId);if(pe){let te=(z+H)/2;pe.x=te,pe.y=Z-o/6}O.append("rect").attr("class","wardley-pipeline-box").attr("x",z-15).attr("y",Z).attr("width",H-z+30).attr("height",ne).attr("fill","none").attr("stroke",a.axisColor).attr("stroke-width",1.5).attr("rx",4).attr("ry",4)}})}let R=m.append("g").attr("class","wardley-links"),M=new Map;u.pipelines.forEach(O=>{M.set(O.nodeId,new Set(O.componentIds))});let P=u.links.filter(O=>!(!k.has(O.source)||!k.has(O.target)||M.get(O.target)?.has(O.source)));R.selectAll("line").data(P).enter().append("line").attr("class",O=>`wardley-link${O.dashed?" wardley-link--dashed":""}`).attr("x1",O=>{let $=k.get(O.source),W=k.get(O.target),z=u.nodes.find(ne=>ne.id===O.source).isPipelineParent?o/Math.sqrt(2):i.nodeRadius,H=W.x-$.x,K=W.y-$.y,J=Math.sqrt(H*H+K*K);return $.x+H/J*z}).attr("y1",O=>{let $=k.get(O.source),W=k.get(O.target),z=u.nodes.find(ne=>ne.id===O.source).isPipelineParent?o/Math.sqrt(2):i.nodeRadius,H=W.x-$.x,K=W.y-$.y,J=Math.sqrt(H*H+K*K);return $.y+K/J*z}).attr("x2",O=>{let $=k.get(O.source),W=k.get(O.target),z=u.nodes.find(ne=>ne.id===O.target).isPipelineParent?o/Math.sqrt(2):i.nodeRadius,H=$.x-W.x,K=$.y-W.y,J=Math.sqrt(H*H+K*K);return W.x+H/J*z}).attr("y2",O=>{let $=k.get(O.source),W=k.get(O.target),z=u.nodes.find(ne=>ne.id===O.target).isPipelineParent?o/Math.sqrt(2):i.nodeRadius,H=$.x-W.x,K=$.y-W.y,J=Math.sqrt(H*H+K*K);return W.y+K/J*z}).attr("stroke",a.linkStroke).attr("stroke-width",1).attr("stroke-dasharray",O=>O.dashed?"6 6":null).attr("marker-end",O=>O.flow==="forward"||O.flow==="bidirectional"?`url(#link-arrow-end-${t})`:null).attr("marker-start",O=>O.flow==="backward"||O.flow==="bidirectional"?`url(#link-arrow-start-${t})`:null),R.selectAll("text").data(P.filter(O=>O.label)).enter().append("text").attr("class","wardley-link-label").attr("x",O=>{let $=k.get(O.source),W=k.get(O.target),V=($.x+W.x)/2,z=W.y-$.y,H=W.x-$.x,K=Math.sqrt(H*H+z*z),J=8,ne=z/K;return V+ne*J}).attr("y",O=>{let $=k.get(O.source),W=k.get(O.target),V=($.y+W.y)/2,z=W.x-$.x,H=W.y-$.y,K=Math.sqrt(z*z+H*H),J=8,ne=-z/K;return V+ne*J}).attr("fill",a.axisTextColor).attr("font-size",i.labelFontSize).attr("text-anchor","middle").attr("dominant-baseline","middle").attr("transform",O=>{let $=k.get(O.source),W=k.get(O.target),V=($.x+W.x)/2,z=($.y+W.y)/2,H=W.x-$.x,K=W.y-$.y,J=Math.sqrt(H*H+K*K),ne=8,Z=K/J,pe=-H/J,te=V+Z*ne,fe=z+pe*ne,ce=Math.atan2(K,H)*180/Math.PI;return(ce>90||ce<-90)&&(ce+=180),`rotate(${ce} ${te} ${fe})`}).text(O=>O.label);let N=m.append("g").attr("class","wardley-trends"),_=u.trends.map(O=>{let $=k.get(O.nodeId);if(!$)return null;let W=x(O.targetX),V=b(O.targetY),z=W-$.x,H=V-$.y,K=Math.sqrt(z*z+H*H),J=i.nodeRadius+2,ne=K>J?W-z/K*J:W,Z=K>J?V-H/K*J:V;return{origin:$,targetX:W,targetY:V,adjustedX2:ne,adjustedY2:Z}}).filter(O=>O!==null);N.selectAll("line").data(_).enter().append("line").attr("class","wardley-trend").attr("x1",O=>O.origin.x).attr("y1",O=>O.origin.y).attr("x2",O=>O.adjustedX2).attr("y2",O=>O.adjustedY2).attr("stroke",a.evolutionStroke).attr("stroke-width",1).attr("stroke-dasharray","4 4").attr("marker-end",`url(#arrow-${t})`);let I=m.append("g").attr("class","wardley-nodes").selectAll("g").data(u.nodes).enter().append("g").attr("class",O=>["wardley-node",O.className?`wardley-node--${O.className}`:""].filter(Boolean).join(" "));I.filter(O=>O.sourceStrategy==="outsource").append("circle").attr("class","wardley-outsource-overlay").attr("cx",O=>k.get(O.id).x).attr("cy",O=>k.get(O.id).y).attr("r",i.nodeRadius*2).attr("fill","#666").attr("stroke",a.componentStroke).attr("stroke-width",1),I.filter(O=>O.sourceStrategy==="buy").append("circle").attr("class","wardley-buy-overlay").attr("cx",O=>k.get(O.id).x).attr("cy",O=>k.get(O.id).y).attr("r",i.nodeRadius*2).attr("fill","#ccc").attr("stroke",a.componentStroke).attr("stroke-width",1),I.filter(O=>O.sourceStrategy==="build").append("circle").attr("class","wardley-build-overlay").attr("cx",O=>k.get(O.id).x).attr("cy",O=>k.get(O.id).y).attr("r",i.nodeRadius*2).attr("fill","#eee").attr("stroke","#000").attr("stroke-width",1);let D=I.filter(O=>O.sourceStrategy==="market");D.append("circle").attr("class","wardley-market-overlay").attr("cx",O=>k.get(O.id).x).attr("cy",O=>k.get(O.id).y).attr("r",i.nodeRadius*2).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",1),I.filter(O=>!O.isPipelineParent&&O.sourceStrategy!=="market"&&O.className!=="anchor").append("circle").attr("cx",O=>k.get(O.id).x).attr("cy",O=>k.get(O.id).y).attr("r",i.nodeRadius).attr("fill",a.componentFill).attr("stroke",a.componentStroke).attr("stroke-width",1);let L=i.nodeRadius*.7,B=i.nodeRadius*1.2;if(D.append("line").attr("class","wardley-market-line").attr("x1",O=>k.get(O.id).x).attr("y1",O=>k.get(O.id).y-B).attr("x2",O=>k.get(O.id).x-B*Math.cos(Math.PI/6)).attr("y2",O=>k.get(O.id).y+B*Math.sin(Math.PI/6)).attr("stroke",a.componentStroke).attr("stroke-width",1),D.append("line").attr("class","wardley-market-line").attr("x1",O=>k.get(O.id).x-B*Math.cos(Math.PI/6)).attr("y1",O=>k.get(O.id).y+B*Math.sin(Math.PI/6)).attr("x2",O=>k.get(O.id).x+B*Math.cos(Math.PI/6)).attr("y2",O=>k.get(O.id).y+B*Math.sin(Math.PI/6)).attr("stroke",a.componentStroke).attr("stroke-width",1),D.append("line").attr("class","wardley-market-line").attr("x1",O=>k.get(O.id).x+B*Math.cos(Math.PI/6)).attr("y1",O=>k.get(O.id).y+B*Math.sin(Math.PI/6)).attr("x2",O=>k.get(O.id).x).attr("y2",O=>k.get(O.id).y-B).attr("stroke",a.componentStroke).attr("stroke-width",1),D.append("circle").attr("class","wardley-market-dot").attr("cx",O=>k.get(O.id).x).attr("cy",O=>k.get(O.id).y-B).attr("r",L).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",2),D.append("circle").attr("class","wardley-market-dot").attr("cx",O=>k.get(O.id).x-B*Math.cos(Math.PI/6)).attr("cy",O=>k.get(O.id).y+B*Math.sin(Math.PI/6)).attr("r",L).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",2),D.append("circle").attr("class","wardley-market-dot").attr("cx",O=>k.get(O.id).x+B*Math.cos(Math.PI/6)).attr("cy",O=>k.get(O.id).y+B*Math.sin(Math.PI/6)).attr("r",L).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",2),I.filter(O=>O.isPipelineParent===!0).append("rect").attr("x",O=>k.get(O.id).x-o/2).attr("y",O=>k.get(O.id).y-o/2).attr("width",o).attr("height",o).attr("fill",a.componentFill).attr("stroke",a.componentStroke).attr("stroke-width",1),I.filter(O=>O.inertia===!0).append("line").attr("class","wardley-inertia").attr("x1",O=>{let $=k.get(O.id),W=O.isPipelineParent?o/2+15:i.nodeRadius+15;return O.sourceStrategy&&(W+=i.nodeRadius+10),$.x+W}).attr("y1",O=>{let $=k.get(O.id),W=O.isPipelineParent?o:i.nodeRadius*2;return $.y-W/2}).attr("x2",O=>{let $=k.get(O.id),W=O.isPipelineParent?o/2+15:i.nodeRadius+15;return O.sourceStrategy&&(W+=i.nodeRadius+10),$.x+W}).attr("y2",O=>{let $=k.get(O.id),W=O.isPipelineParent?o:i.nodeRadius*2;return $.y+W/2}).attr("stroke",a.componentStroke).attr("stroke-width",6),I.append("text").attr("x",O=>{let $=k.get(O.id);if(O.className==="anchor")return O.labelOffsetX!==void 0?$.x+O.labelOffsetX:$.x;let W=i.nodeLabelOffset;O.sourceStrategy&&O.labelOffsetX===void 0&&(W+=10);let V=O.labelOffsetX??W;return $.x+V}).attr("y",O=>{let $=k.get(O.id);if(O.className==="anchor")return O.labelOffsetY!==void 0?$.y+O.labelOffsetY:$.y-3;let W=-i.nodeLabelOffset;O.sourceStrategy&&O.labelOffsetY===void 0&&(W-=10);let V=O.labelOffsetY??W;return $.y+V}).attr("class","wardley-node-label").attr("fill",O=>O.className==="evolved"?a.evolutionStroke:O.className==="anchor"?"#000":a.componentLabelColor).attr("font-size",i.labelFontSize).attr("font-weight",O=>O.className==="anchor"?"bold":"normal").attr("text-anchor",O=>O.className==="anchor"?"middle":"start").attr("dominant-baseline",O=>O.className==="anchor"?"middle":"auto").text(O=>O.label),u.annotations.length>0){let O=m.append("g").attr("class","wardley-annotations");if(u.annotations.forEach($=>{let W=$.coordinates.map(V=>({x:x(V.x),y:b(V.y)}));if(W.length>1)for(let V=0;V{let z=O.append("g").attr("class","wardley-annotation");z.append("circle").attr("cx",V.x).attr("cy",V.y).attr("r",10).attr("fill","white").attr("stroke",a.axisColor).attr("stroke-width",1.5),z.append("text").attr("x",V.x).attr("y",V.y).attr("text-anchor","middle").attr("dominant-baseline","central").attr("font-size",10).attr("fill",a.axisTextColor).attr("font-weight","bold").text($.number)})}),u.annotationsBox){let $=x(u.annotationsBox.x),W=b(u.annotationsBox.y),V=10,z=16,H=11,K=O.append("g").attr("class","wardley-annotations-box"),J=[...u.annotations].filter(Z=>Z.text).sort((Z,pe)=>Z.number-pe.number),ne=[];if(J.forEach((Z,pe)=>{let te=K.append("text").attr("x",$+V).attr("y",W+V+(pe+1)*z).attr("font-size",H).attr("fill",a.axisTextColor).attr("text-anchor","start").attr("dominant-baseline","middle").text(`${Z.number}. ${Z.text}`);ne.push(te)}),ne.length>0){let Z=0,pe=0;ne.forEach(Fe=>{let Ye=Fe.node(),He=Ye.getComputedTextLength();Z=Math.max(Z,He);let ye=Ye.getBBox();pe=Math.max(pe,ye.height)});let te=Z+V*2+105,fe=J.length*z+V*2+pe/2,ce=i.padding,de=d-i.padding-te,Ee=i.padding,we=f-i.padding-fe;$=Math.max(ce,Math.min($,de)),W=Math.max(Ee,Math.min(W,we)),ne.forEach((Fe,Ye)=>{Fe.attr("x",$+V).attr("y",W+V+(Ye+1)*z)}),K.insert("rect","text").attr("x",$).attr("y",W).attr("width",te).attr("height",fe).attr("fill","white").attr("stroke",a.axisColor).attr("stroke-width",1.5).attr("rx",4).attr("ry",4)}}}if(u.notes.length>0){let O=m.append("g").attr("class","wardley-notes");u.notes.forEach($=>{let W=x($.x),V=b($.y);O.append("text").attr("x",W).attr("y",V).attr("text-anchor","start").attr("font-size",11).attr("fill",a.axisTextColor).attr("font-weight","bold").text($.text)})}if(u.accelerators.length>0){let O=m.append("g").attr("class","wardley-accelerators");u.accelerators.forEach($=>{let W=x($.x),V=b($.y),z=60,H=30,K=20,J=` + M ${W} ${V-H/2} + L ${W+z-K} ${V-H/2} + L ${W+z-K} ${V-H/2-8} + L ${W+z} ${V} + L ${W+z-K} ${V+H/2+8} + L ${W+z-K} ${V+H/2} + L ${W} ${V+H/2} + Z + `;O.append("path").attr("d",J).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",1),O.append("text").attr("x",W+z/2).attr("y",V+H/2+15).attr("text-anchor","middle").attr("font-size",10).attr("fill",a.axisTextColor).attr("font-weight","bold").text($.name)})}if(u.deaccelerators.length>0){let O=m.append("g").attr("class","wardley-deaccelerators");u.deaccelerators.forEach($=>{let W=x($.x),V=b($.y),z=60,H=30,K=20,J=` + M ${W+z} ${V-H/2} + L ${W+K} ${V-H/2} + L ${W+K} ${V-H/2-8} + L ${W} ${V} + L ${W+K} ${V+H/2+8} + L ${W+K} ${V+H/2} + L ${W+z} ${V+H/2} + Z + `;O.append("path").attr("d",J).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",1),O.append("text").attr("x",W+z/2).attr("y",V+H/2+15).attr("text-anchor","middle").attr("font-size",10).attr("fill",a.axisTextColor).attr("font-weight","bold").text($.name)})}},"draw"),I$e={draw:d8t}});var N$e,P$e=F(()=>{"use strict";er();Mc();br();N$e=s(({wardley:e}={})=>{let t=ka(),r=Lt(),n=Xr(t,r.themeVariables),i=Xr(n.wardley,e);return` + .wardley-background { + fill: ${i.backgroundColor}; + } + .wardley-axes line, .wardley-axes path { + stroke: ${i.axisColor}; + } + .wardley-axis-label { + fill: ${i.axisTextColor}; + } + .wardley-stage-label { + fill: ${i.axisTextColor}; + } + .wardley-grid line { + stroke: ${i.gridColor}; + } + .wardley-node circle { + fill: ${i.componentFill}; + stroke: ${i.componentStroke}; + } + .wardley-node-label { + fill: ${i.componentLabelColor}; + } + .wardley-link { + stroke: ${i.linkStroke}; + } + .wardley-link--dashed { + stroke-dasharray: 4 4; + } + .wardley-link-label { + fill: ${i.axisTextColor}; + } + .wardley-trend line { + stroke: ${i.evolutionStroke}; + } + .wardley-annotation-line { + stroke: ${i.annotationStroke}; + } + .wardley-annotation circle { + fill: ${i.annotationFill}; + stroke: ${i.annotationStroke}; + } + .wardley-annotation text { + fill: ${i.annotationTextColor}; + } + .wardley-annotations-box rect { + fill: ${i.annotationFill}; + stroke: ${i.annotationStroke}; + } + .wardley-annotations-box text { + fill: ${i.annotationTextColor}; + } + .wardley-pipeline-box { + stroke: ${i.componentStroke}; + } + .wardley-notes text { + fill: ${i.axisTextColor}; + } + `},"styles")});var O$e={};sr(O$e,{diagram:()=>f8t});var f8t,B$e=F(()=>{"use strict";R$e();D$e();M$e();P$e();f8t={parser:SX,db:L$e,renderer:I$e,styles:N$e}});var G$e,ow,y8t,v8t,x8t,b8t,T8t,C8t,bx,wX=F(()=>{"use strict";br();Qi();St();er();$n();G$e=s(()=>({domains:new Map,transitions:[]}),"createDefaultData"),ow=G$e(),y8t=s(()=>ow.domains,"getDomains"),v8t=s(()=>ow.transitions,"getTransitions"),x8t=s(e=>{if(e)for(let t of e){let r=t.domain,n=(t.items??[]).map(i=>({label:i.label}));ow.domains.set(r,{name:r,items:n})}},"setDomains"),b8t=s(e=>{e&&(ow.transitions=e.filter(t=>t.from===t.to?(se.warn(`Cynefin: self-loop transition on domain "${t.from}" is not meaningful and will be skipped.`),!1):!0).map(t=>({from:t.from,to:t.to,label:t.label||void 0})))},"setTransitions"),T8t=s(()=>Xr({...mr.cynefin,...Lt().cynefin}),"getConfig"),C8t=s(()=>{Tr(),ow=G$e()},"clear"),bx={getDomains:y8t,getTransitions:v8t,setDomains:x8t,setTransitions:b8t,getConfig:T8t,clear:C8t,setAccTitle:Lr,getAccTitle:Ir,setDiagramTitle:Gr,getDiagramTitle:Pr,getAccDescription:Nr,setAccDescription:Mr}});var S8t,z$e,V$e=F(()=>{"use strict";ls();St();to();wX();S8t=s(e=>{Hn(e,bx),bx.setDomains(e.domains),bx.setTransitions(e.transitions)},"populate"),z$e={parse:s(async e=>{let t=await Li("cynefin",e);se.debug(t),S8t(t)},"parse")}});function YL(e){let t=e+1831565813|0;return t=Math.imul(t^t>>>15,t|1),t^=t+Math.imul(t^t>>>7,t|61),((t^t>>>14)>>>0)/4294967296}function w8t(e){let t=0;for(let r=0;r{"use strict";s(YL,"seededRandom");s(w8t,"hashString");s(W$e,"resolveSeed");s(q$e,"generateFoldPath");s(H$e,"generateHorizontalBoundary");s(U$e,"generateCliffPath");s(Y$e,"generateConfusionPath")});var X$e,k8t,E8t,kX,A8t,K$e,Z$e=F(()=>{"use strict";cs();Vn();St();br();Mc();er();j$e();X$e={complex:{model:"Probe \u2192 Sense \u2192 Respond",practice:"Emergent Practices"},complicated:{model:"Sense \u2192 Analyse \u2192 Respond",practice:"Good Practices"},clear:{model:"Sense \u2192 Categorise \u2192 Respond",practice:"Best Practices"},chaotic:{model:"Act \u2192 Sense \u2192 Respond",practice:"Novel Practices"},confusion:{model:"",practice:"Disorder"}},k8t=s((e,t)=>{let r=e/2,n=t/2;return{complex:{cx:r/2,cy:n/2,x:0,y:0,w:r,h:n},complicated:{cx:r+r/2,cy:n/2,x:r,y:0,w:r,h:n},chaotic:{cx:r/2,cy:n+n/2,x:0,y:n,w:r,h:n},clear:{cx:r+r/2,cy:n+n/2,x:r,y:n,w:r,h:n},confusion:{cx:r,cy:n,x:r*.7,y:n*.7,w:r*.6,h:n*.6}}},"getDomainLayouts"),E8t=s(()=>{let e=ka(),t=Lt();return Xr(e,t.themeVariables).cynefin},"getCynefinDomainColors"),kX=3,A8t=s((e,t,r,n)=>{let i=n.db,a=i.getDomains(),o=i.getTransitions(),l=i.getDiagramTitle(),u=i.getAccTitle(),h=i.getAccDescription(),d=i.getConfig(),f=E8t();se.debug("Rendering Cynefin diagram");let p=d.width,m=d.height,g=d.padding,y=d.showDomainDescriptions,v=d.boundaryAmplitude,x=p+g*2,b=m+g*2,T={complex:f.complexBg,complicated:f.complicatedBg,clear:f.clearBg,chaotic:f.chaoticBg,confusion:f.confusionBg},w=wn(t);jr(w,b,x,d.useMaxWidth??!0),w.attr("viewBox",`0 0 ${x} ${b}`),u&&w.append("title").text(u),h&&w.append("desc").text(h);let C=w.append("g").attr("transform",`translate(${g}, ${g})`),S=k8t(p,m),k=W$e(d.seed,t),R=C.append("g").attr("class","cynefin-backgrounds"),M=["complex","complicated","chaotic","clear"];for(let O of M){let $=S[O];R.append("rect").attr("class","cynefinDomain").attr("x",$.x).attr("y",$.y).attr("width",$.w).attr("height",$.h).attr("fill",T[O]).attr("fill-opacity",.4).attr("stroke","none")}let P=C.append("g").attr("class","cynefin-boundaries");P.append("path").attr("class","cynefinBoundary").attr("d",q$e(p,m,k,v)).attr("fill","none"),P.append("path").attr("class","cynefinBoundary").attr("d",H$e(p,m,k+100,v)).attr("fill","none"),P.append("path").attr("class","cynefinCliff").attr("d",U$e(p,m)).attr("fill","none");let N=p*.15,_=m*.15;C.append("path").attr("class","cynefinConfusion").attr("d",Y$e(p/2,m/2,N,_)).attr("fill",T.confusion).attr("fill-opacity",.5);let A=C.append("g").attr("class","cynefin-labels");for(let O of M){let $=S[O];A.append("text").attr("class","cynefinDomainLabel").attr("x",$.cx).attr("y",y?$.cy-30:$.cy).attr("text-anchor","middle").attr("dominant-baseline","middle").text(O.charAt(0).toUpperCase()+O.slice(1))}if(A.append("text").attr("class","cynefinDomainLabel").attr("x",p/2).attr("y",y?m/2-10:m/2).attr("text-anchor","middle").attr("dominant-baseline","middle").text("Confusion"),y){let O=C.append("g").attr("class","cynefin-subtitles");for(let $ of M){let W=S[$],V=X$e[$];O.append("text").attr("class","cynefinSubtitle").attr("x",W.cx).attr("y",W.cy-10).attr("text-anchor","middle").attr("dominant-baseline","middle").text(V.model),O.append("text").attr("class","cynefinSubtitle").attr("x",W.cx).attr("y",W.cy+5).attr("text-anchor","middle").attr("dominant-baseline","middle").text(V.practice)}O.append("text").attr("class","cynefinSubtitle").attr("x",p/2).attr("y",m/2+8).attr("text-anchor","middle").attr("dominant-baseline","middle").text(X$e.confusion.practice)}let I=C.append("g").attr("class","cynefin-items"),D=26,L=10,B=["complex","complicated","chaotic","clear","confusion"];for(let O of B){let $=a.get(O);if(!$||$.items.length===0)continue;let W=S[O],V=O==="confusion",z=$.items,H=0;V&&$.items.length>kX&&(H=$.items.length-kX,z=$.items.slice(0,kX));let K;if(V){let J=y?22:14;K=W.cy+J}else K=W.cy+(y?25:15);if([...z].forEach((J,ne)=>{let Z=K+ne*(D+4),pe=I.append("g"),te=pe.append("text").attr("class","cynefinItemText").attr("x",0).attr("y",D/2).attr("text-anchor","middle").attr("dominant-baseline","central").text(J.label),fe=J.label.length*7,ce=te.node();if(ce&&typeof ce.getBBox=="function"){let we=ce.getBBox();we.width>0&&(fe=we.width)}let de=fe+L*2,Ee=W.cx-de/2;pe.attr("transform",`translate(${Ee}, ${Z})`),pe.insert("rect","text").attr("class","cynefinItem").attr("x",0).attr("y",0).attr("width",de).attr("height",D).attr("rx",4).attr("ry",4).attr("fill",T[O]).attr("fill-opacity",.95),te.attr("x",de/2).attr("y",D/2)}),H>0){let J=K+z.length*(D+4),ne=`+${H} more`,Z=I.append("g"),pe=Z.append("text").attr("class","cynefinItemText").attr("x",0).attr("y",D/2).attr("text-anchor","middle").attr("dominant-baseline","central").text(ne),te=ne.length*7,fe=pe.node();if(fe&&typeof fe.getBBox=="function"){let Ee=fe.getBBox();Ee.width>0&&(te=Ee.width)}let ce=te+L*2,de=W.cx-ce/2;Z.attr("transform",`translate(${de}, ${J})`),Z.insert("rect","text").attr("class","cynefinItemOverflow").attr("x",0).attr("y",0).attr("width",ce).attr("height",D).attr("rx",4).attr("ry",4).attr("fill",T[O]).attr("fill-opacity",.6),pe.attr("x",ce/2).attr("y",D/2)}}if(o.length>0){let O=w.select("defs").empty()?w.append("defs"):w.select("defs"),$=`cynefin-arrow-${t}`;O.append("marker").attr("id",$).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerWidth",6).attr("markerHeight",6).attr("orient","auto-start-reverse").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").attr("class","cynefinArrowHead");let W=C.append("g").attr("class","cynefin-arrows");o.forEach(V=>{let z=S[V.from],H=S[V.to];if(!z||!H)return;if(V.from===V.to){se.warn(`Cynefin renderer: skipping self-loop on domain "${V.from}"`);return}let K=z.cx,J=z.cy,ne=H.cx,Z=H.cy,pe=(K+ne)/2,te=(J+Z)/2,fe=ne-K,ce=Z-J,de=Math.sqrt(fe*fe+ce*ce),Ee=de*.15,we=-ce/de,Fe=fe/de,Ye=pe+we*Ee,He=te+Fe*Ee;W.append("path").attr("class","cynefinArrowLine").attr("d",`M${K},${J} Q${Ye},${He} ${ne},${Z}`).attr("fill","none").attr("marker-end",`url(#${$})`),V.label&&W.append("text").attr("class","cynefinArrowLabel").attr("x",Ye).attr("y",He-6).attr("text-anchor","middle").attr("dominant-baseline","auto").text(V.label)})}l&&C.append("text").attr("class","cynefinTitle").attr("x",p/2).attr("y",-g/2).attr("text-anchor","middle").attr("dominant-baseline","middle").text(l)},"draw"),K$e={draw:A8t}});var R8t,_8t,Q$e,J$e=F(()=>{"use strict";er();Mc();br();R8t=s(()=>{let e=ka(),t=Lt();return Xr(e,t.themeVariables).cynefin},"getCynefinTheme"),_8t=s(()=>{let e=R8t();return` + .cynefinDomain { + stroke: none; + } + .cynefinDomainLabel { + font-size: ${e.domainFontSize}px; + font-weight: bold; + fill: ${e.labelColor}; + } + .cynefinSubtitle { + font-size: ${e.itemFontSize-1}px; + fill: ${e.textColor}; + font-style: italic; + } + .cynefinItem { + fill-opacity: 0.95; + stroke: ${e.boundaryColor}; + stroke-width: 1; + } + .cynefinItemText { + font-size: ${e.itemFontSize}px; + fill: ${e.textColor}; + } + .cynefinItemOverflow { + fill-opacity: 0.6; + stroke: ${e.boundaryColor}; + stroke-width: 1; + stroke-dasharray: 3 2; + } + .cynefinBoundary { + stroke: ${e.boundaryColor}; + stroke-width: ${e.boundaryWidth}; + stroke-dasharray: 6 3; + } + .cynefinCliff { + stroke: ${e.cliffColor}; + stroke-width: ${e.cliffWidth}; + } + .cynefinConfusion { + stroke: ${e.boundaryColor}; + stroke-width: 1.5; + stroke-dasharray: 4 2; + } + .cynefinArrowLine { + stroke: ${e.arrowColor}; + stroke-width: ${e.arrowWidth}; + fill: none; + } + .cynefinArrowHead { + fill: ${e.arrowColor}; + stroke: none; + } + .cynefinArrowLabel { + font-size: ${e.itemFontSize-1}px; + fill: ${e.textColor}; + } + .cynefinTitle { + font-size: ${e.domainFontSize+2}px; + font-weight: bold; + fill: ${e.labelColor}; + } + `},"styles"),Q$e=_8t});var eFe={};sr(eFe,{diagram:()=>L8t});var L8t,tFe=F(()=>{"use strict";V$e();wX();Z$e();J$e();L8t={parser:z$e,db:bx,renderer:K$e,styles:Q$e}});var EX,AX,RX,_X,jL,Pp,Tx,M8t,iFe,aFe,N8t,P8t,O8t,B8t,$8t,F8t,G8t,z8t,V8t,fn,Zu=F(()=>{"use strict";Jt();St();$n();Yr();EX="",AX="",RX="",_X=[],jL=new Map,Pp=s(e=>xr(e,Pe()),"sanitizeText"),Tx=s(e=>{switch(e.type){case"terminal":return{...e,value:Pp(e.value)};case"nonterminal":return{...e,name:Pp(e.name)};case"sequence":return{...e,elements:e.elements.map(Tx)};case"choice":return{...e,alternatives:e.alternatives.map(Tx)};case"optional":return{...e,element:Tx(e.element)};case"repetition":return{...e,element:Tx(e.element),separator:e.separator?Tx(e.separator):void 0};case"special":return{...e,text:Pp(e.text)}}},"sanitizeAstNode"),M8t=s(()=>{EX="",AX="",RX="",_X.length=0,jL.clear(),Tr(),se.debug("[Railroad] Database cleared")},"clear"),iFe=s(e=>{EX=Pp(e),se.debug("[Railroad] Title set:",e)},"setTitle"),aFe=s(()=>EX,"getTitle"),N8t=s(e=>{let t={...e,name:Pp(e.name),definition:Tx(e.definition),comment:e.comment?Pp(e.comment):void 0};se.debug("[Railroad] Adding rule:",t.name),jL.has(t.name)&&se.warn(`[Railroad] Rule '${t.name}' is already defined. Overwriting.`),_X.push(t),jL.set(t.name,t)},"addRule"),P8t=s(()=>_X,"getRules"),O8t=s(e=>jL.get(e),"getRule"),B8t=s(e=>{AX=Pp(e).replace(/^\s+/g,""),se.debug("[Railroad] Accessibility title set:",e)},"setAccTitle"),$8t=s(()=>AX,"getAccTitle"),F8t=s(e=>{RX=Pp(e).replace(/\n\s+/g,` +`),se.debug("[Railroad] Accessibility description set:",e)},"setAccDescription"),G8t=s(()=>RX,"getAccDescription"),z8t=iFe,V8t=aFe,fn={clear:M8t,setTitle:iFe,getTitle:aFe,addRule:N8t,getRules:P8t,getRule:O8t,setAccTitle:B8t,getAccTitle:$8t,setAccDescription:F8t,getAccDescription:G8t,setDiagramTitle:z8t,getDiagramTitle:V8t}});var W8t,Cx,q8t,H8t,sFe,oFe=F(()=>{"use strict";ls();St();to();Zu();W8t=Fv().Railroad.parser.LangiumParser,Cx=s(e=>{switch(e.$type){case"RailroadTerminalExpr":return{type:"terminal",value:e.value};case"RailroadNonTerminalExpr":return{type:"nonterminal",name:e.name};case"RailroadSpecialExpr":return{type:"special",text:e.text};case"RailroadSequenceExpr":{let t=e.elements.map(Cx);return t.length===1?t[0]:{type:"sequence",elements:t}}case"RailroadChoiceExpr":{let t=e.alternatives.map(Cx);return t.length===1?t[0]:{type:"choice",alternatives:t}}case"RailroadOptionalExpr":return{type:"optional",element:Cx(e.element)};case"RailroadOneOrMoreExpr":return{type:"repetition",element:Cx(e.element),min:1,max:1/0};case"RailroadZeroOrMoreExpr":return{type:"repetition",element:Cx(e.element),min:0,max:1/0};default:throw new Error(`Unsupported railroad expression: ${e.$type}`)}},"transformExpression"),q8t=s(e=>({name:e.name,definition:Cx(e.definition)}),"transformRule"),H8t=s(e=>{Hn(e,fn),e.title&&fn.setTitle(e.title),e.rules.map(t=>fn.addRule(q8t(t)))},"populateDb"),sFe={parse:s(e=>{fn.clear(),se.debug("[Railroad Parser] Starting Langium parse");let t=W8t.parse(e);if(t.lexerErrors.length>0||t.parserErrors.length>0)throw new od(t);let r=t.value;se.debug("[Railroad Parser] Parsed rules:",r.rules.length),H8t(r),se.debug("[Railroad Parser] Parse complete")},"parse"),parser:{yy:fn}}});var Ua,lFe=F(()=>{"use strict";Ua={compactMode:!1,padding:10,verticalSeparation:8,horizontalSeparation:10,arcRadius:10,fontSize:14,fontFamily:"monospace",terminalFill:"#FFFFC0",terminalStroke:"#000000",terminalTextColor:"#000000",nonTerminalFill:"#FFFFFF",nonTerminalStroke:"#000000",nonTerminalTextColor:"#000000",lineColor:"#000000",strokeWidth:2,markerFill:"#000000",commentFill:"#E8E8E8",commentStroke:"#888888",commentTextColor:"#666666",specialFill:"#F0E0FF",specialStroke:"#8800CC",ruleNameColor:"#000066",showMarkers:!0,markerRadius:5}});var U8t,Y8t,j8t,cFe,X8t,K8t,Un,uFe,P0,Z8t,Q8t,XL,Op,Sx=F(()=>{"use strict";br();Mc();lFe();U8t=/^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$|^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\([\d\s%+,./-]+\)$|^[a-z]+$/i,Y8t=/^[\w "',.-]+$/,j8t=new Set(["compactMode","padding","verticalSeparation","horizontalSeparation","arcRadius","fontSize","fontFamily","terminalFill","terminalStroke","terminalTextColor","nonTerminalFill","nonTerminalStroke","nonTerminalTextColor","lineColor","strokeWidth","markerFill","commentFill","commentStroke","commentTextColor","specialFill","specialStroke","ruleNameColor","showMarkers","markerRadius"]),cFe=s(e=>e?Object.keys(e).every(t=>t==="railroad"||j8t.has(t)):!1,"isRailroadStyleOptions"),X8t=s(e=>e?"railroad"in e&&e.railroad?e.railroad:cFe(e)?e:{}:{},"extractRailroadOverrides"),K8t=s(e=>{if(!e||cFe(e))return{};let{railroad:t,svgId:r,theme:n,look:i,...a}=e;return a},"extractThemeOverrides"),Un=s((e,t)=>{if(typeof e!="string")return t;let r=e.trim();return U8t.test(r)?r:t},"sanitizeColorValue"),uFe=s((e,t)=>{if(typeof e!="string")return t;let r=e.trim();return Y8t.test(r)?r:t},"sanitizeFontFamilyValue"),P0=s((e,t)=>{let r=typeof e=="number"?e:typeof e=="string"?Number.parseFloat(e):Number.NaN;return Number.isFinite(r)&&r>=0?r:t},"sanitizeNumberValue"),Z8t=s(e=>{let t=typeof e=="number"?e:typeof e=="string"?Number.parseFloat(e):Number.NaN;return Number.isFinite(t)&&t>0?t:void 0},"parseThemeFontSize"),Q8t=s(e=>{let t=uFe(e.fontFamily,Ua.fontFamily),r=Z8t(e.fontSize)??Ua.fontSize;return{...Ua,fontFamily:t,fontSize:r,terminalFill:Un(e.secondBkg??e.secondaryColor,Ua.terminalFill),terminalStroke:Un(e.secondaryBorderColor??e.lineColor,Ua.terminalStroke),terminalTextColor:Un(e.secondaryTextColor??e.textColor,Ua.terminalTextColor),nonTerminalFill:Un(e.mainBkg??e.background,Ua.nonTerminalFill),nonTerminalStroke:Un(e.primaryBorderColor??e.lineColor,Ua.nonTerminalStroke),nonTerminalTextColor:Un(e.primaryTextColor??e.textColor,Ua.nonTerminalTextColor),lineColor:Un(e.lineColor,Ua.lineColor),markerFill:Un(e.lineColor,Ua.markerFill),commentFill:Un(e.labelBackground??e.tertiaryColor,Ua.commentFill),commentStroke:Un(e.tertiaryBorderColor??e.lineColor,Ua.commentStroke),commentTextColor:Un(e.tertiaryTextColor??e.textColor,Ua.commentTextColor),specialFill:Un(e.tertiaryColor??e.secondaryColor,Ua.specialFill),specialStroke:Un(e.tertiaryBorderColor??e.secondaryBorderColor,Ua.specialStroke),ruleNameColor:Un(e.titleColor??e.textColor,Ua.ruleNameColor)}},"buildThemeDefaults"),XL=s(e=>{let t=Lt(),r={...ka(),...t.themeVariables??{},...K8t(e)},n=Q8t(r),i={...t.railroad??{},...X8t(e)};return{compactMode:i.compactMode??n.compactMode,padding:P0(i.padding,n.padding),verticalSeparation:P0(i.verticalSeparation,n.verticalSeparation),horizontalSeparation:P0(i.horizontalSeparation,n.horizontalSeparation),arcRadius:P0(i.arcRadius,n.arcRadius),fontSize:P0(i.fontSize,n.fontSize),fontFamily:uFe(i.fontFamily,n.fontFamily),terminalFill:Un(i.terminalFill,n.terminalFill),terminalStroke:Un(i.terminalStroke,n.terminalStroke),terminalTextColor:Un(i.terminalTextColor,n.terminalTextColor),nonTerminalFill:Un(i.nonTerminalFill,n.nonTerminalFill),nonTerminalStroke:Un(i.nonTerminalStroke,n.nonTerminalStroke),nonTerminalTextColor:Un(i.nonTerminalTextColor,n.nonTerminalTextColor),lineColor:Un(i.lineColor,n.lineColor),strokeWidth:P0(i.strokeWidth,n.strokeWidth),markerFill:Un(i.markerFill,n.markerFill),commentFill:Un(i.commentFill,n.commentFill),commentStroke:Un(i.commentStroke,n.commentStroke),commentTextColor:Un(i.commentTextColor,n.commentTextColor),specialFill:Un(i.specialFill,n.specialFill),specialStroke:Un(i.specialStroke,n.specialStroke),ruleNameColor:Un(i.ruleNameColor,n.ruleNameColor),showMarkers:i.showMarkers??n.showMarkers,markerRadius:P0(i.markerRadius,n.markerRadius)}},"buildRailroadStyleOptions"),Op=s(e=>{let{fontFamily:t,fontSize:r,terminalFill:n,terminalStroke:i,terminalTextColor:a,nonTerminalFill:o,nonTerminalStroke:l,nonTerminalTextColor:u,lineColor:h,strokeWidth:d,markerFill:f,commentFill:p,commentStroke:m,commentTextColor:g,specialFill:y,specialStroke:v,ruleNameColor:x}=XL(e);return` + .railroad-diagram { + font-family: ${t}; + font-size: ${r}px; + } + + .railroad-terminal rect { + fill: ${n}; + stroke: ${i}; + stroke-width: ${d}px; + } + + .railroad-terminal text { + fill: ${a}; + font-family: ${t}; + font-size: ${r}px; + text-anchor: middle; + dominant-baseline: middle; + } + + .railroad-nonterminal rect { + fill: ${o}; + stroke: ${l}; + stroke-width: ${d}px; + } + + .railroad-nonterminal text { + fill: ${u}; + font-family: ${t}; + font-size: ${r}px; + text-anchor: middle; + dominant-baseline: middle; + } + + .railroad-line { + stroke: ${h}; + stroke-width: ${d}px; + fill: none; + } + + .railroad-start circle, + .railroad-end circle { + fill: ${f}; + } + + .railroad-comment ellipse { + fill: ${p}; + stroke: ${m}; + stroke-width: ${d}px; + } + + .railroad-comment text { + fill: ${g}; + font-style: italic; + font-family: ${t}; + font-size: ${r}px; + text-anchor: middle; + dominant-baseline: middle; + } + + .railroad-special rect { + fill: ${y}; + stroke: ${v}; + stroke-width: ${d}px; + stroke-dasharray: 5,3; + } + + .railroad-special text { + fill: ${u}; + font-family: ${t}; + font-size: ${r}px; + text-anchor: middle; + dominant-baseline: middle; + } + + .railroad-rule-name { + font-weight: bold; + fill: ${x}; + font-family: ${t}; + font-size: ${r}px; + } + + .railroad-group { + /* Grouping container, no specific styles */ + } +`},"getStyles")});var Io,LX,hFe,J8t,Bp,lw=F(()=>{"use strict";St();br();cs();Vn();Zu();Sx();Io=class{constructor(){this.d=""}static{s(this,"PathBuilder")}moveTo(t,r){return this.d+=`M ${t} ${r} `,this}lineTo(t,r){return this.d+=`L ${t} ${r} `,this}horizontalTo(t){return this.d+=`H ${t} `,this}verticalTo(t){return this.d+=`V ${t} `,this}arcTo(t,r,n,i,a,o,l){return this.d+=`A ${t} ${r} ${n} ${i?1:0} ${a?1:0} ${o} ${l} `,this}build(){return this.d.trim()}},LX=class{constructor(t,r=XL()){this.textCache=new Map;this.svg=t,this.config=r}static{s(this,"RailroadRenderer")}measureText(t){if(this.textCache.has(t))return this.textCache.get(t);let r=this.svg.append("text").attr("font-family",this.config.fontFamily).attr("font-size",this.config.fontSize).text(t),n=r.node().getBBox(),i={width:n.width,height:n.height};return r.remove(),this.textCache.set(t,i),i}renderTerminal(t,r){let n=this.measureText(r),i=n.width+this.config.padding*2,a=n.height+this.config.padding*2,o=t.append("g").attr("class","railroad-terminal");return o.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",a).attr("rx",10).attr("ry",10),o.append("text").attr("x",i/2).attr("y",a/2).text(r),{element:o.node(),dimensions:{width:i,height:a,up:a/2,down:a/2}}}renderNonTerminal(t,r){let n=this.measureText(r),i=n.width+this.config.padding*2,a=n.height+this.config.padding*2,o=t.append("g").attr("class","railroad-nonterminal");return o.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",a),o.append("text").attr("x",i/2).attr("y",a/2).text(r),{element:o.node(),dimensions:{width:i,height:a,up:a/2,down:a/2}}}renderSequence(t,r){let n=r.map(h=>this.renderExpression(t,h)),i=0,a=0,o=0;for(let h of n)i+=h.dimensions.width,a=Math.max(a,h.dimensions.up),o=Math.max(o,h.dimensions.down);i+=(n.length-1)*this.config.horizontalSeparation;let l=t.append("g").attr("class","railroad-sequence"),u=0;for(let h=0;hthis.renderExpression(t,p)),i=0,a=0;for(let p of n)i=Math.max(i,p.dimensions.width),a+=p.dimensions.height;a+=(n.length-1)*this.config.verticalSeparation;let o=this.config.arcRadius,l=o*4,u=i+l,h=t.append("g").attr("class","railroad-choice"),d=0,f=a/2;for(let p of n){let m=d,g=m+p.dimensions.up,y=o*2+(i-p.dimensions.width)/2;h.node().appendChild(p.element).setAttribute("transform",`translate(${y}, ${m})`);let x=new Io,b=g>f;g===f?x.moveTo(0,f).lineTo(y,g):x.moveTo(0,f).arcTo(o,o,0,!1,b,o,f+(b?o:-o)).lineTo(o,g-(b?o:-o)).arcTo(o,o,0,!1,!b,o*2,g).lineTo(y,g),h.append("path").attr("class","railroad-line").attr("d",x.build());let T=new Io,w=y+p.dimensions.width,C=u-o*2;g===f?T.moveTo(w,g).lineTo(u,f):T.moveTo(w,g).lineTo(C,g).arcTo(o,o,0,!1,!b,u-o,g+(b?-o:o)).lineTo(u-o,f+(b?o:-o)).arcTo(o,o,0,!1,b,u,f),h.append("path").attr("class","railroad-line").attr("d",T.build()),d+=p.dimensions.height+this.config.verticalSeparation}return{element:h.node(),dimensions:{width:u,height:a,up:f,down:a-f}}}renderOptional(t,r){let n=this.renderExpression(t,r),i=this.config.arcRadius,a=i*2,o=n.dimensions.width+i*4,l=n.dimensions.height+a,u=t.append("g").attr("class","railroad-optional"),h=i*2,d=a;u.node().appendChild(n.element).setAttribute("transform",`translate(${h}, ${d})`);let p=d+n.dimensions.up,m=new Io().moveTo(0,p).lineTo(i*2,p);u.append("path").attr("class","railroad-line").attr("d",m.build());let g=new Io().moveTo(h+n.dimensions.width,p).lineTo(o,p);u.append("path").attr("class","railroad-line").attr("d",g.build());let y=new Io().moveTo(0,p).arcTo(i,i,0,!1,!1,i,p-i).lineTo(i,i).arcTo(i,i,0,!1,!0,i*2,0).lineTo(o-i*2,0).arcTo(i,i,0,!1,!0,o-i,i).lineTo(o-i,p-i).arcTo(i,i,0,!1,!1,o,p);return u.append("path").attr("class","railroad-line").attr("d",y.build()),{element:u.node(),dimensions:{width:o,height:l,up:p,down:l-p}}}renderRepetition(t,r,n){let i=this.renderExpression(t,r),a=this.config.arcRadius,o=a*2,l=i.dimensions.width+a*4,u=n===0,h=i.dimensions.height+o+(u?o:0),d=t.append("g").attr("class","railroad-repetition"),f=a*2,p=u?o:0;d.node().appendChild(i.element).setAttribute("transform",`translate(${f}, ${p})`);let g=p+i.dimensions.up;d.append("path").attr("class","railroad-line").attr("d",new Io().moveTo(0,g).lineTo(a*2,g).build()),d.append("path").attr("class","railroad-line").attr("d",new Io().moveTo(f+i.dimensions.width,g).lineTo(l,g).build());let y=p+i.dimensions.height+a,v=new Io().moveTo(f+i.dimensions.width,g).arcTo(a,a,0,!1,!0,f+i.dimensions.width+a,g+a).lineTo(f+i.dimensions.width+a,y).arcTo(a,a,0,!1,!0,f+i.dimensions.width,y+a).lineTo(a*2,y+a).arcTo(a,a,0,!1,!0,a,y).lineTo(a,g+a).arcTo(a,a,0,!1,!0,a*2,g);if(d.append("path").attr("class","railroad-line").attr("d",v.build()),u){let x=new Io().moveTo(0,g).arcTo(a,a,0,!1,!1,a,g-a).lineTo(a,a).arcTo(a,a,0,!1,!0,a*2,0).lineTo(l-a*2,0).arcTo(a,a,0,!1,!0,l-a,a).lineTo(l-a,g-a).arcTo(a,a,0,!1,!1,l,g);d.append("path").attr("class","railroad-line").attr("d",x.build())}return{element:d.node(),dimensions:{width:l,height:h,up:g,down:h-g}}}renderSpecial(t,r){let n=this.measureText("? "+r+" ?"),i=n.width+this.config.padding*2,a=n.height+this.config.padding*2,o=t.append("g").attr("class","railroad-special");return o.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",a),o.append("text").attr("x",i/2).attr("y",a/2).text("? "+r+" ?"),{element:o.node(),dimensions:{width:i,height:a,up:a/2,down:a/2}}}renderExpression(t,r){switch(r.type){case"terminal":return this.renderTerminal(t,r.value);case"nonterminal":return this.renderNonTerminal(t,r.name);case"sequence":return this.renderSequence(t,r.elements);case"choice":return this.renderChoice(t,r.alternatives);case"optional":return this.renderOptional(t,r.element);case"repetition":return this.renderRepetition(t,r.element,r.min);case"special":return this.renderSpecial(t,r.text);default:throw new Error(`Unknown node type: ${r.type}`)}}renderRule(t,r){let n=this.svg.append("g").attr("class","railroad-rule").attr("transform",`translate(0, ${r})`),i=t.name+" =",a=this.measureText(i).width+20,o=a+20,l=n.append("g"),u=this.renderExpression(l,t.definition),h=Math.max(20,u.dimensions.up),d=h-u.dimensions.up;return l.attr("transform",`translate(${o}, ${d})`),n.append("g").attr("class","railroad-rule-name-group").append("text").attr("class","railroad-rule-name").attr("x",0).attr("y",h).text(i),n.append("g").attr("class","railroad-start").append("circle").attr("cx",a).attr("cy",h).attr("r",this.config.markerRadius),n.append("g").attr("class","railroad-end").append("circle").attr("cx",o+u.dimensions.width+10).attr("cy",h).attr("r",this.config.markerRadius),n.append("path").attr("class","railroad-line").attr("d",new Io().moveTo(a+this.config.markerRadius,h).lineTo(o,h).build()),n.append("path").attr("class","railroad-line").attr("d",new Io().moveTo(o+u.dimensions.width,h).lineTo(o+u.dimensions.width+10-this.config.markerRadius,h).build()),{height:Math.max(40,d+u.dimensions.height+this.config.padding*2),width:o+u.dimensions.width+10+this.config.markerRadius}}renderDiagram(t){let r=this.config.padding,n=0;for(let i of t){let a=this.renderRule(i,r);r+=a.height+this.config.verticalSeparation,n=Math.max(n,a.width)}return{width:n+this.config.padding*2,height:r+this.config.padding}}},hFe=s((e,t,r)=>{jr(e,t.height,t.width,r),e.attr("viewBox",`0 0 ${t.width} ${t.height}`)},"configureRailroadSvgSize"),J8t=s((e,t,r)=>{se.debug(`[Railroad] Rendering diagram +`+e);try{let n=wn(t);n.attr("class","railroad-diagram");let a=Lt().railroad?.useMaxWidth??!0,o=fn.getRules();if(se.debug(`[Railroad] Rendering ${o.length} rules`),o.length===0){se.warn("[Railroad] No rules to render"),hFe(n,{height:100,width:200},a);return}let u=new LX(n,XL()).renderDiagram(o);hFe(n,u,a),se.debug("[Railroad] Render complete")}catch(n){throw se.error("[Railroad] Render error:",n),n}},"draw"),Bp={draw:J8t}});var fFe={};sr(fFe,{default:()=>eMt,diagram:()=>dFe});var dFe,eMt,pFe=F(()=>{"use strict";oFe();Zu();lw();Sx();dFe={parser:sFe,db:fn,renderer:Bp,styles:Op},eMt=dFe});var nMt,KL,iMt,yFe,aMt,sMt,oMt,lMt,vFe,xFe=F(()=>{"use strict";ls();St();to();Zu();nMt=Gv().RailroadEbnf.parser.LangiumParser,KL=s(e=>{let t=e.alternatives.map(iMt);return t.length===1?t[0]:{type:"choice",alternatives:t}},"transformChoice"),iMt=s(e=>{let t=e.elements.map(sMt);return t.length===1?t[0]:{type:"sequence",elements:t}},"transformSequence"),yFe=s(e=>{switch(e.$type){case"EbnfTerminal":return{type:"terminal",value:e.value};case"EbnfNonTerminal":return{type:"nonterminal",name:e.name};case"EbnfSpecial":return{type:"special",text:e.text};case"EbnfGroup":return KL(e.element);case"EbnfOptional":return{type:"optional",element:KL(e.element)};case"EbnfRepetition":return{type:"repetition",element:KL(e.element),min:0,max:1/0};default:throw new Error(`Unsupported EBNF primary node: ${e.$type}`)}},"transformPrimary"),aMt=s((e,t)=>{switch(t.$type){case"EbnfOptionalPostfix":return{type:"optional",element:e};case"EbnfZeroOrMorePostfix":return{type:"repetition",element:e,min:0,max:1/0};case"EbnfOneOrMorePostfix":return{type:"repetition",element:e,min:1,max:1/0};case"EbnfExceptionPostfix":return{type:"sequence",elements:[e,{type:"terminal",value:"-"},yFe(t.except)]};default:throw new Error(`Unsupported EBNF postfix node: ${t.$type}`)}},"transformPostfix"),sMt=s(e=>e.postfixes.reduce((t,r)=>aMt(t,r),yFe(e.base)),"transformTerm"),oMt=s(e=>({name:e.name,definition:KL(e.definition)}),"transformRule"),lMt=s(e=>{Hn(e,fn),e.title&&fn.setTitle(e.title),e.rules.map(t=>fn.addRule(oMt(t)))},"populateDb"),vFe={parse:s(e=>{fn.clear(),se.debug("[EBNF Parser] Starting Langium parse");let t=nMt.parse(e);if(t.lexerErrors.length>0||t.parserErrors.length>0)throw new od(t);let r=t.value;se.debug("[EBNF Parser] Parsed rules:",r.rules.length),lMt(r),se.debug("[EBNF Parser] Parse complete")},"parse"),parser:{yy:fn}}});var bFe={};sr(bFe,{diagram:()=>cMt});var cMt,TFe=F(()=>{"use strict";xFe();Zu();lw();Sx();cMt={parser:vFe,db:fn,renderer:Bp,styles:Op}});var dMt,DX,fMt,pMt,mMt,gMt,yMt,vMt,wFe,kFe=F(()=>{"use strict";ls();St();to();Zu();dMt=zv().RailroadAbnf.parser.LangiumParser,DX=s(e=>{let t=e.alternatives.map(fMt);return t.length===1?t[0]:{type:"choice",alternatives:t}},"transformAlternation"),fMt=s(e=>{let t=e.elements.map(mMt);return t.length===1?t[0]:{type:"sequence",elements:t}},"transformConcatenation"),pMt=s(e=>{if(e.includes("*")){let[r,n]=e.split("*"),i=r?parseInt(r,10):0,a=n?parseInt(n,10):1/0;return{min:i,max:a}}let t=parseInt(e,10);return{min:t,max:t}},"parseRepeat"),mMt=s(e=>{let t=gMt(e.primary);if(!e.repeat)return t;let{min:r,max:n}=pMt(e.repeat);return r===0&&n===1?{type:"optional",element:t}:{type:"repetition",element:t,min:r,max:n}},"transformElement"),gMt=s(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return DX(e.element);case"AbnfOptionalGroup":return{type:"optional",element:DX(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),yMt=s(e=>({name:e.name,definition:DX(e.definition)}),"transformRule"),vMt=s(e=>{Hn(e,fn),e.title&&fn.setTitle(e.title),e.rules.map(t=>fn.addRule(yMt(t)))},"populateDb"),wFe={parse:s(e=>{fn.clear(),se.debug("[ABNF Parser] Starting Langium parse");let t=dMt.parse(e);if(t.lexerErrors.length>0||t.parserErrors.length>0)throw new od(t);let r=t.value;se.debug("[ABNF Parser] Parsed rules:",r.rules.length),vMt(r),se.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:fn}}});var EFe={};sr(EFe,{diagram:()=>xMt});var xMt,AFe=F(()=>{"use strict";kFe();Zu();lw();Sx();xMt={parser:wFe,db:fn,renderer:Bp,styles:Op}});var CMt,DFe,SMt,wMt,LFe,kMt,EMt,AMt,RMt,IFe,MFe=F(()=>{"use strict";ls();St();to();Zu();CMt=Vv().RailroadPeg.parser.LangiumParser,DFe=s(e=>{let t=e.alternatives.map(SMt);return t.length===1?t[0]:{type:"choice",alternatives:t}},"transformOrderedChoice"),SMt=s(e=>{let t=e.elements.map(wMt);return t.length===1?t[0]:{type:"sequence",elements:t}},"transformSequence"),wMt=s(e=>{let t=kMt(e.suffix);return e.operator?{type:"special",text:e.operator==="&"?`&${LFe(t)}`:`!${LFe(t)}`}:t},"transformPrefix"),LFe=s(e=>{switch(e.type){case"terminal":return`"${e.value}"`;case"nonterminal":return e.name;case"special":return e.text;default:return"(...)"}},"nodeToLabel"),kMt=s(e=>{let t=EMt(e.primary);if(!e.operator)return t;switch(e.operator){case"?":return{type:"optional",element:t};case"*":return{type:"repetition",element:t,min:0,max:1/0};case"+":return{type:"repetition",element:t,min:1,max:1/0};default:throw new Error(`Unsupported PEG suffix operator: ${e.operator}`)}},"transformSuffix"),EMt=s(e=>{switch(e.$type){case"PegLiteral":return{type:"terminal",value:e.value};case"PegIdentifier":return{type:"nonterminal",name:e.name};case"PegGroup":return DFe(e.element);case"PegAny":return{type:"special",text:e.dot};default:throw new Error(`Unsupported PEG primary node: ${e.$type}`)}},"transformPrimary"),AMt=s(e=>({name:e.name,definition:DFe(e.definition)}),"transformRule"),RMt=s(e=>{Hn(e,fn),e.title&&fn.setTitle(e.title),e.rules.map(t=>fn.addRule(AMt(t)))},"populateDb"),IFe={parse:s(e=>{fn.clear(),se.debug("[PEG Parser] Starting Langium parse");let t=CMt.parse(e);if(t.lexerErrors.length>0||t.parserErrors.length>0)throw new od(t);let r=t.value;se.debug("[PEG Parser] Parsed rules:",r.rules.length),RMt(r),se.debug("[PEG Parser] Parse complete")},"parse"),parser:{yy:fn}}});var NFe={};sr(NFe,{diagram:()=>_Mt});var _Mt,PFe=F(()=>{"use strict";MFe();Zu();lw();Sx();_Mt={parser:IFe,db:fn,renderer:Bp,styles:Op}});var yNt={};sr(yNt,{clearLayoutRenderState:()=>K9,createCommonLayoutRenderer:()=>f1,default:()=>gNt,defaultMeasureLayout:()=>Z9,paintLayoutData:()=>Q9});Hl();Y7();Xp();var hZe=s(e=>/^\s*C4Context|C4Container|C4Component|C4Dynamic|C4Deployment/.test(e),"detector"),dZe=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(Mce(),Ice));return{id:"c4",diagram:e}},"loader"),fZe={id:"c4",detector:hZe,loader:dZe},Nce=fZe;var cCe="flowchart",sgt=s((e,t)=>t?.flowchart?.defaultRenderer==="dagre-wrapper"||t?.flowchart?.defaultRenderer==="elk"?!1:/^\s*graph/.test(e),"detector"),ogt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(aC(),pA));return{id:cCe,diagram:e}},"loader"),lgt={id:cCe,detector:sgt,loader:ogt},uCe=lgt;var hCe="flowchart-v2",cgt=s((e,t)=>t?.flowchart?.defaultRenderer==="dagre-d3"?!1:(t?.flowchart?.defaultRenderer==="elk"&&(t.layout="elk"),/^\s*graph/.test(e)&&t?.flowchart?.defaultRenderer==="dagre-wrapper"?!0:/^\s*flowchart/.test(e)),"detector"),ugt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(aC(),pA));return{id:hCe,diagram:e}},"loader"),hgt={id:hCe,detector:cgt,loader:ugt},dCe=hgt;var yCe="swimlane",pgt=s(e=>/^\s*swimlane-beta\b/.test(e),"detector"),mgt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(gCe(),mCe));return{id:yCe,diagram:e}},"loader"),ggt={id:yCe,detector:pgt,loader:mgt},vCe=ggt;var Tgt=s(e=>/^\s*erDiagram/.test(e),"detector"),Cgt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(ACe(),ECe));return{id:"er",diagram:e}},"loader"),Sgt={id:"er",detector:Tgt,loader:Cgt},RCe=Sgt;var H7e="gitGraph",j4t=s(e=>/^\s*gitGraph/.test(e),"detector"),X4t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(q7e(),W7e));return{id:H7e,diagram:e}},"loader"),K4t={id:H7e,detector:j4t,loader:X4t},U7e=K4t;var kIe="gantt",$3t=s(e=>/^\s*gantt/.test(e),"detector"),F3t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(wIe(),SIe));return{id:kIe,diagram:e}},"loader"),G3t={id:kIe,detector:$3t,loader:F3t},EIe=G3t;var PIe="info",H3t=s(e=>/^\s*info/.test(e),"detector"),U3t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(NIe(),MIe));return{id:PIe,diagram:e}},"loader"),OIe={id:PIe,detector:H3t,loader:U3t};var a5t=s(e=>/^\s*pie/.test(e),"detector"),s5t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(HIe(),qIe));return{id:"pie",diagram:e}},"loader"),UIe={id:"pie",detector:a5t,loader:s5t};var a8e="quadrantChart",S5t=s(e=>/^\s*quadrantChart/.test(e),"detector"),w5t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(i8e(),n8e));return{id:a8e,diagram:e}},"loader"),k5t={id:a8e,detector:S5t,loader:w5t},s8e=k5t;var O8e="xychart",H5t=s(e=>/^\s*xychart(-beta)?/.test(e),"detector"),U5t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(P8e(),N8e));return{id:O8e,diagram:e}},"loader"),Y5t={id:O8e,detector:H5t,loader:U5t},B8e=Y5t;var U8e="requirement",Q5t=s(e=>/^\s*requirement(Diagram)?/.test(e),"detector"),J5t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(H8e(),q8e));return{id:U8e,diagram:e}},"loader"),eAt={id:U8e,detector:Q5t,loader:J5t},Y8e=eAt;var hMe="sequence",KAt=s(e=>/^\s*sequenceDiagram/.test(e),"detector"),ZAt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(uMe(),cMe));return{id:hMe,diagram:e}},"loader"),QAt={id:hMe,detector:KAt,loader:ZAt},dMe=QAt;var vMe="class",iRt=s((e,t)=>t?.class?.defaultRenderer==="dagre-wrapper"?!1:/^\s*classDiagram/.test(e),"detector"),aRt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(yMe(),gMe));return{id:vMe,diagram:e}},"loader"),sRt={id:vMe,detector:iRt,loader:aRt},xMe=sRt;var CMe="classDiagram",lRt=s((e,t)=>/^\s*classDiagram/.test(e)&&t?.class?.defaultRenderer==="dagre-wrapper"?!0:/^\s*classDiagram-v2/.test(e),"detector"),cRt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(TMe(),bMe));return{id:CMe,diagram:e}},"loader"),uRt={id:CMe,detector:lRt,loader:cRt},SMe=uRt;var tNe="state",ORt=s((e,t)=>t?.state?.defaultRenderer==="dagre-wrapper"?!1:/^\s*stateDiagram/.test(e),"detector"),BRt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(eNe(),JMe));return{id:tNe,diagram:e}},"loader"),$Rt={id:tNe,detector:ORt,loader:BRt},rNe=$Rt;var aNe="stateDiagram",GRt=s((e,t)=>!!(/^\s*stateDiagram-v2/.test(e)||/^\s*stateDiagram/.test(e)&&t?.state?.defaultRenderer==="dagre-wrapper"),"detector"),zRt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(iNe(),nNe));return{id:aNe,diagram:e}},"loader"),VRt={id:aNe,detector:GRt,loader:zRt},sNe=VRt;var TNe="journey",c6t=s(e=>/^\s*journey/.test(e),"detector"),u6t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(bNe(),xNe));return{id:TNe,diagram:e}},"loader"),h6t={id:TNe,detector:c6t,loader:u6t},CNe=h6t;St();cs();Vn();var d6t=s((e,t,r)=>{se.debug(`rendering svg for syntax error +`);let n=wn(t),i=n.append("g");n.attr("viewBox","0 0 2412 512"),jr(n,100,512,!0),i.append("path").attr("class","error-icon").attr("d","m411.313,123.313c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32-9.375,9.375-20.688-20.688c-12.484-12.5-32.766-12.5-45.25,0l-16,16c-1.261,1.261-2.304,2.648-3.31,4.051-21.739-8.561-45.324-13.426-70.065-13.426-105.867,0-192,86.133-192,192s86.133,192 192,192 192-86.133 192-192c0-24.741-4.864-48.327-13.426-70.065 1.402-1.007 2.79-2.049 4.051-3.31l16-16c12.5-12.492 12.5-32.758 0-45.25l-20.688-20.688 9.375-9.375 32.001-31.999zm-219.313,100.687c-52.938,0-96,43.063-96,96 0,8.836-7.164,16-16,16s-16-7.164-16-16c0-70.578 57.422-128 128-128 8.836,0 16,7.164 16,16s-7.164,16-16,16z"),i.append("path").attr("class","error-icon").attr("d","m459.02,148.98c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l16,16c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16.001-16z"),i.append("path").attr("class","error-icon").attr("d","m340.395,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16-16c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l15.999,16z"),i.append("path").attr("class","error-icon").attr("d","m400,64c8.844,0 16-7.164 16-16v-32c0-8.836-7.156-16-16-16-8.844,0-16,7.164-16,16v32c0,8.836 7.156,16 16,16z"),i.append("path").attr("class","error-icon").attr("d","m496,96.586h-32c-8.844,0-16,7.164-16,16 0,8.836 7.156,16 16,16h32c8.844,0 16-7.164 16-16 0-8.836-7.156-16-16-16z"),i.append("path").attr("class","error-icon").attr("d","m436.98,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688l32-32c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32c-6.251,6.25-6.251,16.375-0.001,22.625z"),i.append("text").attr("class","error-text").attr("x",1440).attr("y",250).attr("font-size","150px").style("text-anchor","middle").text("Syntax error in text"),i.append("text").attr("class","error-text").attr("x",1250).attr("y",400).attr("font-size","100px").style("text-anchor","middle").text(`mermaid version ${r}`)},"draw"),jY={draw:d6t},SNe=jY;var f6t={db:{},renderer:jY,parser:{parse:s(()=>{},"parse")}},wNe=f6t;var kNe="flowchart-elk",p6t=s((e,t={})=>/^\s*flowchart-elk/.test(e)||/^\s*(flowchart|graph)/.test(e)&&t?.flowchart?.defaultRenderer==="elk"?(t.layout="elk",!0):!1,"detector"),m6t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(aC(),pA));return{id:kNe,diagram:e}},"loader"),g6t={id:kNe,detector:p6t,loader:m6t},ENe=g6t;var oPe="timeline",z6t=s(e=>/^\s*timeline/.test(e),"detector"),V6t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(sPe(),aPe));return{id:oPe,diagram:e}},"loader"),W6t={id:oPe,detector:z6t,loader:V6t},lPe=W6t;var kPe="mindmap",Q6t=s(e=>/^\s*mindmap/.test(e),"detector"),J6t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(wPe(),SPe));return{id:kPe,diagram:e}},"loader"),e_t={id:kPe,detector:Q6t,loader:J6t},EPe=e_t;var $Pe="kanban",g_t=s(e=>/^\s*kanban/.test(e),"detector"),y_t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(BPe(),OPe));return{id:$Pe,diagram:e}},"loader"),v_t={id:$Pe,detector:g_t,loader:y_t},FPe=v_t;var bOe="sankey",V_t=s(e=>/^\s*sankey(-beta)?/.test(e),"detector"),W_t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(xOe(),vOe));return{id:bOe,diagram:e}},"loader"),q_t={id:bOe,detector:V_t,loader:W_t},TOe=q_t;var _Oe="packet",J_t=s(e=>/^\s*packet(-beta)?/.test(e),"detector"),eLt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(ROe(),AOe));return{id:_Oe,diagram:e}},"loader"),LOe={id:_Oe,detector:J_t,loader:eLt};var zOe="radar",CLt=s(e=>/^\s*radar-beta/.test(e),"detector"),SLt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(GOe(),FOe));return{id:zOe,diagram:e}},"loader"),VOe={id:zOe,detector:CLt,loader:SLt};var p9e="block",rDt=s(e=>/^\s*block(-beta)?/.test(e),"detector"),nDt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(f9e(),d9e));return{id:p9e,diagram:e}},"loader"),iDt={id:p9e,detector:rDt,loader:nDt},m9e=iDt;var P9e="treeView",RDt=s(e=>/^\s*treeView-beta/.test(e),"detector"),_Dt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(N9e(),M9e));return{id:P9e,diagram:e}},"loader"),LDt={id:P9e,detector:RDt,loader:_Dt},O9e=LDt;var aBe="architecture",HDt=s(e=>/^\s*architecture/.test(e),"detector"),UDt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(iBe(),nBe));return{id:aBe,diagram:e}},"loader"),YDt={id:aBe,detector:HDt,loader:UDt},sBe=YDt;var xBe="eventmodeling",D7t=s(e=>/^\s*eventmodeling/.test(e),"detector"),I7t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(vBe(),yBe));return{id:xBe,diagram:e}},"loader"),M7t={id:xBe,detector:D7t,loader:I7t},bBe=M7t;var BBe="ishikawa",U7t=s(e=>/^\s*ishikawa(-beta)?\b/i.test(e),"detector"),Y7t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(OBe(),PBe));return{id:BBe,diagram:e}},"loader"),$Be={id:BBe,detector:U7t,loader:Y7t};var p$e="venn",DIt=s(e=>/^\s*venn-beta/.test(e),"detector"),IIt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(f$e(),d$e));return{id:p$e,diagram:e}},"loader"),MIt={id:p$e,detector:DIt,loader:IIt},m$e=MIt;Xp();Jt();var k$e="treemap",VIt=s(e=>/^\s*treemap/.test(e),"detector"),WIt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(w$e(),S$e));return{id:k$e,diagram:e}},"loader"),E$e={id:k$e,detector:VIt,loader:WIt};var $$e="wardley",p8t=s(e=>/^\s*wardley-beta/i.test(e),"detector"),m8t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(B$e(),O$e));return{id:$$e,diagram:e}},"loader"),g8t={id:$$e,detector:p8t,loader:m8t},F$e=g8t;var rFe="cynefin",D8t=s(e=>/^\s*cynefin-beta(?:[\s:]|$)/.test(e),"detector"),I8t=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(tFe(),eFe));return{id:rFe,diagram:e}},"loader"),nFe={id:rFe,detector:D8t,loader:I8t};var mFe="railroad",tMt=s(e=>/^\s*railroad-beta/i.test(e),"detector"),rMt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(pFe(),fFe));return{id:mFe,diagram:e}},"loader"),gFe={id:mFe,detector:tMt,loader:rMt};var CFe="railroadEbnf",uMt=s(e=>/^\s*railroad-ebnf-beta/i.test(e),"detector"),hMt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(TFe(),bFe));return{id:CFe,diagram:e}},"loader"),SFe={id:CFe,detector:uMt,loader:hMt};var RFe="railroadAbnf",bMt=s(e=>/^\s*railroad-abnf-beta/i.test(e),"detector"),TMt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(AFe(),EFe));return{id:RFe,diagram:e}},"loader"),_Fe={id:RFe,detector:bMt,loader:TMt};var OFe="railroadPeg",LMt=s(e=>/^\s*railroad-peg-beta/i.test(e),"detector"),DMt=s(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(PFe(),NFe));return{id:OFe,diagram:e}},"loader"),BFe={id:OFe,detector:LMt,loader:DMt};var $Fe=!1,wx=s(()=>{$Fe||($Fe=!0,Kp("error",wNe,e=>e.toLowerCase().trim()==="error"),Kp("---",{db:{clear:s(()=>{},"clear")},styles:{},renderer:{draw:s(()=>{},"draw")},parser:{parse:s(()=>{throw new Error("Diagrams beginning with --- are not valid. If you were trying to use a YAML front-matter, please ensure that you've correctly opened and closed the YAML front-matter with un-indented `---` blocks")},"parse")},init:s(()=>null,"init")},e=>e.toLowerCase().trimStart().startsWith("---")),tb(ENe,EPe,sBe),tb(Nce,FPe,SMe,xMe,RCe,EIe,OIe,UIe,Y8e,dMe,vCe,dCe,uCe,lPe,U7e,sNe,rNe,CNe,s8e,TOe,LOe,B8e,m9e,bBe,O9e,VOe,$Be,E$e,gFe,SFe,_Fe,BFe,m$e,F$e,nFe))},"addDiagrams");St();Xp();Jt();var FFe=s(async()=>{se.debug("Loading registered diagrams");let t=(await Promise.allSettled(Object.entries(ch).map(async([r,{detector:n,loader:i}])=>{if(i)try{ob(r)}catch{try{let{diagram:a,id:o}=await i();Kp(o,a,n)}catch(a){throw se.error(`Failed to load external diagram with key ${r}. Removing from detectors.`),delete ch[r],a}}}))).filter(r=>r.status==="rejected");if(t.length>0){se.error(`Failed to load ${t.length} external diagrams`);for(let r of t)se.error(r);throw new Error(`Failed to load ${t.length} external diagrams`)}},"loadRegisteredDiagrams");St();Wr();var kx="comm",ZL="rule",QL="decl";var GFe="@media",zFe="@import";var VFe="@supports";var WFe="@namespace",cw="@keyframes";var JL="@layer",qFe="@scope";var IX=Math.abs,uw=String.fromCharCode;function eD(e){return e.trim()}s(eD,"trim");function Ex(e,t,r){return e.replace(t,r)}s(Ex,"replace");function HFe(e,t,r){return e.indexOf(t,r)}s(HFe,"indexof");function vd(e,t){return e.charCodeAt(t)|0}s(vd,"charat");function xd(e,t,r){return e.slice(t,r)}s(xd,"substr");function Mo(e){return e.length}s(Mo,"strlen");function tD(e){return e.length}s(tD,"sizeof");function Ax(e,t){return t.push(e),e}s(Ax,"append");var rD=1,Rx=1,UFe=0,zl=0,la=0,Lx="";function nD(e,t,r,n,i,a,o,l){return{value:e,root:t,parent:r,type:n,props:i,children:a,line:rD,column:Rx,length:o,return:"",siblings:l}}s(nD,"node");function YFe(){return la}s(YFe,"char");function jFe(){return la=zl>0?vd(Lx,--zl):0,Rx--,la===10&&(Rx=1,rD--),la}s(jFe,"prev");function Vl(){return la=zl2||_x(la)>3?"":" "}s(ZFe,"whitespace");function QFe(e,t){for(;--t&&Vl()&&!(la<48||la>102||la>57&&la<65||la>70&&la<97););return iD(e,hw()+(t<6&&bd()==32&&Vl()==32))}s(QFe,"escaping");function MX(e){for(;Vl();)switch(la){case e:return zl;case 34:case 39:e!==34&&e!==39&&MX(la);break;case 40:e===41&&MX(e);break;case 92:Vl();break}return zl}s(MX,"delimiter");function JFe(e,t){for(;Vl()&&e+la!==57;)if(e+la===84&&bd()===47)break;return"/*"+iD(t,zl-1)+"*"+uw(e===47?e:Vl())}s(JFe,"commenter");function eGe(e){for(;!_x(bd());)Vl();return iD(e,zl)}s(eGe,"identifier");function nGe(e){return KFe(sD("",null,null,null,[""],e=XFe(e),0,[0],e))}s(nGe,"compile");function sD(e,t,r,n,i,a,o,l,u){for(var h=0,d=0,f=o,p=0,m=0,g=0,y=1,v=1,x=1,b=0,T="",w=i,C=a,S=n,k=T;v;)switch(g=b,b=Vl()){case 40:if(g!=108&&vd(k,f-1)==58){HFe(k+=Ex(aD(b),"&","&\f"),"&\f",IX(h?l[h-1]:0))!=-1&&(x=-1);break}case 34:case 39:case 91:k+=aD(b);break;case 9:case 10:case 13:case 32:k+=ZFe(g);break;case 92:k+=QFe(hw()-1,7);continue;case 47:switch(bd()){case 42:case 47:Ax(MMt(JFe(Vl(),hw()),t,r,u),u),(_x(g||1)==5||_x(bd()||1)==5)&&Mo(k)&&xd(k,-1,void 0)!==" "&&(k+=" ");break;default:k+="/"}break;case 123*y:l[h++]=Mo(k)*x;case 125*y:case 59:case 0:switch(b){case 0:case 125:v=0;case 59+d:x==-1&&(k=Ex(k,/\f/g,"")),m>0&&(Mo(k)-f||y===0&&g===47)&&Ax(m>32?rGe(k+";",n,r,f-1,u):rGe(Ex(k," ","")+";",n,r,f-2,u),u);break;case 59:k+=";";default:if(Ax(S=tGe(k,t,r,h,d,i,l,T,w=[],C=[],f,a),a),b===123)if(d===0)sD(k,t,S,S,w,a,f,l,C);else{switch(p){case 99:if(vd(k,3)===110)break;case 108:if(vd(k,2)===97)break;default:d=0;case 100:case 109:case 115:}d?sD(e,S,S,n&&Ax(tGe(e,S,S,0,0,i,l,T,i,w=[],f,C),C),i,C,f,l,n?w:C):sD(k,S,S,S,[""],C,0,l,C)}}h=d=m=0,y=x=1,T=k="",f=o;break;case 58:f=1+Mo(k),m=g;default:if(y<1){if(b==123)--y;else if(b==125&&y++==0&&jFe()==125)continue}switch(k+=uw(b),b*y){case 38:x=d>0?1:(k+="\f",-1);break;case 44:l[h++]=(Mo(k)-1)*x,x=1;break;case 64:bd()===45&&(k+=aD(Vl())),p=bd(),d=f=Mo(T=k+=eGe(hw())),b++;break;case 45:g===45&&Mo(k)==2&&(y=0)}}return a}s(sD,"parse");function tGe(e,t,r,n,i,a,o,l,u,h,d,f){for(var p=i-1,m=i===0?a:[""],g=tD(m),y=0,v=0,x=0;y0?m[b]+" "+T:Ex(T,/&\f/g,m[b])))&&(u[x++]=w);return nD(e,t,r,i===0?ZL:l,u,h,d,f)}s(tGe,"ruleset");function MMt(e,t,r,n){return nD(e,t,r,kx,uw(YFe()),xd(e,2,-2),0,n)}s(MMt,"comment");function rGe(e,t,r,n,i){return nD(e,t,r,QL,xd(e,0,n),xd(e,n+1,-1),n,i)}s(rGe,"declaration");function oD(e,t){for(var r="",n=0;n{lGe.forEach(e=>{e()}),lGe=[]},"attachFunctions");St();My();var uGe=s(e=>e.replace(/^\s*%%(?!{)[^\n]+\n?/gm,"").trimStart(),"cleanupComments");hk();P2();function hGe(e){let t=e.match(uk);if(!t)return{text:e,metadata:{}};let r=t[1],n=r?t[2].split(` +`).map(o=>o.startsWith(r)?o.slice(r.length):o).join(` +`):t[2],i=af(n,{schema:nf})??{};i=typeof i=="object"&&!Array.isArray(i)?i:{};let a={};return i.displayMode&&(a.displayMode=i.displayMode.toString()),i.title&&(a.title=i.title.toString()),i.config&&(a.config=i.config),{text:e.slice(t[0].length),metadata:a}}s(hGe,"extractFrontMatter");er();var PMt=s(e=>e.replace(/\r\n?/g,` +`).replace(/<(\w+)([^>]*)>/g,(t,r,n)=>"<"+r+n.replace(/="([^"]*)"/g,"='$1'")+">"),"cleanupText"),OMt=s(e=>{let{text:t,metadata:r}=hGe(e),{displayMode:n,title:i,config:a={}}=r;return n&&(a.gantt||(a.gantt={}),a.gantt.displayMode=n),{title:i,config:a,text:t}},"processFrontmatter"),BMt=s(e=>{let t=or.detectInit(e)??{},r=or.detectDirective(e,"wrap");return Array.isArray(r)?t.wrap=r.some(({type:n})=>n==="wrap"):r?.type==="wrap"&&(t.wrap=!0),{text:pae(e),directive:t}},"processDirectives");function NX(e){let t=PMt(e),r=OMt(t),n=BMt(r.text),i=Xr(r.config,n.directive);return e=uGe(n.text),{code:e,title:r.title,config:i}}s(NX,"preprocessDiagram");Q7();Cw();er();function dGe(e){let t=new TextEncoder().encode(e),r=Array.from(t,n=>String.fromCodePoint(n)).join("");return btoa(r)}s(dGe,"toBase64");Sw();var $Mt=5e4,FMt="graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa",GMt="sandbox",zMt="loose",VMt="http://www.w3.org/2000/svg",WMt="http://www.w3.org/1999/xlink",qMt="http://www.w3.org/1999/xhtml",HMt="100%",UMt="100%",YMt="border:0;margin:0;",jMt="margin:0",XMt="allow-top-navigation-by-user-activation allow-popups",KMt='The "iframe" tag is not supported by your browser.',ZMt=["foreignobject"],QMt=["dominant-baseline"];function gGe(e){let t=NX(e);return Gx(),aZ(t.config??{}),t}s(gGe,"processAndSetConfigs");async function JMt(e,t){wx();try{let{code:r,config:n}=gGe(e);return{diagramType:(await yGe(r)).type,config:n}}catch(r){if(t?.suppressErrors)return!1;throw r}}s(JMt,"parse");var fGe=s((e,t,r=[])=>{let n=FD(`{ ${r.join(" !important; ")} !important; }`);return`.${e} ${t} ${n}`},"cssImportantStyles"),eNt=s((e,t=new Map)=>{let r=new CSSStyleSheet;if(e.fontFamily!==void 0&&r.insertRule(`:root { --mermaid-font-family: ${e.fontFamily}}`,r.cssRules.length),e.altFontFamily!==void 0&&r.insertRule(`:root { --mermaid-alt-font-family: ${e.altFontFamily}}`,r.cssRules.length),t instanceof Map){let l=ai(e)?["> *","span"]:["rect","polygon","ellipse","circle","path"];t.forEach(u=>{e4(u.styles)||l.forEach(h=>{r.insertRule(fGe(u.id,h,u.styles),r.cssRules.length)}),e4(u.textStyles)||r.insertRule(fGe(u.id,"tspan",(u?.textStyles||[]).map(h=>h.replace("color","fill"))),r.cssRules.length)})}let n="";if(e.themeCSS!==void 0)if(typeof r.replaceSync=="function"){let i=new CSSStyleSheet;i.replaceSync(e.themeCSS),n=Z7(i)+` +`}else n+=`${e.themeCSS} +`;return n+Z7(r)},"createCssStyles"),tNt=s((e,t)=>oD(nGe(`${e}{${t}}`),aGe([s(function(n,i,a,o){if(n.type==="rule"&&Array.isArray(n.props)){if(n.parent&&n.parent.type===cw)return;n.props=n.props.map(l=>l===e&&Array.isArray(n.children)&&n.children.every(h=>h.type!=="decl"?!1:new Set(["font-family","font-size","fill"]).has(h.props))||(l.startsWith(`${e} `)||l.startsWith(`${e}>`))&&!l.startsWith(`${e} ||`)?l:`${e} ${l}`)}else n.type.startsWith("@")&&([...[GFe,VFe,JL,qFe,"@container","@starting-style"],cw].includes(n.type)||(se.warn(`Removing unsupported at-rule ${n.type} from CSS`),n.type=kx))},"addNamespace"),iGe])),"compileCSS"),rNt=s((e,t,r,n)=>{let i=eNt(e,r),a=xJ(t,i,{...e.themeVariables,theme:e.theme,look:e.look},n);return tNt(n,a)},"createUserStyles"),nNt=s((e="",t,r)=>{let n=e;return!r&&!t&&(n=n.replace(/marker-end="url\([\d+./:=?A-Za-z-]*?#/g,'marker-end="url(#')),n=lo(n),n=n.replace(/
    /g,"
    "),n},"cleanUpSvgCode"),iNt=s((e="",t)=>{let r=t?.viewBox?.baseVal?.height?t.viewBox.baseVal.height+"px":UMt,n=dGe(`${e}`);return``},"putIntoIFrame"),pGe=s((e,t,r,n,i)=>{let a=e.append("div");a.attr("id",r),n&&a.attr("style",n);let o=a.append("svg").attr("id",t).attr("width","100%").attr("xmlns",VMt);return i&&o.attr("xmlns:xlink",i),o.append("g"),e},"appendDivSvgG");function mGe(e,t){return e.append("iframe").attr("id",t).attr("style","width: 100%; height: 100%;").attr("sandbox","")}s(mGe,"sandboxedIframe");var aNt=s((e,t,r,n)=>{e.getElementById(t)?.remove(),e.getElementById(r)?.remove(),e.getElementById(n)?.remove()},"removeExistingElements"),sNt=s(async function(e,t,r){wx();let n=gGe(t);t=n.code;let i=Lt();se.debug(i),t.length>(i?.maxTextSize??$Mt)&&(t=FMt);let a=`#${e}`,o="i"+e,l="#"+o,u="d"+e,h="#"+u,d=s(()=>{let I=ut(p?l:h).node();I&&"remove"in I&&I.remove()},"removeTempElements"),f=ut(document.body),p=i.securityLevel===GMt,m=i.securityLevel===zMt,g=i.fontFamily;if(r!==void 0){if(r&&(r.innerHTML=""),p){let A=mGe(ut(r),o);f=ut(A.nodes()[0].contentDocument.body),f.node().style.margin="0"}else f=ut(r);pGe(f,e,u,`font-family: ${g}`,WMt)}else{if(aNt(document,e,u,o),p){let A=mGe(ut(document.body),o);f=ut(A.nodes()[0].contentDocument.body),f.node().style.margin="0"}else f=ut("body");pGe(f,e,u)}let y,v;try{y=await Dx.fromText(t,{title:n.title})}catch(A){if(i.suppressErrorRendering)throw d(),A;y=await Dx.fromText("error"),v=A}let x=f.select(h).node(),b=y.type,T=x.firstChild,w=T.firstChild,C=y.renderer.getClasses?.(t,y),S=rNt(i,b,C,a),k=document.createElement("style");k.innerHTML=S,T.insertBefore(k,w);try{await y.renderer.draw(t,e,"11.17.2",y)}catch(A){throw i.suppressErrorRendering?d():SNe.draw(t,e,"11.17.2"),A}let R=f.select(`${h} svg`),M=y.db.getAccTitle?.(),P=y.db.getAccDescription?.();lNt(b,R,M,P);let _=s(()=>{f.select(`[id="${e}"]`).selectAll("foreignobject > *").attr("xmlns",qMt);let A=f.select(h).node().innerHTML;if(se.debug("config.arrowMarkerAbsolute",i.arrowMarkerAbsolute),A=nNt(A,p,Aa(i.arrowMarkerAbsolute)),p){let I=f.select(h+" svg").node();A=iNt(A,I)}else m||(A=ao.sanitize(A,{ADD_TAGS:ZMt,ADD_ATTR:QMt,HTML_INTEGRATION_POINTS:{foreignobject:!0}}));return cGe(),A},"serializeSvg")();if(v)throw v;return d(),{diagramType:b,svg:_,bindFunctions:y.db.bindFunctions}},"render");function oNt(e={}){let t=ii({},e);t?.fontFamily&&!t.themeVariables?.fontFamily&&(t.themeVariables||(t.themeVariables={}),t.themeVariables.fontFamily=t.fontFamily),rZ(t),t?.theme&&t.theme in dl?t.themeVariables=dl[t.theme].getThemeVariables(t.themeVariables):t&&(t.themeVariables=dl.default.getThemeVariables(t.themeVariables));let r=typeof t=="object"?GD(t):zD();Mx(r.logLevel),wx()}s(oNt,"initialize");var yGe=s((e,t={})=>{let{code:r}=NX(e);return Dx.fromText(r,t)},"getDiagramFromText");function lNt(e,t,r,n){sGe(t,e),oGe(t,r,n,t.attr("id"))}s(lNt,"addA11yInfo");var $p=Object.freeze({render:sNt,parse:JMt,getDiagramFromText:yGe,initialize:oNt,getConfig:Lt,setConfig:kw,getSiteConfig:zD,updateSiteConfig:nZ,reset:s(()=>{Gx()},"reset"),globalReset:s(()=>{Gx(wd)},"globalReset"),defaultConfig:wd});Mx(Lt().logLevel);Gx(Lt());ep();er();H3();var cNt=s((e,t,r)=>{se.warn(e),XN(e)?(r&&r(e.str,e.hash),t.push({...e,message:e.str,error:e})):(r&&r(e),e instanceof Error&&t.push({str:e.message,message:e.message,hash:e.name,error:e}))},"handleError"),vGe=s(async function(e={querySelector:".mermaid"}){try{await uNt(e)}catch(t){if(XN(t)&&se.error(t.str),Td.parseError&&Td.parseError(t),!e.suppressErrors)throw se.error("Use the suppressErrors option to suppress these errors"),t}},"run"),uNt=s(async function({postRenderCallback:e,querySelector:t,nodes:r}={querySelector:".mermaid"}){let n=$p.getConfig();se.debug(`${e?"":"No "}Callback function found`);let i;if(r)i=r;else if(t)i=document.querySelectorAll(t);else throw new Error("Nodes and querySelector are both undefined");se.debug(`Found ${i.length} diagrams`),n?.startOnLoad!==void 0&&(se.debug("Start On Load: "+n?.startOnLoad),$p.updateSiteConfig({startOnLoad:n?.startOnLoad}));let a=new or.InitIDGenerator(n.deterministicIds,n.deterministicIDSeed),o,l=[];for(let u of Array.from(i)){se.info("Rendering diagram: "+u.id);if(u.getAttribute("data-processed"))continue;u.setAttribute("data-processed","true");let h=`mermaid-${a.next()}`;o=u.innerHTML,o=ck(or.entityDecode(o)).trim().replace(//gi,"
    ");let d=or.detectInit(o);d&&se.debug("Detected early reinit: ",d);try{let{svg:f,bindFunctions:p}=await CGe(h,o,u);u.innerHTML=f,e&&await e(h),p&&p(u)}catch(f){cNt(f,l,Td.parseError)}}if(l.length>0)throw l[0]},"runThrowsErrors"),xGe=s(function(e){$p.initialize(e)},"initialize"),hNt=s(async function(e,t,r){se.warn("mermaid.init is deprecated. Please use run instead."),e&&xGe(e);let n={postRenderCallback:r,querySelector:".mermaid"};typeof t=="string"?n.querySelector=t:t&&(t instanceof HTMLElement?n.nodes=[t]:n.nodes=t),await vGe(n)},"init"),dNt=s(async(e,{lazyLoad:t=!0}={})=>{wx(),tb(...e),t===!1&&await FFe()},"registerExternalDiagrams"),bGe=s(function(){if(Td.startOnLoad){let{startOnLoad:e}=$p.getConfig();e&&Td.run().catch(t=>se.error("Mermaid failed to initialize",t))}},"contentLoaded");if(typeof document<"u"){window.addEventListener("load",bGe,!1)}var fNt=s(function(e){Td.parseError=e},"setParseErrorHandler"),lD=[],PX=!1,TGe=s(async()=>{if(!PX){for(PX=!0;lD.length>0;){let e=lD.shift();if(e)try{await e()}catch(t){se.error("Error executing queue",t)}}PX=!1}},"executeQueue"),pNt=s(async(e,t)=>new Promise((r,n)=>{let i=s(()=>new Promise((a,o)=>{$p.parse(e,t).then(l=>{a(l),r(l)},l=>{se.error("Error parsing",l),Td.parseError?.(l),o(l),n(l)})}),"performCall");lD.push(i),TGe().catch(n)}),"parse"),CGe=s((e,t,r)=>new Promise((n,i)=>{let a=s(()=>new Promise((o,l)=>{$p.render(e,t,r).then(u=>{o(u),n(u)},u=>{se.error("Error parsing",u),Td.parseError?.(u),l(u),i(u)})}),"performCall");lD.push(a),TGe().catch(i)}),"render"),mNt=s(()=>Object.keys(ch).map(e=>({id:e})),"getRegisteredDiagramsMetadata"),Td={startOnLoad:!0,mermaidAPI:$p,parse:pNt,render:CGe,init:hNt,run:vGe,registerExternalDiagrams:dNt,registerLayoutLoaders:eG,initialize:xGe,parseError:void 0,contentLoaded:bGe,setParseErrorHandler:fNt,detectType:ry,registerIconPacks:ey,getRegisteredDiagramsMetadata:mNt},gNt=Td;return RGe(yNt);})(); +/*! Bundled license information: + +lodash-es/lodash.js: + (** + * @license + * Lodash (Custom Build) + * Build: `lodash modularize exports="es" -o ./` + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + *) +*/ +/*! Check if previously processed */ +/*! + * Wait for document loaded before starting the execution + */ +/*! Bundled license information: + +dompurify/dist/purify.es.mjs: + (*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE *) + +lodash-es/lodash.js: + (** + * @license + * Lodash (Custom Build) + * Build: `lodash modularize exports="es" --repo lodash/lodash#4.18.1 -o ./` + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + *) + +cytoscape/dist/cytoscape.esm.mjs: + (*! + Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable + Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com) + Licensed under The MIT License (http://opensource.org/licenses/MIT) + *) + (*! + Event object based on jQuery events, MIT license + + https://jquery.org/license/ + https://tldrlegal.com/license/mit-license + https://github.com/jquery/jquery/blob/master/src/event.js + *) + (*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License *) + (*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License *) +*/ +globalThis["mermaid"] = globalThis.__esbuild_esm_mermaid_nm["mermaid"].default; diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/AppContainer.kt b/android/app/src/main/java/io/github/kahme247/ompweb/AppContainer.kt new file mode 100644 index 000000000..e7781ed97 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/AppContainer.kt @@ -0,0 +1,713 @@ +package io.github.kahme247.ompweb + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.Uri +import androidx.core.content.pm.PackageInfoCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import io.github.kahme247.ompweb.data.ComposerDraftStore +import io.github.kahme247.ompweb.data.DraftStore +import io.github.kahme247.ompweb.data.EncryptedCookieRecords +import io.github.kahme247.ompweb.data.KeystoreCookieCipher +import io.github.kahme247.ompweb.data.OriginBoundCookieJar +import io.github.kahme247.ompweb.data.OriginScopedDraftStore +import io.github.kahme247.ompweb.data.SessionCacheStore +import io.github.kahme247.ompweb.data.SettingsStore +import io.github.kahme247.ompweb.events.EventMux +import io.github.kahme247.ompweb.events.HttpEventTransport +import io.github.kahme247.ompweb.net.ApiClient +import io.github.kahme247.ompweb.net.AppInfo +import io.github.kahme247.ompweb.net.AgentExtensionApi +import io.github.kahme247.ompweb.net.HttpAndroidReleaseSource +import io.github.kahme247.ompweb.net.HttpAndroidUpdateDownloader +import io.github.kahme247.ompweb.net.HttpAndroidUpdateSource +import io.github.kahme247.ompweb.net.HttpMcpApi +import io.github.kahme247.ompweb.net.HttpNativeSettingsApi +import io.github.kahme247.ompweb.net.HttpNotificationHistoryApi +import io.github.kahme247.ompweb.net.HttpNotificationsApi +import io.github.kahme247.ompweb.net.HttpPluginsApi +import io.github.kahme247.ompweb.net.HttpSkillsApi +import io.github.kahme247.ompweb.net.HttpPolicyInterceptor +import io.github.kahme247.ompweb.net.HttpSttApi +import io.github.kahme247.ompweb.net.NotificationsApi +import io.github.kahme247.ompweb.net.ServerAddress +import io.github.kahme247.ompweb.net.ServerProbe +import io.github.kahme247.ompweb.net.btwApi +import io.github.kahme247.ompweb.net.filesApi +import io.github.kahme247.ompweb.net.gitApi +import io.github.kahme247.ompweb.net.systemApi +import io.github.kahme247.ompweb.net.worktreesApi +import io.github.kahme247.ompweb.notify.NotificationCenter +import io.github.kahme247.ompweb.ui.AppViewModel +import io.github.kahme247.ompweb.ui.chat.ChatViewModel +import io.github.kahme247.ompweb.ui.chat.SpeechController +import io.github.kahme247.ompweb.ui.hosttools.HostToolsController +import io.github.kahme247.ompweb.ui.panels.btw.BtwViewModel +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionStrings +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionUiController +import io.github.kahme247.ompweb.ui.composer.AndroidCompletionSound +import io.github.kahme247.ompweb.ui.composer.ComposerStrings +import io.github.kahme247.ompweb.ui.composer.controls.ComposerControlsViewModel +import io.github.kahme247.ompweb.ui.composer.controls.WordPredictor +import io.github.kahme247.ompweb.ui.composer.controls.wordInputCapabilities +import io.github.kahme247.ompweb.ui.composer.dictation.AndroidDictationStore +import io.github.kahme247.ompweb.ui.composer.dictation.AndroidOnDeviceStt +import io.github.kahme247.ompweb.ui.composer.dictation.DictationStrings +import io.github.kahme247.ompweb.ui.composer.dictation.DictationViewModel +import io.github.kahme247.ompweb.ui.connect.ConnectViewModel +import io.github.kahme247.ompweb.ui.files.FilesViewModel +import io.github.kahme247.ompweb.ui.files.PreferencesFileTabStore +import io.github.kahme247.ompweb.ui.git.GitChangesViewModel +import io.github.kahme247.ompweb.ui.render.RenderEnvironment +import io.github.kahme247.ompweb.ui.sessions.DirectoryPickerViewModel +import io.github.kahme247.ompweb.ui.sessions.NewSessionViewModel +import io.github.kahme247.ompweb.ui.sessions.SessionExport +import io.github.kahme247.ompweb.ui.sessions.SessionsViewModel +import io.github.kahme247.ompweb.ui.settings.AndroidUpdateController +import io.github.kahme247.ompweb.ui.settings.NativeSettingsViewModel +import io.github.kahme247.ompweb.ui.settings.PackageInstallerAndroidApkInstaller +import io.github.kahme247.ompweb.ui.settings.PackageManagerAndroidApkInspector +import io.github.kahme247.ompweb.ui.settings.SettingsAgentsHostViewModel +import io.github.kahme247.ompweb.ui.settings.SettingsHostViewModel +import io.github.kahme247.ompweb.ui.settings.SettingsProvidersHostViewModel +import io.github.kahme247.ompweb.ui.settings.SettingsViewModel +import io.github.kahme247.ompweb.ui.worktrees.WorktreeCache +import io.github.kahme247.ompweb.ui.worktrees.WorktreeStrings +import io.github.kahme247.ompweb.ui.worktrees.WorktreesViewModel +import java.io.File +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient + +/** + * A notification tap that asked for a session, plus the installation it was + * posted for (`scheme://host:port`, null when not configured yet). + */ +data class PendingSessionOpen(val sessionId: String, val origin: String?) + +/** + * Manual dependency graph for the single-module app. + * + * There is exactly one [OkHttpClient] and one [EventMux] per process: the SSE + * stream is shared by every screen, and the cookie jar has to see every + * request to bind the session to the configured origin. + */ +class AppContainer(context: Context) { + + private val appContext = context.applicationContext + + val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + /** The installed app's `versionName` (intro line of the new-session pane). */ + val appVersion: String = runCatching { + appContext.packageManager.getPackageInfo(appContext.packageName, 0).versionName + }.getOrNull().orEmpty().ifBlank { "—" } + + /** The installed `versionCode`; Android decides installability by it. */ + val appVersionCode: Long = runCatching { + PackageInfoCompat.getLongVersionCode( + appContext.packageManager.getPackageInfo(appContext.packageName, 0), + ) + }.getOrDefault(0L) + + private val _serverInfo = MutableStateFlow(null) + + /** + * `GET /api/app-info` of the configured server: version for display and + * the capabilities the screens gate on (`messageSearch`). + */ + val serverInfo: StateFlow = _serverInfo.asStateFlow() + + /** `version` of the configured server, once known. */ + val serverVersion: StateFlow = _serverInfo + .map { info -> info?.version?.takeIf { it.isNotBlank() } } + .stateIn(appScope, SharingStarted.Eagerly, null) + + private var serverInfoRequested = false + + /** + * Loads the server version and capabilities once. The shell calls this + * when it becomes visible; a failure keeps the version line empty and the + * capability-gated features off instead of surfacing an error. + */ + fun refreshServerInfo() { + if (serverInfoRequested || _serverInfo.value != null) return + serverInfoRequested = true + appScope.launch { + runCatching { api.appInfo() } + .onSuccess { info -> _serverInfo.value = info } + serverInfoRequested = false + } + } + + val settings = SettingsStore(appContext, appScope) + + /** + * The app's own update source. It talks to GitHub or the user's custom + * feed, never to the configured server, so it gets a client without the + * session cookie jar. The plain-HTTP policy is enforced on every hop; + * release assets may redirect to a download host, which is why + * cross-origin redirects are allowed for this cookie-free client. + */ + private val updateClient: OkHttpClient = HttpPolicyInterceptor.install( + OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .callTimeout(0, TimeUnit.SECONDS), + allowCrossOriginRedirects = true, + ).build() + + /** + * The installed app's own update check and install. Downloads go to the + * app cache and are verified against the feed and the installed signing + * certificate before PackageInstaller sees them. + */ + val androidUpdates = AndroidUpdateController( + source = HttpAndroidUpdateSource(HttpAndroidReleaseSource(updateClient), updateClient), + settings = settings, + scope = appScope, + currentVersion = appVersion, + installedPackageName = appContext.packageName, + currentVersionCode = appVersionCode, + downloader = HttpAndroidUpdateDownloader( + client = updateClient, + cacheDir = File(appContext.cacheDir, "updates"), + ), + inspector = PackageManagerAndroidApkInspector(appContext), + installer = PackageInstallerAndroidApkInstaller(appContext), + ) + + private val cookieCipher = KeystoreCookieCipher() + + private val cookieRecords = EncryptedCookieRecords( + file = File(appContext.filesDir, "session-cookies.bin"), + cipher = cookieCipher, + ) + + val cookieJar = OriginBoundCookieJar( + configuredOrigin = { settings.originNow()?.origin?.toHttpUrlOrNull() }, + records = cookieRecords, + ) + + private val unauthorized = MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + /** Fires with the origin that answered 401, from any screen. */ + val authEvents: SharedFlow = unauthorized.asSharedFlow() + + private val _sessionEpoch = MutableStateFlow(0) + + /** + * Bumped whenever the signed-in user's data is dropped (sign-out, server + * switch, 401). Settings-scoped view models clear their state on a change, + * so no models.yml/API key survives into the next installation. + */ + val sessionEpoch: StateFlow = _sessionEpoch.asStateFlow() + + private val httpClient: OkHttpClient = HttpPolicyInterceptor.install( + OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .cookieJar(cookieJar), + ).build() + + val api = ApiClient( + client = httpClient, + origin = { settings.originNow() }, + onUnauthorized = { origin -> unauthorized.tryEmit(origin) }, + ) + + /** + * A derived client (shares pool, dispatcher and cookie jar) for the probes: + * the jar makes the post-sign-in `/api/app-info` check authenticated, the + * derived client refuses redirects and gives up after eight seconds. + */ + private val probeClient: OkHttpClient = httpClient.newBuilder() + .followRedirects(false) + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .callTimeout(8, TimeUnit.SECONDS) + .build() + + val probe = ServerProbe(probeClient) + + val eventMux = EventMux( + transport = HttpEventTransport(api, appScope), + scope = appScope, + // Monotonic and sleep-inclusive: a wall-clock jump must not stall the + // network-triggered reconnect guard. + clock = { android.os.SystemClock.elapsedRealtime() }, + ) + + /** + * Cache-first storage for the workspace list and the last opened + * transcripts. It lives in `filesDir` (never in the backup set) and is + * deleted by [clearLocalUserData] on logout, a server switch and a 401. + */ + val sessionCache = SessionCacheStore(File(appContext.filesDir, SESSION_CACHE_DIR)) + + private var networkCallback: ConnectivityManager.NetworkCallback? = null + + /** + * One app-wide connectivity callback. A network coming back (Wi-Fi after + * mobile data, Tailscale roaming) reconnects the shared event stream right + * away instead of leaving a half-open socket until its read timeout, which + * is what made the app show the previous state for up to two minutes after + * a network change. `EventMux.onNetworkAvailable` rate-limits the retry, so + * an `onAvailable` burst cannot become a reconnect storm, and the + * notification service no longer registers a callback of its own — that + * one existed only while its foreground service ran. + */ + private fun registerNetworkCallback() { + val manager = appContext.getSystemService(ConnectivityManager::class.java) ?: return + val callback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + eventMux.onNetworkAvailable() + } + } + runCatching { manager.registerDefaultNetworkCallback(callback) } + .onSuccess { networkCallback = callback } + } + + val notificationsApi: NotificationsApi = HttpNotificationsApi(api, settings::originNow) + + /** + * Notification identity, registration, presence and the foreground + * service. `clearLocalUserData()` forgets the device; the shell reports + * the visible session and the process lifecycle reports foreground. + */ + val notifications = NotificationCenter( + context = appContext, + settings = settings, + api = notificationsApi, + mux = eventMux, + scope = appScope, + isSignedIn = { + val snapshot = settings.snapshot.value + snapshot.origin != null && (!snapshot.needsPassword || cookieJar.hasCookies()) + }, + ) + + /** App-wide read-aloud engine; binds TTS lazily on first use. */ + val speech = SpeechController(appContext) + + /** + * Composer drafts keyed by session id or `new:` (survives process + * death). Keys are scoped to the configured origin, so a server switch can + * never show another installation's recovered queue text. + */ + private val rawDrafts = DraftStore(appContext, appScope) + val draftStore: ComposerDraftStore = OriginScopedDraftStore( + rawDrafts, + appScope, + settings.snapshot + .map { snapshot -> snapshot.origin?.origin } + .stateIn(appScope, SharingStarted.Eagerly, settings.snapshot.value.origin?.origin), + ) + + /** Localized composer labels for the view models (no `Context` there). */ + val composerStrings = ComposerStrings.from(appContext) + + private val sttApi = HttpSttApi(api) + private val dictationStore = AndroidDictationStore(appContext) + + /** Slice B controls for the chat composer; `open()` rebinds it per session. */ + val chatControls = ComposerControlsViewModel(api, settings, appScope) + + /** Slice C dictation for the chat composer; owned by the process so jobs adopt. */ + val chatDictation = DictationViewModel( + sttApi, + dictationStore, + appScope, + DictationStrings.from(appContext), + settings = settings.snapshot, + onDevice = AndroidOnDeviceStt(appContext), + ) + + val wordPredictor = WordPredictor( + api = api, + settings = settings, + scope = appScope, + capabilities = { wordInputCapabilities(appContext) }, + ) + + private val completionSound = AndroidCompletionSound() + + /** Slice C's localized labels, shared by its two controllers. */ + private val extensionStrings = ExtensionStrings.from(appContext) + + /** Slice B: side questions for the open chat (`bind` on session open). */ + val chatBtw = BtwViewModel(api.btwApi(), appScope) + + /** Slice C: extension dialogs/status/terminal for the open chat. */ + val chatExtensionUi = ExtensionUiController( + api = AgentExtensionApi(api), + scope = appScope, + strings = extensionStrings, + ) + + /** + * Slice C's host-tool bridge. App-wide on purpose: `host_tool_call` frames + * arrive on the shared `host-tools` channel for every session, so the + * consent prompt must work while no chat is open. Started by the signed-in + * shell, not before. + */ + val hostTools = HostToolsController( + api = AgentExtensionApi(api), + events = eventMux, + settings = settings, + scope = appScope, + strings = extensionStrings, + ) + + /** A separate controls instance: a new session has its own spawn choices. */ + val newSessionControls = ComposerControlsViewModel(api, settings, appScope) + val newSessionDictation = DictationViewModel( + sttApi, + dictationStore, + appScope, + DictationStrings.from(appContext), + settings = settings.snapshot, + onDevice = AndroidOnDeviceStt(appContext), + ) + + /** + * URIs a share sheet handed us; the sessions screen consumes them into a + * chooser and then into a draft. Held here because the activity may be + * recreated before the screen composes. + */ + private val _pendingShare = MutableStateFlow>(emptyList()) + val pendingShare = _pendingShare.asStateFlow() + + fun offerShare(uris: List) { + if (uris.isNotEmpty()) _pendingShare.value = uris + } + + fun consumeShare(): List { + val uris = _pendingShare.value + if (uris.isNotEmpty()) _pendingShare.value = emptyList() + return uris + } + + /** + * A notification tap (PendingIntent) or a deep link asked for a session. + * Held here because the activity may be recreated before the shell is + * composed. + */ + private val _pendingSessionOpen = MutableStateFlow(null) + val pendingSessionOpen = _pendingSessionOpen.asStateFlow() + + /** A `ompweb://cwd?path=…` link; the installation it was minted for, if named. */ + private val _pendingCwdOpen = MutableStateFlow(null) + val pendingCwdOpen: StateFlow = _pendingCwdOpen.asStateFlow() + + fun offerSessionOpen(sessionId: String, origin: String?) { + if (sessionId.isNotBlank()) _pendingSessionOpen.value = PendingSessionOpen(sessionId, origin) + } + + /** + * A workspace deep link (`ompweb://cwd?path=…`). The path is validated by + * the new-session pane's `prepare` against the configured server, so a + * dead path shows its error there instead of opening a broken composer. + */ + fun offerCwdOpen(path: String, origin: String?) { + if (path.isNotBlank()) _pendingCwdOpen.value = PendingSessionOpen(path, origin) + } + + /** + * The requested session, but only when it still belongs to the configured + * installation — a tap on a notification of a server the user left must + * not open (and scroll into) a same-id session of the new one. A + * mismatching request is consumed without effect. + * + * A link that names no origin (the plain `ompweb://session/` share + * link) is bound at consumption time: it is accepted only while a server + * is configured, because the id is meaningless without one. + */ + fun consumeSessionOpen(): String? { + val pending = _pendingSessionOpen.value ?: return null + _pendingSessionOpen.value = null + val configured = settings.originNow()?.origin + val matches = pending.origin?.let { it == configured } ?: (configured != null) + return pending.sessionId.takeIf { matches } + } + + /** The requested workspace path, with the same origin rule as sessions. */ + fun consumeCwdOpen(): String? { + val pending = _pendingCwdOpen.value ?: return null + _pendingCwdOpen.value = null + val configured = settings.originNow()?.origin + val matches = pending.origin?.let { it == configured } ?: (configured != null) + return pending.sessionId.takeIf { matches } + } + + /** + * Drop everything that belongs to the signed-in user of the current + * origin: unsent drafts (and their recovered queue text), share payloads, + * local recordings/playback copies, finished exports and camera staging. + * Called on logout, a server switch and a 401, so nothing of one + * installation survives into another. + * + * Suspends until the notification teardown has unregistered the device: + * the caller may clear the cookie and the origin only afterwards. + */ + suspend fun clearLocalUserData() { + // Unregister before the cookie/origin disappear: the server should stop + // routing to this device, and the identity is forgotten afterwards. + notifications.clearDevice() + // The list belongs to the old installation; a load still in flight + // must not land in the next one. + notificationHistoryViewModel.reset() + // Files: tabs, explorer and cached worktree lists are installation state. + filesViewModel.reset() + worktreeCache.invalidate() + worktreesViewModel.load(null) + gitChangesViewModel.bind(null) + _pendingSessionOpen.value = null + _pendingCwdOpen.value = null + _serverInfo.value = null + serverInfoRequested = false + hostTools.reset() + draftStore.clearAll() + // The list and the transcripts belong to the installation that is being + // left: nothing of it may paint the next server's workspace. + withContext(Dispatchers.IO) { sessionCache.clearAll() } + // Same rule for the heap: the list/ETag per origin and the shared detail + // answers (whole transcripts) must not outlive the account either. + api.clearMemoryCaches() + _pendingShare.value = emptyList() + chatDictation.clearSessionData() + newSessionDictation.clearSessionData() + dictationStore.purgeOrphans() + SessionExport.purgeStaleExports(appContext) + File(appContext.cacheDir, "camera").deleteRecursively() + // Settings view models hold the previous installation's models.yml. + _sessionEpoch.update { it + 1 } + } + + /** + * Slice B's history mirror; process-scoped like its API, and fed live by + * [NotificationCenter.onDelivered]. + */ + val notificationHistoryViewModel = io.github.kahme247.ompweb.ui.notifications.NotificationHistoryViewModel( + HttpNotificationHistoryApi(api), + appScope, + ) + + /** Slice B: worktree lists, shared by the new-session pane and the Files panel. */ + val worktreeCache = WorktreeCache(api.worktreesApi()) + + /** Slice B: the Files panel's Git tab, bound to the explorer cwd. */ + val gitChangesViewModel = GitChangesViewModel(api.gitApi(), appScope) + + /** Slice B: the Files panel's Worktrees tab. */ + val worktreesViewModel = WorktreesViewModel( + api = api.worktreesApi(), + scope = appScope, + cache = worktreeCache, + strings = WorktreeStrings.from(appContext), + ) + + /** + * Phase 9's Files panel: explorer, tabs, viewer and live file events. + * Process-scoped so open tabs survive rotation; the tab paths are restored + * by [PreferencesFileTabStore] after process death. + */ + val filesViewModel = FilesViewModel( + api = api.filesApi(), + git = api.gitApi(), + events = eventMux, + scope = appScope, + tabStore = PreferencesFileTabStore(appContext), + cacheDir = appContext.cacheDir, + origin = { settings.snapshot.value.origin?.origin }, + awaitOrigin = { + settings.awaitLoaded() + settings.snapshot.value.origin?.origin + }, + ) + + val appViewModelFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { AppViewModel(this@AppContainer) } + } + + val connectViewModelFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { ConnectViewModel(probe) } + } + + val sessionsViewModelFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { + SessionsViewModel( + api, + eventMux, + settings, + settings::originNow, + serverInfo, + cache = sessionCache, + ) + } + } + + val newSessionViewModelFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { + NewSessionViewModel( + api = api, + drafts = draftStore, + composerStrings = composerStrings, + controls = newSessionControls, + dictation = newSessionDictation, + worktreeCache = worktreeCache, + ) + } + } + + val directoryPickerViewModelFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { DirectoryPickerViewModel(api) } + } + + /** Settings → System & Updates, Usage and provider limits. */ + val settingsViewModelFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { SettingsViewModel(api.systemApi(), settings) } + } + + /** Owns the store of the settings-scoped view models (see SettingsHostViewModel). */ + val settingsHostViewModelFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { SettingsHostViewModel(this@AppContainer) } + } + + val settingsProvidersHostFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { SettingsProvidersHostViewModel(this@AppContainer) } + } + + val settingsAgentsHostFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { SettingsAgentsHostViewModel(this@AppContainer) } + } + + /** Native OMP settings, MCP, Skills and Plugins (Slice B controllers). */ + val nativeSettingsViewModelFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { + NativeSettingsViewModel( + settingsApi = HttpNativeSettingsApi(api), + mcpApi = HttpMcpApi(api), + skillsApi = HttpSkillsApi(api), + pluginsApi = HttpPluginsApi(api), + ) + } + } + + val chatViewModelFactory: ViewModelProvider.Factory = viewModelFactory { + initializer { + ChatViewModel( + api, + eventMux, + settings, + speech, + createSavedStateHandle(), + appContext.getString(R.string.chat_prompt_maybe_started), + appContext.getString(R.string.chat_stop_unconfirmed), + draftStore, + composerStrings, + chatControls, + chatDictation, + wordPredictor, + completionSound, + chatBtw, + chatExtensionUi, + hostTools, + files = api.filesApi(), + skillDiagnostics = api, + transcripts = sessionCache, + // Sign-out/switch/401 wipes the disk cache; the same bump drops + // the transcript this activity-scoped view model holds. + sessionEpoch = sessionEpoch, + ) + } + } + + init { + // A network change must reach the shared event stream even when the + // notification service is not running (it used to be the only + // registrar, so the UI alone waited out the read timeout). + registerNetworkCallback() + // The render layer resolves relative media URLs and loads images with + // the same authenticated client as the chat transport. + RenderEnvironment.configure( + baseUrlProvider = { settings.originNow()?.origin }, + httpClient = httpClient, + ) + // Exports are one-shot share payloads; anything still in the cache was + // left by an interrupted share or a process death. + SessionExport.purgeStaleExports(appContext) + // Recordings/playback downloads from a dead process are unreachable; + // the kept recording (retry fallback) survives. + dictationStore.purgeOrphans() + ProcessLifecycleOwner.get().lifecycle.addObserver( + object : DefaultLifecycleObserver { + /** False until the cold start has been seen; see [onStart]. */ + private var foregrounded = false + + override fun onStart(owner: LifecycleOwner) { + notifications.setAppForeground(true) + eventMux.onForeground() + // Every return to the foreground continues an install that + // stopped at NeedsPermission — the shell banner has no + // screen-level resume hook, so coming back from Android's + // "install unknown apps" page used to leave it stuck there. + // Harmless in every other state. + androidUpdates.resumeInstallAfterPermission() + // Android keeps the process alive for days, so a check on + // start alone would rarely run again. Every later return + // to the foreground rechecks, and the controller bypasses + // its six-hour cache once the last answer is older than + // 30 minutes - otherwise a published release could stay + // unseen for most of the day. Inside that window the + // cached answer is reused, so app switching stays offline. + // The cold start is skipped: the shell staggers its own + // check until content is on screen, and the stored source + // is seeded by then. + if (foregrounded) { + androidUpdates.checkOnForeground() + } else { + foregrounded = true + } + } + + override fun onStop(owner: LifecycleOwner) { + notifications.setAppForeground(false) + eventMux.onBackground() + } + }, + ) + } + + private companion object { + /** `filesDir` subdirectory of the cache-first payloads. */ + const val SESSION_CACHE_DIR = "session-cache" + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/MainActivity.kt b/android/app/src/main/java/io/github/kahme247/ompweb/MainActivity.kt new file mode 100644 index 000000000..90a43a55a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/MainActivity.kt @@ -0,0 +1,170 @@ +package io.github.kahme247.ompweb + +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import io.github.kahme247.ompweb.notify.NotificationIntents +import io.github.kahme247.ompweb.notify.NotificationOpenToken +import io.github.kahme247.ompweb.ui.OmpWebRoot + +/** + * Single activity. Besides the launcher entry it is a share target + * (`ACTION_SEND`/`ACTION_SEND_MULTIPLE`): the incoming stream URIs land in the + * container and the sessions screen asks which chat should receive them. + */ +class MainActivity : ComponentActivity() { + + private lateinit var container: AppContainer + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + super.onCreate(savedInstanceState) + enableEdgeToEdge() + container = (application as OmpWebApplication).container + offerShare(intent) + offerSession(intent) + offerLink(intent) + setContent { + OmpWebRoot(container) + } + } + + /** `singleTop`: sharing from another app reuses this task instead of stacking. */ + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + offerShare(intent) + offerSession(intent) + offerLink(intent) + } + + /** + * A VIEW intent for `ompweb://session/` or `ompweb://cwd?path=…`. + * + * Any app may send these (that is what a share link is for), so the only + * trust anchor is the optional `origin` query parameter: a link minted for + * another server is consumed without effect by the container. A link + * without an origin is bound to the configured server when it is + * consumed. The data is cleared afterwards so a recreation (rotation) does + * not open it twice. + */ + private fun offerLink(source: Intent?) { + if (source?.action != Intent.ACTION_VIEW) return + val link = parseShellLink(source.data) ?: return + source.data = null + when (link) { + is ShellLink.Session -> container.offerSessionOpen(link.sessionId, link.origin) + is ShellLink.Workspace -> container.offerCwdOpen(link.path, link.origin) + } + } + + /** + * A notification tap asks for one session. The activity is exported (share + * target), so an explicit intent with a session id from any app must not + * force a chat open: only the HMAC token of our own PendingIntent is + * accepted. The extras are removed so a recreation (rotation) does not + * reopen the session. + */ + private fun offerSession(source: Intent?) { + val sessionId = source?.getStringExtra(NotificationIntents.EXTRA_SESSION_ID)?.takeIf { it.isNotBlank() } + ?: return + val token = source.getStringExtra(NotificationIntents.EXTRA_OPEN_TOKEN) + val origin = source.getStringExtra(NotificationIntents.EXTRA_ORIGIN) + source.removeExtra(NotificationIntents.EXTRA_SESSION_ID) + source.removeExtra(NotificationIntents.EXTRA_OPEN_TOKEN) + source.removeExtra(NotificationIntents.EXTRA_ORIGIN) + if (!NotificationOpenToken.verify(this, sessionId, token)) return + container.offerSessionOpen(sessionId, origin) + } + + /** + * Hand the share payload to the container once, then strip it from the + * intent: the same intent is redelivered when the activity is recreated + * (rotation), and offering it again would attach the files twice. + */ + private fun offerShare(source: Intent?) { + val uris = streamUris(source) + if (uris.isEmpty()) return + container.offerShare(uris) + source?.apply { + action = null + clipData = null + @Suppress("DEPRECATION") + removeExtra(Intent.EXTRA_STREAM) + } + } + + private fun streamUris(intent: Intent?): List { + if (intent == null) return emptyList() + // Only share actions carry streams; a launcher intent with stale clip + // data must not produce attachments. + if (intent.action != Intent.ACTION_SEND && intent.action != Intent.ACTION_SEND_MULTIPLE) { + return emptyList() + } + @Suppress("DEPRECATION") + val fromExtra: List = when (intent.action) { + Intent.ACTION_SEND -> listOfNotNull(intent.getParcelableExtra(Intent.EXTRA_STREAM)) + else -> intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM).orEmpty() + } + val candidates = fromExtra.ifEmpty { + // Some apps only fill the clip data. + val clip = intent.clipData ?: return emptyList() + (0 until clip.itemCount).mapNotNull { clip.getItemAt(it).uri } + } + // `file://` URIs would bypass the content resolver's grants and make + // the app read arbitrary paths; accept only provider content. + return candidates.filter { uri -> + uri.scheme?.equals(ContentResolver.SCHEME_CONTENT, ignoreCase = true) == true + } + } +} + +/** The scheme the app registers for share links (`AndroidManifest.xml`). */ +internal const val LINK_SCHEME = "ompweb" + +/** What a VIEW intent asks the app to open. */ +internal sealed interface ShellLink { + data class Session(val sessionId: String, val origin: String?) : ShellLink + + data class Workspace(val path: String, val origin: String?) : ShellLink +} + +/** Session ids are omp's file stems; anything else cannot be a session. */ +private val SESSION_ID_PATTERN = Regex("[A-Za-z0-9._-]{1,128}") + +/** A workspace path from a link is bounded like a `cwd` argument. */ +private const val MAX_LINK_PATH_LENGTH = 4096 + +/** + * Parses `ompweb://session/[?origin=…]` and + * `ompweb://cwd?path=[&origin=…]`. Returns null for every other URI, so + * a malformed link is ignored instead of opening an arbitrary view. + */ +internal fun parseShellLink(data: Uri?): ShellLink? { + if (data == null) return null + if (!data.scheme.equals(LINK_SCHEME, ignoreCase = true)) return null + val origin = data.getQueryParameter("origin")?.takeIf { it.isNotBlank() } + return when (data.host?.lowercase()) { + "session" -> { + val id = data.pathSegments.firstOrNull { it.isNotBlank() } + ?: data.getQueryParameter("id") + id?.takeIf { SESSION_ID_PATTERN.matches(it) }?.let { ShellLink.Session(it, origin) } + } + "cwd" -> { + val fromQuery = data.getQueryParameter("path") + val fromPath = data.pathSegments + .filter { it.isNotBlank() } + .joinToString("/") + .let { if (it.isEmpty()) null else "/$it" } + (fromQuery ?: fromPath) + ?.takeIf { it.isNotBlank() && it.length <= MAX_LINK_PATH_LENGTH } + ?.let { ShellLink.Workspace(it, origin) } + } + else -> null + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/OmpWebApplication.kt b/android/app/src/main/java/io/github/kahme247/ompweb/OmpWebApplication.kt new file mode 100644 index 000000000..aab746322 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/OmpWebApplication.kt @@ -0,0 +1,14 @@ +package io.github.kahme247.ompweb + +import android.app.Application + +class OmpWebApplication : Application() { + + lateinit var container: AppContainer + private set + + override fun onCreate() { + super.onCreate() + container = AppContainer(this) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/BranchTree.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/BranchTree.kt new file mode 100644 index 000000000..be583ddbe --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/BranchTree.kt @@ -0,0 +1,143 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull + +/** One projected branch node of `GET /api/sessions/{id}` (`tree`). */ +data class BranchNode( + val id: String, + val type: String, + val role: String?, + val text: String?, + val children: List, + val compressedEntryIds: List, + val previewRole: String?, + val previewText: String?, +) + +/** One rendered row of the branch navigator (single-child chains compressed). */ +data class BranchRow( + val selectId: String, + val label: String, + val role: String?, + val depth: Int, + val isLast: Boolean, + val parentLines: List, + val skipped: Int, + val active: Boolean, + val onPath: Boolean, + val children: List, +) + +/** + * Server-side branch tree parsing and compression (web: + * `components/BranchNavigator.tsx`). omp accepts a leaf entry id; the + * navigator therefore selects the deepest node of a compressed chain. + */ +object BranchTree { + + fun parse(nodes: List): List = nodes.mapNotNull(::parseNode) + + private fun parseNode(node: JsonObject): BranchNode? { + val entry = node.obj("entry") ?: return null + val id = entry.string("id") ?: return null + val type = entry.string("type") ?: "" + val message = entry.obj("message") + val preview = node.obj("branchPreview") + return BranchNode( + id = id, + type = type, + role = message?.string("role"), + text = message?.let { contentText(it["content"]) }, + children = (node.array("children") ?: JsonArray(emptyList())).mapNotNull { parseNode(it as? JsonObject ?: return@mapNotNull null) }, + compressedEntryIds = (node.array("compressedEntryIds") ?: JsonArray(emptyList())) + .mapNotNull { (it as? kotlinx.serialization.json.JsonPrimitive)?.contentOrNull }, + previewRole = preview?.string("role"), + previewText = preview?.string("text"), + ) + } + + fun hasBranch(nodes: List): Boolean { + if (nodes.size > 1) return true + for (node in nodes) { + if (node.children.size > 1) return true + if (hasBranch(node.children)) return true + } + return false + } + + fun activePathIds(nodes: List, targetId: String?): Set { + if (targetId == null) return emptySet() + fun search(current: List, path: List): List? { + for (node in current) { + val next = path + node.id + if (node.id == targetId || targetId in node.compressedEntryIds) return next + search(node.children, next)?.let { return it } + } + return null + } + return search(nodes, emptyList())?.toSet() ?: emptySet() + } + + fun rows(tree: List, activeLeafId: String?): List { + val active = activePathIds(tree, activeLeafId) + fun build(nodes: List, depth: Int, parentLines: List): List { + return nodes.flatMapIndexed { index, node -> + val last = index == nodes.lastIndex + val compressed = compress(node) + val row = BranchRow( + selectId = compressed.representative.id, + label = compressed.label, + role = compressed.role, + depth = depth, + isLast = last, + parentLines = parentLines, + skipped = compressed.skipped, + active = compressed.representative.id in active, + onPath = node.id in active || compressed.representative.id in active, + children = build(compressed.representative.children, depth + 1, parentLines + (!last)), + ) + listOf(row) + } + } + return build(tree, 0, emptyList()) + } + + private data class Compressed( + val representative: BranchNode, + val skipped: Int, + val label: String, + val role: String?, + ) + + private fun compress(node: BranchNode): Compressed { + var current = node + var previewText = current.previewText + var previewRole = current.previewRole + var labelNode = current + if (current.type == "message") { + previewText = previewText ?: messagePreview(current) + labelNode = current + } + var skipped = current.compressedEntryIds.size + while (current.children.size == 1) { + current = current.children[0] + if (previewText == null) previewText = current.previewText ?: messagePreview(current) + if (labelNode.type != "message" && current.type == "message") labelNode = current + skipped += 1 + current.compressedEntryIds.size + } + val label = previewText ?: messagePreview(labelNode) ?: labelNode.type + val role = previewRole ?: labelNode.role + return Compressed(current, skipped, label, role) + } + + private fun messagePreview(node: BranchNode): String? { + if (node.type != "message") return null + val text = node.text?.replace(WHITESPACE, " ")?.trim().orEmpty() + if (text.isEmpty()) return if (node.role == "assistant") "assistant" else "message" + return if (text.length > 40) text.take(40) + "…" else text + } + + private val WHITESPACE = Regex("\\s+") +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatCoalescer.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatCoalescer.kt new file mode 100644 index 000000000..5b3436129 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatCoalescer.kt @@ -0,0 +1,81 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.JsonObject + +/** Schedules [flush] and returns a cancel function. */ +typealias FlushScheduler = (() -> Unit) -> () -> Unit + +/** + * Display-rate coalescing of `message_update` and `tool_execution_update` + * frames (web: `lib/message-update-coalescer.ts`). + * + * Every update carries the full accumulated partial message, so only the + * newest one is worth rendering. Tool updates keep one latest snapshot per + * `toolCallId`. Any other frame type flushes the buffers first, and + * `message_end` drops a pending message update without touching buffered + * tool updates (those belong to the tool the committed message announced). + */ +class ChatFrameCoalescer( + private val dispatch: (JsonObject) -> Unit, + private val schedule: FlushScheduler = { flush -> flush(); {} }, +) { + private var pendingMessage: JsonObject? = null + private val pendingTools = LinkedHashMap() + private var cancelScheduled: (() -> Unit)? = null + + fun push(frame: JsonObject) { + when (frame.string("type")) { + "message_update" -> { + pendingMessage = frame + scheduleFlush() + } + "tool_execution_update" -> { + val toolCallId = frame.string("toolCallId") + if (toolCallId == null) { + // Two tools must never overwrite each other's slot. + flush() + dispatch(frame) + return + } + pendingTools[toolCallId] = frame + scheduleFlush() + } + "message_end" -> { + pendingMessage = null + if (pendingTools.isEmpty()) cancel() + dispatch(frame) + } + else -> { + if (pendingMessage != null || pendingTools.isNotEmpty()) cancel() + flush() + dispatch(frame) + } + } + } + + fun flush() { + cancelScheduled = null + val message = pendingMessage + val tools = pendingTools.values.toList() + pendingMessage = null + pendingTools.clear() + message?.let(dispatch) + tools.forEach(dispatch) + } + + /** Drop pending output and cancel the scheduled flush (stream replaced). */ + fun reset() { + pendingMessage = null + pendingTools.clear() + cancel() + } + + private fun scheduleFlush() { + if (cancelScheduled == null) cancelScheduled = schedule(::flush) + } + + private fun cancel() { + cancelScheduled?.invoke() + cancelScheduled = null + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatDisplayPrefs.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatDisplayPrefs.kt new file mode 100644 index 000000000..48c30f058 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatDisplayPrefs.kt @@ -0,0 +1,25 @@ +package io.github.kahme247.ompweb.chat + +/** + * How the reader's chat preferences map onto transcript row state (web + * `ChatWindow`/`MessageView`): the three preferences that decide whether a + * row starts folded, open, or as a flat live tail. + */ +object ChatDisplayPrefs { + /** + * Tool cards open while the call runs only when the reader did not ask for + * collapsed cards; finished calls always start folded. + */ + fun toolCallExpanded(toolCallsCollapsed: Boolean, running: Boolean): Boolean = + !toolCallsCollapsed && running + + /** Thinking blocks start open when "expand thinking" is on. */ + fun thinkingExpanded(expandThinking: Boolean): Boolean = expandThinking + + /** + * The running turn renders flat (one row per message, activity visible) + * unless tool activity is hidden; the fold then stays in place. + */ + fun liveTail(hideToolActivity: Boolean, streaming: Boolean, isLastGroup: Boolean): Boolean = + !hideToolActivity && streaming && isLastGroup +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatEngine.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatEngine.kt new file mode 100644 index 000000000..4df5a795e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatEngine.kt @@ -0,0 +1,1376 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import io.github.kahme247.ompweb.domain.SUBAGENT_ACTIVITY_BUFFER_MAX +import io.github.kahme247.ompweb.net.ElsewhereSession +import io.github.kahme247.ompweb.domain.SkillDiagnosticsSnapshot +import io.github.kahme247.ompweb.domain.SubagentAgentSource +import io.github.kahme247.ompweb.domain.SubagentInfo +import io.github.kahme247.ompweb.domain.SubagentSource +import io.github.kahme247.ompweb.domain.parseSubagentActivityEvent +import io.github.kahme247.ompweb.domain.parseSubagentLifecycle +import io.github.kahme247.ompweb.domain.parseSubagentProgress +import io.github.kahme247.ompweb.domain.parseSkillDiagnosticsSnapshot +import io.github.kahme247.ompweb.domain.parseTodoPhases +import io.github.kahme247.ompweb.domain.mergeSubagentRoster +import io.github.kahme247.ompweb.domain.pruneSubagentIds +import kotlin.math.roundToLong + +/** One-shot side effects the view model executes after a frame batch. */ +sealed interface ChatEffect { + /** Reload the display history; `reset` re-reads from the start. */ + data class RequestContext(val reset: Boolean) : ChatEffect + + /** Ask the view model to re-read `GET /api/agent/{id}` for a run snapshot. */ + data object RequestAgentState : ChatEffect + + /** Speak the latest assistant reply when autoplay is enabled. */ + data object SpeakLastReply : ChatEffect + + /** Play the completion tone (`omp-sound-enabled`), on every terminal `agent_end`. */ + data object PlayCompletionTone : ChatEffect + + /** Show the compaction summary banner (auto-dismiss after 6 s). */ + data class ShowCompactResult(val result: CompactResult) : ChatEffect + + /** Show a failed compaction (auto-dismiss after 3 s). */ + data class ShowCompactError(val message: String) : ChatEffect + + /** Re-read the live roster and the on-disk history after a run ended. */ + data object RefreshSubagents : ChatEffect + + /** The session SSE (re)connected: side questions and the roster re-read silently. */ + data object StreamOpened : ChatEffect + + /** + * omp moved this conversation to a copy because another process held the + * file. [fromId] is the original (still the other process's); [toId] is the + * copy. The screens offer opening either one. + */ + data class ContentionFork(val fromId: String, val toId: String?) : ChatEffect +} + +/** + * The chat state machine (Slice E of Phase 5). + * + * All transcript, run and live-tool state is derived from SSE frames, context + * syncs and agent-state snapshots. The engine is transport-free: the view + * model feeds frames, API pages and agent state in and executes the returned + * effects. This is what the fixture-driven unit tests drive. + */ +class ChatEngine( + private val now: () -> Long = { System.currentTimeMillis() }, + scheduler: FlushScheduler? = null, + /** Side-question frames never touch the transcript; the view model owns them. */ + private val onBtwFrame: ((JsonObject) -> Unit)? = null, +) { + private val _state = MutableStateFlow(ChatState()) + val state: StateFlow = _state.asStateFlow() + + private val coalescer = ChatFrameCoalescer( + ::handleFrame, + scheduler ?: { flush -> flush(); {} }, + ) + private val effects = ArrayList() + + private var streamId: String? = null + private var messageSequence = 0L + private var lifecycleSequence = 0L + private var toolsSnapshotSequence = 0L + private val toolSequences = HashMap() + private var lastLiveSequence = -1L + private var lastLiveStreamId: String? = null + + private var runHadContent = false + private var lastRunError: String? = null + /** + * Bumped whenever the roster is cleared. An in-flight `get_subagents` or + * history response captured before the clear must not merge into the new + * roster (web `subagentRosterGenerationRef`). + */ + private var subagentGeneration = 0L + /** + * The prompt ack timed out but omp may already hold the frame + * (`session_slow` + `maybeStarted`). The run stays active (sending stays + * locked) and an apparently empty completion repeats the hint instead of + * claiming the agent stopped without a response. + */ + private var maybeStartedRun = false + /** + * A run ended without a visible answer (e.g. its frames were lost to a + * reconnect). The classification waits for the reload: the session file + * may already hold the reply (web `finishPromptWithoutStream`). + */ + private var awaitingResponseCheck = false + /** Rows committed from frames that no context page has confirmed yet. */ + private val unconfirmed = LinkedHashMap() + + /** Live tokens/s estimate of the current assistant message (see [StreamRateTracker]). */ + private val streamRate = StreamRateTracker() + private var lastRatePublishAt = 0L + private var lastPublishedRate: Double? = null + + // ---------------------------------------------------------------- public API + + fun setSession(sessionId: String?, leafId: String? = null) { + coalescer.reset() + resetStreamRate() + effects.clear() + streamId = null + messageSequence = 0 + lifecycleSequence = 0 + toolsSnapshotSequence = 0 + toolSequences.clear() + lastLiveSequence = -1 + lastLiveStreamId = null + runHadContent = false + lastRunError = null + awaitingResponseCheck = false + maybeStartedRun = false + unconfirmed.clear() + subagentGeneration += 1 + _state.value = ChatState( + sessionId = sessionId, + leafId = leafId, + historyLoading = sessionId != null, + ) + if (sessionId != null) effects += ChatEffect.RequestContext(reset = true) + } + + fun onFrame(frame: JsonObject) { + coalescer.push(frame) + } + + /** The roster generation a hydration request must still match to apply. */ + fun subagentGeneration(): Long = subagentGeneration + + /** Fold roster entries into the current roster (live frames win over history). */ + fun mergeSubagents(incoming: List, skipNewerThan: Long? = null) { + if (incoming.isEmpty()) return + _state.update { + it.copy(subagents = mergeSubagentRoster(it.subagents, incoming, skipNewerThan)) + .withSettledSubagents() + } + } + + /** + * Roster with the transcript's async-result deliveries applied: a detached + * task never gets a lifecycle frame, so its end is only provable from the + * transcript (see `settleSubagentsFromAsyncResults`). + */ + private fun ChatState.withSettledSubagents(): ChatState { + if (subagents.isEmpty()) return this + val settled = settleSubagentsFromAsyncResults(subagents, messages) + return if (settled === subagents) this else copy(subagents = settled) + } + + /** + * Drop live entries the registry no longer knows: omp deletes a subagent + * before `get_subagents` answers once its lifecycle went terminal, so a + * missing entry means a terminal frame was missed. Entries touched after + * the snapshot was requested are newer than it and survive. + */ + fun pruneLiveSubagents(snapshotIds: Set, requestedAt: Long) { + _state.update { state -> + val next = state.subagents.filter { entry -> + entry.source == io.github.kahme247.ompweb.domain.SubagentSource.HISTORY || + entry.id in snapshotIds || + (entry.lastUpdate ?: 0L) >= requestedAt + } + if (next.size == state.subagents.size) state else state.copy(subagents = next) + } + } + + fun flush() { + coalescer.flush() + } + + /** Drain the effects accumulated since the last call. */ + fun drainEffects(): List { + if (effects.isEmpty()) return emptyList() + val drained = ArrayList(effects.size) + for (effect in effects) { + if (drained.lastOrNull() == effect) continue + drained += effect + } + effects.clear() + return drained + } + + /** + * Start an optimistic prompt run. [command] marks a slash command, which + * omp answers without a model turn (web `running_command`). + */ + fun sendStarted(text: String, runId: Long, command: Boolean = false) { + resetStreamRate() + val snapshot = _state.value + _state.value = snapshot.copy( + runId = runId, + runStartedAt = now(), + pendingUser = PendingUser(text), + agentRunning = true, + promptRunning = true, + streamingActive = false, + streaming = null, + liveTokensPerSecond = null, + liveTools = emptyList(), + error = null, + autoRetry = null, + agentPhase = if (command) AgentPhase.RunningCommand else AgentPhase.WaitingModel, + // The prompt is on its way to a live session; the screen must not + // fall back to "Connecting…" over the turn it just started. + liveStateKnown = true, + // This app started the run: it owns the session now (a user who + // chose "send anyway (copy)" gets the copy's run). + foreignRun = null, + ) + runHadContent = false + lastRunError = null + awaitingResponseCheck = false + maybeStartedRun = false + coalescer.reset() + } + + fun sendFailed(message: String?) { + resetStreamRate() + _state.update { + it.copy( + pendingUser = null, + agentRunning = false, + promptRunning = false, + streamingActive = false, + streaming = null, + liveTokensPerSecond = null, + liveTools = emptyList(), + agentPhase = null, + runStartedAt = null, + ) + } + coalescer.reset() + if (!message.isNullOrBlank()) addNotice(NoticeKind.Error, message) + } + + /** + * The prompt ack timed out while omp was busy: the message may already be + * running. Keep the optimistic turn and the run state (so sending stays + * locked) and only add the hint; the next frame or successful agent-state + * snapshot settles the run normally. + */ + fun promptMaybeStarted(message: String?) { + maybeStartedRun = true + addNotice(NoticeKind.Warning, message?.ifBlank { null } ?: "omp is busy — your message may still be running.") + } + + /** + * Another omp process owns this session, as the server just reported (a + * `409 session_running_elsewhere` body or a state snapshot). The banner + * and the send gate read it; a proven local run clears it. + */ + fun setForeignRun(elsewhere: ElsewhereSession?) { + if (_state.value.foreignRun == elsewhere) return + _state.update { it.copy(foreignRun = elsewhere) } + } + + /** The prompt was accepted into a queue instead of starting a run. */ + fun promptQueued(message: String?) { + _state.update { + it.copy( + pendingUser = null, + agentRunning = false, + promptRunning = false, + streamingActive = false, + streaming = null, + agentPhase = null, + ) + } + addNotice(NoticeKind.Info, message?.ifBlank { null } ?: "Message queued") + } + + fun abortSent() { + addNotice(NoticeKind.Info, "Stopping…") + } + + /** omp-web's own `!`/`!!` shell command took the session (`bash` RPC). */ + fun bashStarted() { + _state.update { it.copy(bashRunning = true, error = null) } + } + + /** The `bash` RPC answered (successfully or not): the live row goes away. */ + fun bashFinished() { + _state.update { it.copy(bashRunning = false) } + } + + /** Surface a view-model level message in the chat's notice stack. */ + fun notify(kind: NoticeKind, message: String) { + if (message.isNotBlank()) addNotice(kind, message) + } + + fun setIncludePreCompaction(include: Boolean) { + if (_state.value.includePreCompaction == include) return + _state.update { it.copy(includePreCompaction = include, historyLoading = true) } + effects += ChatEffect.RequestContext(reset = true) + } + + fun setLeaf(leafId: String?) { + _state.update { it.copy(leafId = leafId, historyLoading = true) } + effects += ChatEffect.RequestContext(reset = true) + } + + fun historyRequestFailed(message: String) { + _state.update { + // A cached transcript that is on screen keeps the chat usable: it + // becomes the loaded state, so the failure offers a retry banner + // above it instead of the full-pane load error. + val seeded = it.historyStale + it.copy( + historyLoading = false, + pagesLoading = false, + historyStale = false, + historyLoaded = it.historyLoaded || seeded, + historyError = if (it.historyLoaded && !seeded) null else message, + ) + } + } + + /** + * Paint a transcript read from the disk cache while the server answer is + * still on its way (cache-first open). + * + * Refuses whenever the engine already has content — a live frame or a + * context page that arrived first always wins — and never seeds the live + * half of the page (stream cursors, tool state), which only the server may + * report. + */ + fun seedCachedHistory(page: ContextPage): Boolean { + val state = _state.value + if (state.historyLoaded || state.messages.isNotEmpty() || state.entryIds.isNotEmpty()) return false + if (page.messages.isEmpty()) return false + _state.update { + it.copy( + messages = page.messages, + entryIds = page.entryIds, + leafId = page.leafId ?: it.leafId, + thinkingLevel = page.thinkingLevel ?: it.thinkingLevel, + model = page.model ?: it.model, + historyStale = true, + ).withSettledSubagents() + } + return true + } + + fun pagesLoading(loading: Boolean) { + _state.update { it.copy(pagesLoading = loading) } + } + + /** Apply one context-sync page (display history + live snapshot). */ + fun applyContextPage(page: ContextPage) { + val state = _state.value + if (page.sessionId != null && state.sessionId != null && page.sessionId != state.sessionId) return + val pageIdentities = page.messages.mapTo(HashSet()) { it.identity() } + unconfirmed.keys.filter { it in pageIdentities }.forEach { unconfirmed.remove(it) } + + val messages: MutableList + val entryIds: MutableList + if (state.messages.isEmpty() && !state.historyLoaded) { + messages = page.messages.toMutableList() + entryIds = page.entryIds.toMutableList() + } else if (page.mode == "append" && page.baseEntryId == state.entryIds.lastOrNull()) { + messages = state.messages.toMutableList() + // Ids must stay index-aligned with the messages: a frame-committed + // row has no id yet, so it keeps a blank slot until a page names it. + entryIds = state.entryIds.toMutableList() + while (entryIds.size < messages.size) entryIds += "" + val seen = entryIds.filter { it.isNotEmpty() }.toHashSet() + for (index in page.entryIds.indices) { + val id = page.entryIds[index] + if (id.isNotEmpty() && !seen.add(id)) continue + val incoming = page.messages[index] + // A frame-committed row lacks the entry id the page now has; + // the page row replaces it in place instead of duplicating it. + val existingIndex = messages.indexOfFirst { it.identity() == incoming.identity() } + if (existingIndex >= 0) { + messages[existingIndex] = incoming + entryIds[existingIndex] = id + } else { + messages += incoming + entryIds += id + } + } + } else { + messages = page.messages.toMutableList() + entryIds = page.entryIds.toMutableList() + } + // A frame-committed row can outrun the session-file write; keep it + // visible (with a blank id) until a page confirms it. + for ((identity, message) in unconfirmed) { + if (messages.none { it.identity() == identity }) { + messages += message + while (entryIds.size < messages.size - 1) entryIds += "" + entryIds += "" + } + } + while (entryIds.size < messages.size) entryIds += "" + + _state.update { + it.copy( + messages = messages, + entryIds = entryIds, + leafId = page.leafId ?: it.leafId, + thinkingLevel = page.thinkingLevel ?: it.thinkingLevel, + model = page.model ?: it.model, + historyLoaded = true, + historyLoading = false, + historyStale = false, + pagesLoading = if (page.hasMore) it.pagesLoading else false, + historyError = null, + pendingUser = it.pendingUser?.takeUnless { pending -> + messages.takeLast(4).any { row -> row is ChatMessage.User && row.text == pending.text } + }, + liveTools = pruneCommittedTools(messages, it.liveTools), + todoPhases = if (it.agentRunning) it.todoPhases else page.todoPhases, + ).withSettledSubagents() + } + page.live?.let(::applyLiveSnapshot) + if (awaitingResponseCheck && !page.hasMore) { + awaitingResponseCheck = false + if (!hasResponseSinceLastPrompt(messages) && !maybeStartedRun) { + addNotice(NoticeKind.Error, NO_RESPONSE_MESSAGE) + } + } + } + + /** Whether the transcript holds an answer (or tool work) after its last prompt. */ + private fun hasResponseSinceLastPrompt(messages: List): Boolean { + val lastPrompt = messages.indexOfLast { it is ChatMessage.User } + for (index in (lastPrompt + 1) until messages.size) { + when (val message = messages[index]) { + is ChatMessage.Assistant -> if (hasVisibleContent(message)) return true + is ChatMessage.ToolResult -> return true + else -> Unit + } + } + return false + } + + private fun pruneCommittedTools( + messages: List, + tools: List, + ): List { + val committed = messages.filterIsInstance().mapTo(HashSet()) { it.toolCallId } + return tools.filterNot { it.toolCallId in committed } + } + + private fun applyLiveSnapshot(live: LiveSnapshot) { + // A page carries `live` only when the server had a live wrapper for + // this session, which is exactly the answer the footer waits for. Set + // it before the cursor fence: even a stale snapshot proves liveness. + markLiveStateKnown() + val cursor = live.cursor + if (cursor != null) { + val newer = cursor.streamId != lastLiveStreamId || cursor.sequence > lastLiveSequence + if (!newer) return + lastLiveStreamId = cursor.streamId + lastLiveSequence = cursor.sequence + toolsSnapshotSequence = maxOf(toolsSnapshotSequence, cursor.sequence) + } + _state.update { + it.copy( + streamingActive = live.isStreaming, + streaming = if (live.isStreaming) live.streamingMessage ?: it.streaming else null, + agentRunning = if (live.isStreaming || live.isPromptRunning || live.isCompacting) true else it.agentRunning, + promptRunning = live.isPromptRunning, + compacting = live.isCompacting, + liveTools = replayToolEvents(it.liveTools, live.toolEvents), + ) + } + if (live.responseObserved) runHadContent = true + if (live.isStreaming && live.streamingMessage != null && hasVisibleContent(live.streamingMessage)) { + runHadContent = true + } + } + + private fun replayToolEvents(existing: List, events: List): List { + val tools = existing.toMutableList() + for (event in events) { + val index = tools.indexOfFirst { it.toolCallId == event.toolCallId } + when (event.type) { + "tool_execution_start", "start" -> { + if (index < 0) { + tools += LiveTool( + toolCallId = event.toolCallId, + toolName = event.toolName ?: event.result?.toolName ?: "", + args = event.args, + result = null, + startedAt = now(), + ) + } + } + "tool_execution_update", "update" -> { + val current = tools.getOrNull(index) ?: continue + tools[index] = current.copy( + result = event.result ?: current.result, + isError = event.isError, + ) + } + } + } + return tools + } + + /** + * Apply a `GET /api/agent/{id}` snapshot (`{running, state}` or the state + * itself). + * + * [expectedRunId] fences the answer against the run it was requested for: + * a snapshot that left the device before a new prompt must never end that + * prompt's run. [expectedQueueRevision] does the same for the queue alone, + * because a live `queue_update` always wins over a slower `get_state`. + * [expectedSkillDiagnosticsRevision] follows the same rule for the skill + * report, whose live `skill_diagnostics_update` must not be overwritten by + * a snapshot that was requested before it arrived. + */ + fun applyAgentState( + root: JsonObject, + expectedRunId: Long? = null, + expectedQueueRevision: Long? = null, + expectedSkillDiagnosticsRevision: Long? = null, + ) { + if (expectedRunId != null && _state.value.runId != expectedRunId) return + val state = root.obj("state") ?: root + val running = root.bool("running") + // `running` is the run in flight, `alive` the child. An older server + // (and the bare state body) has no `alive`: there `running` still meant + // "a child answers", so it is the fallback for ownership only. + val alive = root.bool("alive") ?: running + val isStreaming = state.bool("isStreaming") == true + val isPromptRunning = state.bool("isPromptRunning") == true + val isCompacting = state.bool("isCompacting") == true + val busy = (running ?: true) && (isStreaming || isPromptRunning || isCompacting) + val wasBusy = _state.value.busy || _state.value.streamingActive + val model = state.obj("model")?.let { + val id = it.string("id") ?: it.string("modelId") ?: "" + ModelRef(provider = it.string("provider") ?: "", id = id, name = it.string("name")) + } + val usage = state.obj("contextUsage")?.let { + ContextUsage( + tokens = it.int("tokens") ?: 0, + contextWindow = it.int("contextWindow") ?: 0, + percent = it.double("percent") ?: 0.0, + ) + } + val queueCurrent = expectedQueueRevision == null || expectedQueueRevision == _state.value.queueRevision + val skillCurrent = expectedSkillDiagnosticsRevision == null || + expectedSkillDiagnosticsRevision == _state.value.skillDiagnosticsRevision + // A snapshot without `todoPhases` (older omp) keeps the current plan. + val todoPhases = state["todoPhases"]?.let(::parseTodoPhases) + // The foreign owner is top-level and always present on a server that + // knows it; an absent field (older server) keeps whatever is known. + val elsewhere = if (root.containsKey("elsewhere")) { + io.github.kahme247.ompweb.net.elsewhereFrom(root["elsewhere"]) + } else { + _state.value.foreignRun + } + // Only an explicit report replaces the current one; an absent field + // means "this omp does not report it", not "no diagnostics". + val skillDiagnostics = if (skillCurrent) parseSkillDiagnosticsSnapshot(state["skillDiagnostics"]) else null + _state.update { + it.copy( + agentRunning = if (running == false) false else busy, + promptRunning = isPromptRunning, + streamingActive = isStreaming, + compacting = isCompacting, + agentPhase = when { + busy -> it.agentPhase ?: AgentPhase.WaitingModel + else -> null + }, + model = model ?: it.model, + thinkingLevel = state.string("thinkingLevel") ?: it.thinkingLevel, + contextUsage = usage ?: it.contextUsage, + tokensPerSecond = state.double("tokensPerSecond") ?: it.tokensPerSecond, + liveTokensPerSecond = if (isStreaming) it.liveTokensPerSecond else null, + queue = if (queueCurrent) parseQueue(state.obj("queuedMessages")) ?: it.queue else it.queue, + todoPhases = todoPhases ?: it.todoPhases, + // The server answered for this session: the run state is known, + // even when it answers "nothing is running". + liveStateKnown = true, + skillDiagnostics = skillDiagnostics ?: it.skillDiagnostics, + skillDiagnosticsRevision = if (skillDiagnostics != null) it.skillDiagnosticsRevision + 1 else it.skillDiagnosticsRevision, + // A locally proven run owns the session: the server then + // reports `elsewhere: null` itself, and a stale report must not + // survive a run this app is driving. Ownership follows `alive` + // (our child exists), not `running`: an idle wrapper owns it. + foreignRun = if (alive == true || busy) null else elsewhere, + ) + } + if (state.bool("responseObserved") == true) runHadContent = true + if (wasBusy && !busy && !isStreaming && !isPromptRunning && !isCompacting) { + finishRun(surfaceError = true) + } + } + + /** + * Apply the answer of `set_skill_startup_diagnostics`. [expectedRevision] + * is the `skillDiagnosticsRevision` captured before the command was sent: + * a live report that arrived meanwhile already reflects the new + * preference and must win (web `skillDiagnosticsSeqRef`). Returns whether + * the reply was applied. + */ + fun applySkillDiagnostics(snapshot: SkillDiagnosticsSnapshot, expectedRevision: Long? = null): Boolean { + if (expectedRevision != null && expectedRevision != _state.value.skillDiagnosticsRevision) return false + _state.update { + it.copy( + skillDiagnostics = snapshot, + skillDiagnosticsRevision = it.skillDiagnosticsRevision + 1, + ) + } + return true + } + + // ------------------------------------------------------------- frame handling + + /** + * A live-state read answered 503 `session_slow` (or upstream's + * `session_state_timeout`): omp is alive but its event loop is blocked by a + * large session, so the run state is unknown — not ended. The body carries + * the server's own run state: `false` means no turn is in flight (a fresh + * child loading a large transcript answers `get_state` too late), so the + * client settles instead of showing "working" over an idle chat; `true` or + * an unreported state (older server) keeps the run marked. Only the fences + * in the caller's read decide whether it may still be applied. + */ + fun markLiveStateSlow(running: Boolean?) { + if (running == false) { + markLiveStateIdle() + return + } + _state.update { state -> + if (state.liveStateKnown && state.busy) return@update state + state.copy(liveStateKnown = true, agentRunning = true) + } + } + + /** + * The server has authoritative evidence that nothing runs for this session + * (`running: false` on a slow answer, or the running list no longer naming + * it). Settle through the same path as an idle `GET /api/agent/{id}` + * snapshot, so a missed `agent_end` or an endless 503 cannot leave the + * screen in "working" for hours. + */ + fun markLiveStateIdle() { + if (_state.value.busy || _state.value.streamingActive) finishRun(surfaceError = true) + _state.update { it.copy(liveStateKnown = true) } + } + + /** + * The live run state of this session has been observed (a frame on its + * channel or a `GET /api/agent/{id}` snapshot). Only then may the screen + * report "nothing is running"; before that it says *Connecting…* instead of + * looking idle over a cached transcript. + */ + private fun markLiveStateKnown() { + if (_state.value.liveStateKnown) return + _state.update { it.copy(liveStateKnown = true) } + } + + private fun handleFrame(frame: JsonObject) { + val type = frame.string("type") ?: return + // Any frame proves the session's stream is live, even one the sequence + // floors below drop as stale: the page can stop saying "Connecting…". + markLiveStateKnown() + if (type == "btw_record" || type == "btw_delta") { + onBtwFrame?.invoke(frame) + return + } + if (!observe(frame)) return + when (type) { + "connected" -> { + effects += ChatEffect.StreamOpened + effects += ChatEffect.RequestContext(reset = false) + } + "agent_start" -> handleAgentStart() + "agent_end" -> handleAgentEnd(frame) + "message_start" -> handleMessageStart(frame) + "message_update" -> handleMessageUpdate(frame) + "message_end" -> handleMessageEnd(frame) + "tool_execution_start" -> handleToolStart(frame) + "tool_execution_update" -> handleToolUpdate(frame) + "tool_execution_end" -> handleToolEnd(frame) + "prompt_result" -> handlePromptResult(frame) + "prompt_error" -> { + val message = readAgentError(frame["errorMessage"]) ?: readAgentError(frame["error"]) + ?: readAgentError(frame["message"]) ?: "Command failed" + lastRunError = message + if (_state.value.busy) finishRun(surfaceError = true) else addNotice(NoticeKind.Error, message) + } + "error", "agent_error", "turn_error", "model_error", "server_error", + "internal_error", "rpc_frame_error" -> { + val message = readTerminalError(frame) ?: "The request failed" + lastRunError = message + if (!_state.value.agentRunning) addNotice(NoticeKind.Error, message) + } + "notice" -> handleNotice(frame) + "command_output" -> { + val text = frame.string("text")?.trim().orEmpty() + if (text.isNotEmpty()) addNotice(NoticeKind.Info, text) + } + "thinking_level_changed" -> { + val level = frame.string("thinkingLevel") + _state.update { it.copy(thinkingLevel = level ?: it.thinkingLevel) } + } + "model_changed" -> effects += ChatEffect.RequestAgentState + "config_update" -> { + val model = frame.obj("model")?.let { + ModelRef( + provider = it.string("provider") ?: "", + id = it.string("id") ?: it.string("modelId") ?: "", + name = it.string("name"), + ) + } + _state.update { + it.copy( + model = model ?: it.model, + thinkingLevel = frame.string("thinkingLevel") ?: it.thinkingLevel, + ) + } + } + "available_commands_update" -> { + val commands = io.github.kahme247.ompweb.domain.parseSlashCommands(frame["commands"]) + if (commands.isNotEmpty()) _state.update { it.copy(remoteCommands = commands) } + } + "queue_update" -> { + // A full snapshot only; a partial payload must not clear the + // queue (web `readQueueSnapshot`). + val queue = parseQueue(frame.obj("queuedMessages") ?: frame) + if (queue != null) { + _state.update { it.copy(queue = queue, queueRevision = it.queueRevision + 1) } + } + } + "skill_diagnostics_update" -> { + // The report rides under `data`; a payload the server's parser + // rejected arrives as null and clears the banner (web + // `parseSkillDiagnosticsSnapshot(...) ?? null`). + _state.update { + it.copy( + skillDiagnostics = parseSkillDiagnosticsSnapshot(frame["data"]), + skillDiagnosticsRevision = it.skillDiagnosticsRevision + 1, + ) + } + } + "auto_retry_start" -> { + _state.update { + it.copy( + autoRetry = AutoRetry( + attempt = frame.int("attempt"), + maxAttempts = frame.int("maxAttempts"), + message = frame.string("errorMessage"), + ), + ) + } + } + "auto_retry_end" -> _state.update { it.copy(autoRetry = null) } + "auto_compaction_start" -> _state.update { it.copy(compacting = true) } + "auto_compaction_end" -> { + _state.update { it.copy(compacting = false) } + val error = frame.string("errorMessage")?.takeIf { it.isNotBlank() } + if (error != null) { + effects += ChatEffect.ShowCompactError(error) + } else if (frame.bool("aborted") != true && frame.bool("skipped") != true) { + readCompactResult(frame["result"], "auto")?.let { effects += ChatEffect.ShowCompactResult(it) } + effects += ChatEffect.RequestContext(reset = false) + } + } + "todo_reminder", "todo_auto_clear" -> effects += ChatEffect.RequestAgentState + "subagent_lifecycle" -> handleSubagentLifecycle(frame) + "subagent_progress" -> handleSubagentProgress(frame) + "subagent_event" -> handleSubagentEvent(frame) + // The view model answers these on the host-tools/extension paths; + // they must never reach transcript handling. + "host_tool_call", "host_uri_request", "extension_ui_request", + "extension_ui_cancel", "extension_status_update", "extension_widget_update", + -> Unit + "session_settled" -> Unit + // omp forked this conversation because another process held the + // file (a forced send raced the owner). The copy carries the user's + // text, so it must be visible instead of looking like a new chat; + // the screens offer opening the original from here. + "session_contention_fork" -> { + val fromId = frame.string("fromId") + if (fromId != null) { + effects += ChatEffect.ContentionFork(fromId, frame.string("toId")) + // The copy is this app's session now; the original stays + // with the other process. + setForeignRun(null) + } + } + } + } + + /** + * Per-category sequence floors (web: `hooks/useAgentSession-sync.ts`). + * Frames older than the newest frame of their category (or of a stronger + * category) are dropped, so a reconnect replay cannot resurrect stale + * transcript state. + */ + private fun observe(frame: JsonObject): Boolean { + val web = frame.obj("web") + if (web != null) { + val incomingStream = web.string("streamId") + val sequence = web.long("sequence") ?: 0L + if (incomingStream != null && streamId != null && incomingStream != streamId) { + streamId = incomingStream + resetForEpoch() + return true + } + if (incomingStream != null) streamId = incomingStream + val type = frame.string("type") + val floor = when { + type == null -> 0L + type in LIFECYCLE_FRAMES -> maxOf(lifecycleSequence, messageSequence, newestToolSequence()) + type.startsWith("message_") && !isUserMessageFrame(frame) -> maxOf(lifecycleSequence, messageSequence) + type.startsWith("tool_execution_") -> { + val id = frame.string("toolCallId") + maxOf(lifecycleSequence, toolsSnapshotSequence, id?.let { toolSequences[it] ?: 0L } ?: 0L) + } + else -> 0L + } + if (sequence < floor) return false + when { + type in LIFECYCLE_FRAMES -> lifecycleSequence = sequence + type != null && type.startsWith("message_") && !isUserMessageFrame(frame) -> messageSequence = sequence + type != null && type.startsWith("tool_execution_") -> + frame.string("toolCallId")?.let { toolSequences[it] = sequence } + } + } + return true + } + + private fun newestToolSequence(): Long = toolSequences.values.maxOrNull() ?: 0L + + private fun isUserMessageFrame(frame: JsonObject): Boolean = + frame.obj("message")?.string("role") == "user" || + frame.obj("assistantMessageEvent")?.obj("partial")?.string("role") == "user" + + /** + * Re-evaluate the published live rate without a frame. The composer ticker + * calls this every [STREAM_RATE_PUBLISH_MS] while a reply streams: a stall + * then hides the number after [STREAM_STALE_MS] instead of freezing the + * last value on screen (web `useStreamRate`'s interval). + */ + fun refreshLiveRate(at: Long = now()) { + val state = _state.value + if (!state.streamingActive) return + if (at - lastRatePublishAt < STREAM_RATE_PUBLISH_MS) return + lastRatePublishAt = at + val measured = roundedRate(streamRate.rate(at)) + if (measured == lastPublishedRate) return + lastPublishedRate = measured + _state.update { current -> + if (!current.streamingActive) current + else current.copy(liveTokensPerSecond = measured) + } + } + + /** + * Forget the live rate: the tracker, its publish clock and the last value + * shown. Callers clear `liveTokensPerSecond` in the same state update. + */ + private fun resetStreamRate() { + streamRate.reset() + lastRatePublishAt = 0L + lastPublishedRate = null + } + + /** Rounded to the 0.1 the UI shows, so an unchanged value never rerenders. */ + private fun roundedRate(rate: Double?): Double? = + rate?.let { (it * 10.0).roundToLong() / 10.0 } + + private fun resetForEpoch() { + coalescer.reset() + resetStreamRate() + _state.update { + it.copy( + messages = emptyList(), + entryIds = emptyList(), + streaming = null, + streamingActive = false, + liveTokensPerSecond = null, + agentRunning = false, + promptRunning = false, + compacting = false, + liveTools = emptyList(), + autoRetry = null, + agentPhase = null, + error = null, + runStartedAt = null, + historyLoading = true, + historyLoaded = false, + leafId = it.leafId, + ) + } + unconfirmed.clear() + runHadContent = false + lastRunError = null + messageSequence = 0 + lifecycleSequence = 0 + toolsSnapshotSequence = 0 + toolSequences.clear() + lastLiveSequence = -1 + lastLiveStreamId = null + effects += ChatEffect.RequestContext(reset = true) + } + + private fun handleAgentStart() { + coalescer.reset() + resetStreamRate() + _state.update { + it.copy( + agentRunning = true, + promptRunning = true, + streamingActive = false, + liveTokensPerSecond = null, + liveTools = emptyList(), + autoRetry = null, + error = null, + agentPhase = AgentPhase.WaitingModel, + ) + } + runHadContent = false + lastRunError = null + // A frame is positive evidence: the maybe-started prompt really did run. + maybeStartedRun = false + effects += ChatEffect.RequestContext(reset = false) + } + + private fun handleAgentEnd(frame: JsonObject) { + if (frame.bool("isTerminal") == false) return + if (!_state.value.agentRunning && !_state.value.busy) return + effects += ChatEffect.PlayCompletionTone + val terminalError = readTerminalError(frame) ?: lastRunError + if (terminalError != null) { + addNotice(NoticeKind.Error, terminalError) + } else if (!runHadContent) { + // Do not judge here: the reload below decides whether the model + // really answered (the run's frames may have been lost to a + // reconnect, web `finishPromptWithoutStream`). + awaitingResponseCheck = true + } + finishRun(surfaceError = false) + } + + private fun finishRun(surfaceError: Boolean) { + resetStreamRate() + val error = lastRunError + val hadContent = runHadContent + _state.update { + it.copy( + agentRunning = false, + promptRunning = false, + streamingActive = false, + streaming = null, + liveTokensPerSecond = null, + compacting = false, + liveTools = emptyList(), + autoRetry = null, + agentPhase = null, + runStartedAt = null, + runsEnded = it.runsEnded + 1, + error = if (surfaceError) error ?: it.error else it.error, + // A finished run's live roster is stale: omp drops terminal + // subagents from its registry, and the on-disk history + // repopulates finished chips (web `setSubagents([])`). + subagents = emptyList(), + subagentActivity = emptyMap(), + subagentVersions = emptyMap(), + ) + } + subagentGeneration += 1 + if (surfaceError && error != null && _state.value.notices.none { it.message == error }) { + addNotice(NoticeKind.Error, error) + } + coalescer.reset() + runHadContent = false + lastRunError = null + // `maybeStartedRun` deliberately survives until the reload above + // settles `awaitingResponseCheck`; a new send/frame/session clears it. + effects += ChatEffect.RequestContext(reset = false) + effects += ChatEffect.RefreshSubagents + if (hadContent && error == null) effects += ChatEffect.SpeakLastReply + } + + /** Lifecycle frames carry the roster entry under `payload`. */ + private fun subagentPayload(frame: JsonObject): JsonObject = frame.obj("payload") ?: frame + + private fun handleSubagentLifecycle(frame: JsonObject) { + val info = parseSubagentLifecycle(subagentPayload(frame), now()) ?: return + mergeSubagents(listOf(info)) + } + + /** + * Progress frames carry the full `AgentProgress` snapshot (throttled to one + * per ~150 ms). The reliable key is `progress.id`; `parentToolCallId` and + * `index` are fallbacks, and a known id that vanished means the frame is + * stale — falling back there could overwrite a different child. + */ + private fun handleSubagentProgress(frame: JsonObject) { + val payload = frame.obj("payload") ?: return + val progress = parseSubagentProgress(payload["progress"]) + val progressId = progress?.id + val index = payload.int("index") ?: progress?.index ?: -1 + val parentToolCallId = payload.string("parentToolCallId") + val task = payload.string("task")?.takeIf { it.isNotBlank() } ?: progress?.task + val assignment = payload.string("assignment") ?: progress?.assignment + if (progressId == null && task == null && parentToolCallId == null && index < 0) return + val timestamp = now() + _state.update { state -> + if (state.subagents.isEmpty()) return@update state + val target = when { + progressId != null -> state.subagents.indexOfFirst { it.id == progressId } + parentToolCallId != null && index >= 0 -> state.subagents.indexOfFirst { + it.parentToolCallId == parentToolCallId && it.index == index + } + parentToolCallId != null -> state.subagents.indexOfFirst { it.parentToolCallId == parentToolCallId } + index >= 0 -> state.subagents.indexOfFirst { it.index == index } + else -> -1 + } + if (target < 0) return@update state + val current = state.subagents[target] + val next = current.copy( + agent = payload.string("agent") ?: current.agent, + // The snapshot's agent-source literal lives in payload.agentSource, + // not payload.agent (which holds the agent name). + agentSource = SubagentAgentSource.fromWire(payload.string("agentSource")) ?: current.agentSource, + sessionFile = payload.string("sessionFile") ?: current.sessionFile, + detached = payload.bool("detached") ?: current.detached, + task = task ?: current.task, + assignment = assignment ?: current.assignment, + progress = progress ?: current.progress, + lastUpdate = timestamp, + source = SubagentSource.LIVE, + ) + // Progress arrives every ~150 ms; skip the rerender when no + // displayed field changed. + if (next.copy(lastUpdate = current.lastUpdate) == current) return@update state + state.copy(subagents = state.subagents.toMutableList().also { it[target] = next }) + } + } + + /** + * A child's own session event: bump the per-child revision the transcript + * dialog debounces on and keep a bounded live-activity buffer. + */ + private fun handleSubagentEvent(frame: JsonObject) { + val payload = frame.obj("payload") ?: return + val id = payload.string("id")?.takeIf { it.isNotEmpty() } ?: return + val activity = parseSubagentActivityEvent(payload, now()) + _state.update { state -> + val versions = state.subagentVersions.toMutableMap() + versions[id] = (versions[id] ?: 0L) + 1 + val activityMap = if (activity == null) { + state.subagentActivity + } else { + val merged = (state.subagentActivity[id].orEmpty() + activity).takeLast(SUBAGENT_ACTIVITY_BUFFER_MAX) + state.subagentActivity + (id to merged) + } + state.copy( + subagentVersions = pruneSubagentIds(versions), + subagentActivity = pruneSubagentIds(activityMap), + ) + } + } + + private fun handleMessageStart(frame: JsonObject) { + val message = frame.obj("message") ?: return + when (message.string("role")) { + "user" -> Unit + "assistant" -> { + if (!_state.value.agentRunning) return + val parsed = parseChatMessage(message, null, "streaming") as? ChatMessage.Assistant ?: return + resetStreamRate() + _state.update { + it.copy(streaming = parsed, streamingActive = true, liveTokensPerSecond = null) + } + if (hasVisibleContent(parsed)) runHadContent = true + } + } + } + + private fun handleMessageUpdate(frame: JsonObject) { + if (!_state.value.agentRunning) return + val message = frame.obj("message") + ?: frame.obj("assistantMessageEvent")?.obj("partial") + ?: return + if (message.string("role") != "assistant") return + val parsed = parseChatMessage(message, null, "streaming") as? ChatMessage.Assistant ?: return + val preview = parsed.copy(streaming = true) + val at = now() + streamRate.observe(estimateStreamTokens(parsed.blocks), at) + // Publish at most twice a second, and only when the displayed 0.1 + // actually changed; the estimate runs per frame either way. + var publishRate = false + if (at - lastRatePublishAt >= STREAM_RATE_PUBLISH_MS) { + lastRatePublishAt = at + val measured = roundedRate(streamRate.rate(at)) + if (measured != lastPublishedRate) { + lastPublishedRate = measured + publishRate = true + } + } + _state.update { + it.copy( + streaming = preview, + streamingActive = true, + liveTokensPerSecond = if (publishRate) lastPublishedRate else it.liveTokensPerSecond, + ) + } + if (hasVisibleContent(parsed)) runHadContent = true + } + + private fun handleMessageEnd(frame: JsonObject) { + val message = frame.obj("message") ?: return + val parsed = parseChatMessage(message, null, "committed:${message.string("role")}:${now()}") ?: return + val identity = parsed.identity() + val alreadyCommitted = _state.value.messages.any { it.identity() == identity } + if (parsed is ChatMessage.Assistant) { + resetStreamRate() + if (parsed.stopReason == "error" || !parsed.errorMessage.isNullOrBlank()) { + lastRunError = parsed.errorMessage?.trim()?.ifEmpty { null } ?: lastRunError + } + if (hasVisibleContent(parsed)) runHadContent = true + } + _state.update { + it.copy( + streaming = if (parsed is ChatMessage.Assistant) null else it.streaming, + streamingActive = if (parsed is ChatMessage.Assistant) false else it.streamingActive, + liveTokensPerSecond = if (parsed is ChatMessage.Assistant) null else it.liveTokensPerSecond, + messages = if (alreadyCommitted) it.messages else it.messages + parsed, + pendingUser = if (parsed is ChatMessage.User && it.pendingUser?.text == parsed.text) null else it.pendingUser, + liveTools = if (parsed is ChatMessage.ToolResult) { + it.liveTools.filterNot { tool -> tool.toolCallId == parsed.toolCallId } + } else it.liveTools, + ).withSettledSubagents() + } + if (!alreadyCommitted) unconfirmed[identity] = parsed + if (parsed is ChatMessage.Assistant) effects += ChatEffect.RequestContext(reset = false) + } + + private fun handleToolStart(frame: JsonObject) { + val id = frame.string("toolCallId") ?: return + val name = frame.string("toolName") ?: "" + val startedAt = frame.long("timestamp") ?: now() + _state.update { state -> + val existing = state.liveTools.firstOrNull { it.toolCallId == id } + val tool = LiveTool( + toolCallId = id, + toolName = name.ifEmpty { existing?.toolName.orEmpty() }, + args = frame.obj("args") ?: existing?.args, + result = null, + startedAt = existing?.startedAt ?: startedAt, + ) + val liveTools = if (existing == null) state.liveTools + tool + else state.liveTools.map { if (it.toolCallId == id) tool else it } + state.copy( + liveTools = liveTools, + agentPhase = withRunningTool(state.agentPhase, RunningTool(id, tool.toolName)), + ) + } + } + + private fun handleToolUpdate(frame: JsonObject) { + val id = frame.string("toolCallId") ?: return + val partial = frame.obj("partialResult") + val result = partial?.let { + parseToolResultPayload(it, "live:$id")?.copy(partial = true) + } + _state.update { state -> + val tool = state.liveTools.firstOrNull { it.toolCallId == id } + ?: LiveTool( + toolCallId = id, + toolName = frame.string("toolName") ?: "", + args = frame.obj("args"), + result = null, + startedAt = now(), + ) + val updated = tool.copy( + toolName = frame.string("toolName") ?: tool.toolName, + args = frame.obj("args") ?: tool.args, + result = result ?: tool.result, + isError = frame.bool("isError") == true, + ) + if (state.liveTools.any { it.toolCallId == id }) { + state.copy(liveTools = state.liveTools.map { if (it.toolCallId == id) updated else it }) + } else { + state.copy(liveTools = state.liveTools + updated) + } + } + } + + private fun handleToolEnd(frame: JsonObject) { + val id = frame.string("toolCallId") ?: return + val result = frame.obj("result")?.let { parseToolResultPayload(it, "live:$id") } + val isError = frame.bool("isError") == true || result?.isError == true + val endedAt = now() + _state.update { state -> + val tool = state.liveTools.firstOrNull { it.toolCallId == id } + if (tool == null) state + else state.copy( + liveTools = state.liveTools.map { + if (it.toolCallId != id) it + else it.copy( + toolName = frame.string("toolName") ?: it.toolName, + result = result ?: it.result, + isError = isError, + endedAt = endedAt, + ) + }, + agentPhase = withoutRunningTool(state.agentPhase, id), + ) + } + } + + private fun handlePromptResult(frame: JsonObject) { + if (frame.bool("agentInvoked") != false) return + if (!_state.value.agentRunning && !_state.value.busy) { + effects += ChatEffect.RequestAgentState + return + } + finishRun(surfaceError = false) + } + + private fun handleNotice(frame: JsonObject) { + val level = frame.string("level") + val message = readAgentError(frame["message"]) + ?: readAgentError(frame["errorMessage"]) + ?: readAgentError(frame["error"]) + ?: return + if (message.isBlank()) return + val kind = when (level) { + "error" -> NoticeKind.Error + "warning" -> NoticeKind.Warning + else -> NoticeKind.Info + } + addNotice(kind, message) + } + + /** An in-app notice from a surface outside the frame pump (extension UI, host access). */ + fun notice(kind: NoticeKind, message: String) = addNotice(kind, message) + + /** + * Drop a notice the bar dismissed (user or timer). Without this the dedupe + * in [addNotice] would swallow the same message forever in one session — + * a repeated host-access notice must be able to show again. + */ + fun dismissNotice(notice: ChatNotice) { + _state.update { state -> + if (notice in state.notices) state.copy(notices = state.notices - notice) else state + } + } + + // ------------------------------------------------------------------- helpers + + private fun addNotice(kind: NoticeKind, message: String) { + if (message.isBlank()) return + _state.update { state -> + if (state.notices.any { it.kind == kind && it.message == message }) state + else state.copy(notices = (state.notices + ChatNotice(kind, message, now())).takeLast(MAX_NOTICES)) + } + } + + private fun hasVisibleContent(message: ChatMessage.Assistant): Boolean = + message.blocks.any { block -> + (block is AssistantBlock.Text && block.text.isNotBlank()) || block is AssistantBlock.Image + } + + /** Register a running tool; a repeated start for the same id replaces it. */ + private fun withRunningTool(phase: AgentPhase?, tool: RunningTool): AgentPhase { + val tools = (phase as? AgentPhase.RunningTools)?.tools.orEmpty() + return AgentPhase.RunningTools(tools.filterNot { it.id == tool.id } + tool) + } + + /** Tool ends return to the model unless a sibling tool still runs. */ + private fun withoutRunningTool(phase: AgentPhase?, id: String): AgentPhase? { + val tools = (phase as? AgentPhase.RunningTools)?.tools ?: return phase + val rest = tools.filterNot { it.id == id } + return if (rest.isEmpty()) AgentPhase.WaitingModel else AgentPhase.RunningTools(rest) + } +} + +private val LIFECYCLE_FRAMES = setOf( + "agent_start", + "agent_end", + "prompt_result", + "prompt_error", + "auto_compaction_start", + "auto_compaction_end", +) + +private const val MAX_NOTICES = 5 + +private const val NO_RESPONSE_MESSAGE = "The model returned no response" + +/** Cross-source identity of a message (same role + timestamp = same message). */ +internal fun ChatMessage.identity(): String = when (this) { + is ChatMessage.User -> "user:${timestamp ?: key}" + is ChatMessage.Assistant -> "assistant:${timestamp ?: key}" + is ChatMessage.ToolResult -> "toolResult:${toolCallId.ifEmpty { timestamp?.toString() ?: key }}" + is ChatMessage.BashExecution -> "bash:${timestamp ?: key}" + is ChatMessage.Custom -> "custom:${customType}:${timestamp ?: key}" + is ChatMessage.Other -> "other:$role:${timestamp ?: key}" +} + +/** Read an error out of omp's several payload shapes. */ +internal fun readAgentError(value: JsonElement?): String? { + when (value) { + is JsonPrimitive -> { + if (!value.isString) return null + return value.content.trim().ifEmpty { null } + } + is JsonObject -> { + for (key in listOf("errorMessage", "error", "message", "detail")) { + readAgentError(value[key])?.let { return it } + } + if (value.string("stopReason") == "error") return "The request failed" + return null + } + else -> return null + } +} + +internal fun readTerminalError(frame: JsonObject): String? { + for (key in listOf("errorMessage", "error", "message")) { + readAgentError(frame[key])?.let { return it } + } + val messages = frame["messages"] as? JsonArray ?: return null + for (index in messages.indices.reversed()) { + readAgentError(messages[index])?.let { return it } + } + return null +} + +internal fun parseQueue(value: JsonObject?): QueueSnapshot? { + if (value == null) return null + // Both keys are part of the snapshot contract; a partial payload is not + // queue state (web `readQueueSnapshot`). + if (!value.containsKey("steering") || !value.containsKey("followUp")) return null + fun texts(key: String): List = when (val element = value[key]) { + is JsonArray -> element.mapNotNull { item -> + when (item) { + is JsonPrimitive -> item.content + is JsonObject -> item.string("text") ?: item.string("message") + else -> null + } + }.filter { it.isNotBlank() } + is JsonPrimitive -> element.content.takeIf { it.isNotBlank() }?.let(::listOf) ?: emptyList() + else -> emptyList() + } + return QueueSnapshot( + steering = texts("steering"), + followUp = texts("followUp"), + ) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatJson.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatJson.kt new file mode 100644 index 000000000..057b59384 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatJson.kt @@ -0,0 +1,203 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.longOrNull + +/** Tolerant accessors: every field of an omp frame is optional by contract. */ +internal fun JsonObject.string(key: String): String? = (this[key] as? JsonPrimitive)?.contentOrNull + +internal fun JsonObject.bool(key: String): Boolean? = (this[key] as? JsonPrimitive)?.booleanOrNull + +internal fun JsonObject.int(key: String): Int? = (this[key] as? JsonPrimitive)?.intOrNull + +internal fun JsonObject.long(key: String): Long? = (this[key] as? JsonPrimitive)?.longOrNull + +internal fun JsonObject.double(key: String): Double? = (this[key] as? JsonPrimitive)?.doubleOrNull + +internal fun JsonObject.obj(key: String): JsonObject? = this[key] as? JsonObject + +internal fun JsonObject.array(key: String): JsonArray? = this[key] as? JsonArray + +/** Safe text of a user/custom content payload (string or `[{type:"text"}]`). */ +internal fun contentText(content: JsonElement?): String = when (content) { + is JsonPrimitive -> if (content is JsonNull) "" else content.contentOrNull.orEmpty() + is JsonArray -> content.mapNotNull { block -> + val obj = block as? JsonObject ?: return@mapNotNull null + if (obj.string("type") == "text") obj.string("text") else null + }.joinToString("\n") + else -> "" +} + +internal fun contentImages(content: JsonElement?): List { + val array = content as? JsonArray ?: return emptyList() + return array.mapNotNull { element -> + val block = element as? JsonObject ?: return@mapNotNull null + if (block.string("type") != "image") return@mapNotNull null + parseImage(block) + }.filterNot { it.isEmpty } +} + +/** Both the flat (`data`/`mimeType`) and the nested (`source`) image shapes. */ +internal fun parseImage(block: JsonObject): ChatImage { + val source = block.obj("source") + val mimeType = block.string("mimeType") ?: source?.string("media_type") + val base64 = block.string("data") ?: source?.string("data") + val url = block.string("url") + ?: source?.takeIf { it.string("type") == "url" }?.string("url") + return ChatImage(mimeType = mimeType, url = url, base64 = base64) +} + +private val partialJson = Json { isLenient = true; ignoreUnknownKeys = true } + +/** + * The compaction summary a banner can show (web `readCompactResult`): only a + * numeric `tokensBefore` qualifies, and a missing estimate counts as 0. + */ +internal fun readCompactResult(result: JsonElement?, reason: String): CompactResult? { + val obj = result as? JsonObject ?: return null + val before = obj.long("tokensBefore") ?: return null + return CompactResult(reason = reason, tokensBefore = before, estimatedTokensAfter = obj.long("estimatedTokensAfter") ?: 0L) +} + +/** + * Tool calls reach the app with two field spellings: `id`/`name`/`arguments` + * while streaming, `toolCallId`/`toolName`/`input` in committed messages and + * session files (web: `lib/normalize.ts`). While an argument object is still + * empty, the streaming `partialArgs` JSON is parsed so the row can show the + * command instead of a blank preview. + */ +internal fun parseToolCall(block: JsonObject): AssistantBlock.ToolCall? { + if (block.string("type") != "toolCall") return null + val id = block.string("toolCallId") ?: block.string("id") ?: "" + val name = block.string("toolName") ?: block.string("name") ?: "" + val direct = block.obj("input") ?: block.obj("arguments") + val partialArgs = block.string("partialArgs") + val input = direct?.takeIf { it.isNotEmpty() } + ?: partialArgs?.let { parsePartialArguments(it) } + ?: JsonObject(emptyMap()) + return AssistantBlock.ToolCall( + toolCallId = id, + toolName = name, + input = input, + ) +} + +private fun parsePartialArguments(raw: String): JsonObject? { + if (raw.isBlank()) return null + return try { + partialJson.parseToJsonElement(raw) as? JsonObject + } catch (_: Exception) { + null + } +} + +internal fun parseAssistantBlocks(message: JsonObject): List { + val content = message["content"] as? JsonArray ?: return emptyList() + return content.mapIndexedNotNull { index, element -> + val block = element as? JsonObject ?: return@mapIndexedNotNull null + when (block.string("type")) { + "text" -> AssistantBlock.Text(block.string("text") ?: "") + "thinking" -> AssistantBlock.Thinking( + thinking = block.string("thinking") ?: "", + deferred = block.bool("deferred") == true, + blockIndex = block.int("blockIndex") ?: index, + ) + "toolCall" -> parseToolCall(block) + "image" -> AssistantBlock.Image(parseImage(block)).takeUnless { it.image.isEmpty } + else -> AssistantBlock.Other(block.string("type") ?: "unknown") + } + } +} + +/** Parse one `message` object (SSE frame or `context.messages` entry). */ +internal fun parseChatMessage( + message: JsonObject, + entryId: String?, + syntheticKey: String, +): ChatMessage? { + val timestamp = message.long("timestamp") + return when (message.string("role")) { + "user" -> ChatMessage.User( + key = entryId ?: syntheticKey, + timestamp = timestamp, + entryId = entryId, + text = contentText(message["content"]), + images = contentImages(message["content"]), + branchSummary = message.bool("branchSummary") == true, + ) + "assistant" -> ChatMessage.Assistant( + key = entryId ?: syntheticKey, + timestamp = timestamp, + entryId = entryId, + blocks = parseAssistantBlocks(message), + model = message.string("model"), + provider = message.string("provider"), + stopReason = message.string("stopReason"), + errorMessage = message.string("errorMessage"), + durationMs = message.double("duration"), + usageOutput = message.obj("usage")?.double("output")?.toInt(), + ttftMs = message.double("ttft"), + completedAt = message.long("completedAt"), + ) + "toolResult" -> { + val content = message["content"] + ChatMessage.ToolResult( + key = entryId ?: syntheticKey, + timestamp = timestamp, + entryId = entryId, + toolCallId = message.string("toolCallId") ?: message.string("id") ?: "", + toolName = message.string("toolName") ?: message.string("name") ?: "", + text = contentText(content), + images = contentImages(content), + details = message.obj("details"), + isError = message.bool("isError") == true, + partial = message.bool("partial") == true, + ) + } + "bashExecution" -> ChatMessage.BashExecution( + key = entryId ?: syntheticKey, + timestamp = timestamp, + entryId = entryId, + command = message.string("command") ?: "", + output = message.string("output"), + exitCode = message.int("exitCode"), + cancelled = message.bool("cancelled") == true, + fullOutputPath = message.string("fullOutputPath"), + ) + "custom" -> ChatMessage.Custom( + key = entryId ?: syntheticKey, + timestamp = timestamp, + entryId = entryId, + customType = message.string("customType") ?: "", + text = contentText(message["content"]), + details = message.obj("details"), + display = message.bool("display") != false, + ) + else -> { + val role = message.string("role") ?: return null + ChatMessage.Other(key = entryId ?: syntheticKey, timestamp = timestamp, entryId = entryId, role = role) + } + } +} + +/** + * A tool payload (`partialResult` / `result` / live `toolEvents`) omits the + * `role` field; inject it so it parses as a tool result row. + */ +internal fun parseToolResultPayload(value: JsonObject, key: String): ChatMessage.ToolResult? { + if (value.string("role") != null) return parseChatMessage(value, null, key) as? ChatMessage.ToolResult + return parseChatMessage( + JsonObject(value + ("role" to JsonPrimitive("toolResult"))), + null, + key, + ) as? ChatMessage.ToolResult +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatModels.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatModels.kt new file mode 100644 index 000000000..baef173d4 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ChatModels.kt @@ -0,0 +1,281 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.JsonObject + +/** An image carried by a message. Only one of [url] / [base64] is set. */ +data class ChatImage( + val mimeType: String?, + val url: String? = null, + val base64: String? = null, +) { + val isEmpty: Boolean + get() = url.isNullOrBlank() && base64.isNullOrBlank() +} + +/** + * One block of an assistant message, normalized across the two shapes omp + * uses on the wire (`id`/`name`/`arguments` while streaming, + * `toolCallId`/`toolName`/`input` in the session file). + */ +sealed interface AssistantBlock { + data class Text(val text: String) : AssistantBlock + + data class Thinking( + val thinking: String, + /** True when the server replaced the body by a placeholder (`deferThinking=1`). */ + val deferred: Boolean, + /** Source content index, for `GET .../thinking?blockIndex=`. */ + val blockIndex: Int, + ) : AssistantBlock + + data class ToolCall( + val toolCallId: String, + val toolName: String, + val input: JsonObject, + ) : AssistantBlock + + data class Image(val image: ChatImage) : AssistantBlock + + data class Other(val type: String) : AssistantBlock +} + +/** + * One committed (or currently streaming) transcript message. [key] is stable + * for the lifetime of the row: the entry id when the server gave one, a + * synthetic identity otherwise. + */ +sealed interface ChatMessage { + val key: String + val timestamp: Long? + val entryId: String? + + data class User( + override val key: String, + override val timestamp: Long?, + override val entryId: String?, + val text: String, + val images: List = emptyList(), + /** A compaction branch summary renders like a user row but cannot be forked. */ + val branchSummary: Boolean = false, + ) : ChatMessage + + data class Assistant( + override val key: String, + override val timestamp: Long?, + override val entryId: String?, + val blocks: List, + val model: String? = null, + val provider: String? = null, + val stopReason: String? = null, + val errorMessage: String? = null, + val durationMs: Double? = null, + val streaming: Boolean = false, + /** `usage.output` of the finished message (thinking tokens included). */ + val usageOutput: Int? = null, + /** Time to first token in ms; excluded from the generation rate. */ + val ttftMs: Double? = null, + /** When omp finished the message (ms); fallback span without `duration`. */ + val completedAt: Long? = null, + ) : ChatMessage + + data class ToolResult( + override val key: String, + override val timestamp: Long?, + override val entryId: String?, + val toolCallId: String, + val toolName: String, + val text: String, + val images: List = emptyList(), + val details: JsonObject? = null, + val isError: Boolean = false, + /** A live `partialResult` snapshot for a tool that is still running. */ + val partial: Boolean = false, + /** Passive developer context attached to the batch's last tool card. */ + val passiveContext: String? = null, + ) : ChatMessage + + data class BashExecution( + override val key: String, + override val timestamp: Long?, + override val entryId: String?, + val command: String, + val output: String?, + val exitCode: Int?, + val cancelled: Boolean = false, + val fullOutputPath: String? = null, + ) : ChatMessage + + data class Custom( + override val key: String, + override val timestamp: Long?, + override val entryId: String?, + val customType: String, + val text: String, + val details: JsonObject? = null, + val display: Boolean = true, + ) : ChatMessage + + /** A role this phase does not render (e.g. an unknown extension message). */ + data class Other( + override val key: String, + override val timestamp: Long?, + override val entryId: String?, + val role: String, + ) : ChatMessage +} + +/** A running tool, driven by `tool_execution_*` frames. */ +data class LiveTool( + val toolCallId: String, + val toolName: String, + val args: JsonObject?, + val result: ChatMessage.ToolResult?, + val startedAt: Long, + val endedAt: Long? = null, + val isError: Boolean = false, +) { + val running: Boolean + get() = endedAt == null && result?.partial != true +} + +/** One tool omp is currently executing, as the status line names it. */ +data class RunningTool(val id: String, val name: String) + +/** + * What the running turn waits on (web `AgentPhase`): the model finishing its + * reply, a shell command, or the tools it called. + */ +sealed interface AgentPhase { + data object WaitingModel : AgentPhase + + data object RunningCommand : AgentPhase + + data class RunningTools(val tools: List) : AgentPhase +} + +enum class NoticeKind { Info, Warning, Error } + +data class ChatNotice( + val kind: NoticeKind, + val message: String, + val createdAt: Long, +) + +data class QueueSnapshot( + val steering: List = emptyList(), + val followUp: List = emptyList(), +) + +data class ModelRef(val provider: String, val id: String, val name: String? = null) + +data class ContextUsage(val tokens: Int, val contextWindow: Int, val percent: Double) + +data class AutoRetry(val attempt: Int?, val maxAttempts: Int?, val message: String?) + +/** + * The outcome of a compaction, shown as a transient banner (web + * `CompactResultInfo`). `reason` is `auto` for `auto_compaction_end` and + * `manual` for the RPC `/compact`. + */ +data class CompactResult(val reason: String, val tokensBefore: Long, val estimatedTokensAfter: Long) + +/** An optimistic user row that has not been committed by the server yet. */ +data class PendingUser(val text: String) + +/** + * omp moved a chat to a copy because another process held the file + * (`session_contention_fork`). The original stayed with that process; the copy + * is the conversation this app keeps writing into. + */ +data class ContentionCopy(val originalId: String, val copyId: String?) + +/** The engine-owned chat state. Rendered state is derived from it. */ +data class ChatState( + val sessionId: String? = null, + val messages: List = emptyList(), + val entryIds: List = emptyList(), + val pendingUser: PendingUser? = null, + val streaming: ChatMessage.Assistant? = null, + val liveTools: List = emptyList(), + val notices: List = emptyList(), + /** Terminal run error shown above the composer until the next prompt. */ + val error: String? = null, + val agentRunning: Boolean = false, + val promptRunning: Boolean = false, + val streamingActive: Boolean = false, + /** What the running turn waits on; null while nothing runs. */ + val agentPhase: AgentPhase? = null, + val compacting: Boolean = false, + val autoRetry: AutoRetry? = null, + val model: ModelRef? = null, + val thinkingLevel: String? = null, + val contextUsage: ContextUsage? = null, + val tokensPerSecond: Double? = null, + /** Live estimate from the streamed text/thinking deltas; null when it is not measurable. */ + val liveTokensPerSecond: Double? = null, + val queue: QueueSnapshot = QueueSnapshot(), + /** Bumped by every `queue_update`; a slower `get_state` answer must not override a newer queue. */ + val queueRevision: Long = 0, + val leafId: String? = null, + val includePreCompaction: Boolean = false, + /** True once the first context page arrived; drives the loading state. */ + val historyLoaded: Boolean = false, + val historyLoading: Boolean = false, + /** + * The transcript on screen came from the disk cache and the server answer + * is still on its way. Cleared by the first real page (or by its failure), + * so a cached view can never outlive the live one. + */ + val historyStale: Boolean = false, + /** + * Something about the live run has been observed for this session: a frame + * on its event stream or a `GET /api/agent/{id}` snapshot. While it is + * false the screen shows only history (disk cache first) and must not + * present "nothing runs" as a fact — the footer says *Connecting…* until + * an answer settles it. + */ + val liveStateKnown: Boolean = false, + val historyError: String? = null, + /** True while older pages are still being fetched (`hasMore` from the server). */ + val pagesLoading: Boolean = false, + /** Run generation; stale async completions compare against it. */ + val runId: Long = 0, + /** + * When the current run's prompt was sent. The turn timer measures from + * here, so a tool start or a steering message never resets it; null for a + * run this client did not start (the timer then falls back to the + * transcript). + */ + val runStartedAt: Long? = null, + /** How many runs ended; a stop only targets the run it was clicked on. */ + val runsEnded: Long = 0, + /** omp's slash command roster (`get_commands` / `available_commands_update`). */ + val remoteCommands: List = emptyList(), + /** omp-web's own `!` / `!!` shell command is running (`bash` RPC). */ + val bashRunning: Boolean = false, + /** The live todo plan (`get_state` / context pages), shown above the composer. */ + val todoPhases: List = emptyList(), + /** Roster fed by `subagent_lifecycle`/`subagent_progress`/`get_subagents`/history. */ + val subagents: List = emptyList(), + /** Bounded per-subagent activity buffer for the transcript dialog. */ + val subagentActivity: Map> = emptyMap(), + /** Per-subagent revision bumped by `subagent_event`; the dialog debounces refetches. */ + val subagentVersions: Map = emptyMap(), + /** + * omp's skill-resolution report (`get_state.skillDiagnostics` and + * `skill_diagnostics_update`); null while the server has none. + */ + val skillDiagnostics: io.github.kahme247.ompweb.domain.SkillDiagnosticsSnapshot? = null, + /** Bumped by every applied report; a slower `get_state` answer must not override a newer one. */ + val skillDiagnosticsRevision: Long = 0, + /** + * Another omp process owns this session (`GET /api/agent/{id}.elsewhere`, + * the `409` body, a `running` frame). While it is set, a send would move + * the chat to a copy, so the screen shows the owner and asks before + * sending. Cleared by a snapshot without one and by a locally proven run. + */ + val foreignRun: io.github.kahme247.ompweb.net.ElsewhereSession? = null, +) { + val busy: Boolean + get() = agentRunning || promptRunning || compacting +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/ContextSync.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ContextSync.kt new file mode 100644 index 000000000..4f18974fc --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ContextSync.kt @@ -0,0 +1,133 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull + +/** Durable display-history position scoped to one session and selected view. */ +data class HistoryCursor(val firstEntryId: String?, val lastEntryId: String?) + +/** Ordering within one web-owned RPC process; resets when that process changes. */ +data class StreamCursor(val streamId: String, val sequence: Long) + +/** + * The live half of a context response. Only applied for sequence-newer + * fields; the transcript and the live tool state must not overwrite each + * other (web: `hooks/useAgentSession-sync.ts` + `useAgentSession.ts:2555`). + */ +data class LiveSnapshot( + val cursor: StreamCursor?, + val isStreaming: Boolean, + val isPromptRunning: Boolean, + val isCompacting: Boolean, + val streamingMessage: ChatMessage.Assistant?, + val toolEvents: List, + val responseObserved: Boolean, +) + +data class LiveToolEvent( + val type: String, + val toolCallId: String, + val toolName: String?, + val args: JsonObject?, + val result: ChatMessage.ToolResult?, + val isError: Boolean, +) + +/** One parsed `GET /api/sessions/{id}/context?sync=1` page. */ +data class ContextPage( + val mode: String, + val baseEntryId: String?, + val cursor: HistoryCursor?, + val hasMore: Boolean, + val sessionId: String?, + val leafId: String?, + val thinkingLevel: String?, + val model: ModelRef?, + val entryIds: List, + val messages: List, + val live: LiveSnapshot?, + /** The plan omp derives from the session file; applied while no run is active. */ + val todoPhases: List = emptyList(), +) + +object ContextSync { + + fun parse(root: JsonObject): ContextPage { + val context = root.obj("context") + val entryIds = (context?.array("entryIds") ?: JsonArray(emptyList())) + .mapNotNull { (it as? kotlinx.serialization.json.JsonPrimitive)?.contentOrNull } + val messages = ArrayList() + val rawMessages = context?.array("messages") ?: JsonArray(emptyList()) + for (index in rawMessages.indices) { + val raw = rawMessages[index] as? JsonObject ?: continue + val entryId = entryIds.getOrNull(index) + val message = parseChatMessage(raw, entryId, syntheticKey = "ctx:$index") + if (message != null) messages += message + } + // A malformed page would misalign ids and messages; the caller rejects it. + val aligned = messages.size == entryIds.size + val cursor = root.obj("cursor")?.let { + HistoryCursor(it.string("firstEntryId"), it.string("lastEntryId")) + } + return ContextPage( + mode = root.string("mode") ?: "replace", + baseEntryId = root.string("baseEntryId"), + cursor = cursor, + hasMore = root.bool("hasMore") == true, + sessionId = root.string("sessionId"), + leafId = root.string("leafId"), + thinkingLevel = context?.string("thinkingLevel"), + model = context?.obj("model")?.let { + ModelRef( + provider = it.string("provider") ?: "", + id = it.string("modelId") ?: it.string("id") ?: "", + name = it.string("name"), + ) + }, + entryIds = if (aligned) entryIds else emptyList(), + messages = if (aligned) messages else emptyList(), + live = parseLive(root.obj("live")), + todoPhases = io.github.kahme247.ompweb.domain.parseTodoPhases(context?.get("todoPhases")), + ) + } + + private fun parseLive(live: JsonObject?): LiveSnapshot? { + if (live == null) return null + val cursor = live.obj("cursor")?.let { + val streamId = it.string("streamId") ?: return@let null + StreamCursor(streamId, it.long("sequence") ?: 0L) + } + val streaming = live.obj("streamingMessage")?.let { + parseChatMessage(it, entryId = null, syntheticKey = "live:stream") as? ChatMessage.Assistant + }?.copy(streaming = true) + val toolEvents = live.array("toolEvents")?.mapNotNull { element -> + val event = element as? JsonObject ?: return@mapNotNull null + val id = event.string("toolCallId") ?: return@mapNotNull null + val type = event.string("type") ?: return@mapNotNull null + val partial = event.obj("partialResult")?.let { + parseToolResultPayload(it, "live:tool:$id")?.copy(partial = true) + } + val result = event.obj("result")?.let { + parseToolResultPayload(it, "live:tool:$id") + } + LiveToolEvent( + type = type, + toolCallId = id, + toolName = event.string("toolName"), + args = event.obj("args"), + result = partial ?: result, + isError = event.bool("isError") == true, + ) + } ?: emptyList() + return LiveSnapshot( + cursor = cursor, + isStreaming = live.bool("isStreaming") == true, + isPromptRunning = live.bool("isPromptRunning") == true, + isCompacting = live.bool("isCompacting") == true, + streamingMessage = streaming, + toolEvents = toolEvents, + responseObserved = live.bool("responseObserved") == true, + ) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/FileLinks.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/FileLinks.kt new file mode 100644 index 000000000..28af90fa4 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/FileLinks.kt @@ -0,0 +1,30 @@ +package io.github.kahme247.ompweb.chat + +/** + * File API paths (web: `lib/file-links.ts`). A rendered link is resolved by + * `ui.render.resolveLocalFileTarget`; this object only turns that resolved + * path into the catch-all segments of `GET /api/files/[...path]`. + */ +object FileLinks { + + /** The catch-all segments of `/api/files/[...path]` (the client encodes them). */ + fun apiPathSegments(path: String): List { + val normalized = if (DRIVE_PATH.matches(path) || path.startsWith("\\\\")) { + path.replace('\\', '/') + } else { + path + } + val segments = normalized.split('/').filter { it.isNotEmpty() } + // `.`/`..` never name a file; reaching here with one means the caller + // tried to climb out of the workspace. + if (segments.any { it == "." || it == ".." }) return emptyList() + // A UNC path keeps its `//server` prefix in the first segment. + return if (normalized.startsWith("//") && segments.isNotEmpty()) { + listOf("//${segments[0]}") + segments.drop(1) + } else { + segments + } + } + + private val DRIVE_PATH = Regex("""^[a-zA-Z]:[\\/].*""") +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/ForkTargets.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ForkTargets.kt new file mode 100644 index 000000000..047ddee73 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ForkTargets.kt @@ -0,0 +1,63 @@ +package io.github.kahme247.ompweb.chat + +/** The user entry omp's `branch`/`fork` command accepts for a transcript row. */ +data class ForkTarget(val entryId: String, val editPrompt: Boolean) + +/** + * Fork targets for the transcript (web: `lib/chat-fork.ts`). + * + * omp only branches at a *user* entry. An assistant reply therefore forks at + * the NEXT user prompt (keeping the reply in the new session's history); the + * newest reply falls back to its own turn's prompt with edit-and-resend. + * A row whose turn has no usable prompt, or that resolves to the very first + * prompt of the session, gets no target. + */ +object ForkTargets { + + fun resolve(messages: List, entryIds: List): List { + val roles = messages.map { message -> + when { + message is ChatMessage.User && message.branchSummary -> "branchSummary" + else -> message.role + } + } + val targets = MutableList(roles.size) { null } + + var nextUserEntryId: String? = null + for (index in roles.indices.reversed()) { + when (roles[index]) { + "user" -> nextUserEntryId = entryIds.getOrNull(index) + "assistant" -> { + val target = nextUserEntryId + if (target != null) targets[index] = ForkTarget(target, editPrompt = false) + } + } + } + + var ownUserEntryId: String? = null + var ownIsFirstPrompt = false + for (index in roles.indices) { + if (roles[index] == "user") { + ownIsFirstPrompt = index == 0 + ownUserEntryId = entryIds.getOrNull(index) + } + if (targets[index] != null) continue + val own = ownUserEntryId + if (own == null || ownIsFirstPrompt) continue + if (roles[index] == "user" || roles[index] == "assistant") { + targets[index] = ForkTarget(own, editPrompt = true) + } + } + return targets + } +} + +private val ChatMessage.role: String + get() = when (this) { + is ChatMessage.User -> "user" + is ChatMessage.Assistant -> "assistant" + is ChatMessage.ToolResult -> "toolResult" + is ChatMessage.BashExecution -> "bashExecution" + is ChatMessage.Custom -> "custom" + is ChatMessage.Other -> role + } diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/GenerationSpeed.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/GenerationSpeed.kt new file mode 100644 index 000000000..82188ab39 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/GenerationSpeed.kt @@ -0,0 +1,147 @@ +package io.github.kahme247.ompweb.chat + +import java.util.Locale +import kotlin.math.roundToLong + +/** + * The generation rate of one finished assistant message, derived from omp's + * own bookkeeping (`usage.output`, `duration`, `ttft`, `completedAt`). + * + * [spanMs] is the net generation time: time to first token is excluded, so the + * value describes the speed at which the model produced the answer, not how + * long the request waited. + */ +data class MessageSpeed( + val tokensPerSecond: Double, + val outputTokens: Int, + val spanMs: Long, +) + +/** Below this net generation span a rate is noise (omp's `MIN_DURATION_MS`). */ +const val MIN_MESSAGE_SPAN_MS = 100L + +/** Live estimate: sliding window over the streamed token estimate. */ +const val STREAM_WINDOW_MS = 3_000L + +/** The window needs this much wall time before it may show a value. */ +const val STREAM_MIN_SPAN_MS = 700L + +/** And this many estimated tokens (fewer would amplify jitter). */ +const val STREAM_MIN_TOKENS = 12.0 + +/** No delta for this long means the stream is not producing: hide the rate. */ +const val STREAM_STALE_MS = 2_000L + +/** The UI is refreshed at most this often (2×/s), regardless of delta rate. */ +const val STREAM_RATE_PUBLISH_MS = 500L + +private const val STREAM_MAX_SAMPLES = 256 + +/** + * The final rate of [message], or null when the data cannot carry an honest + * number (error, empty output, sub-100 ms generation, missing timestamps). + */ +fun messageSpeed(message: ChatMessage.Assistant): MessageSpeed? { + if (message.stopReason == "error") return null + val output = message.usageOutput ?: return null + if (output <= 0) return null + val duration = message.durationMs?.takeIf { it.isFinite() && it > 0.0 } + val ttft = message.ttftMs?.takeIf { it.isFinite() && it >= 0.0 } + val completedAt = message.completedAt + val timestamp = message.timestamp + val span = when { + duration != null && ttft != null && ttft < duration -> duration - ttft + duration != null -> duration + completedAt != null && timestamp != null && completedAt > timestamp -> + (completedAt - timestamp).toDouble() + else -> return null + } + if (!span.isFinite() || span < MIN_MESSAGE_SPAN_MS) return null + val rate = output * 1000.0 / span + if (!rate.isFinite() || rate <= 0.0) return null + return MessageSpeed(tokensPerSecond = rate, outputTokens = output, spanMs = span.roundToLong()) +} + +/** + * Estimated tokens of a streaming snapshot: ASCII code points count a quarter, + * every other code point counts one. It is a deliberately cheap stand-in for a + * tokenizer; the committed `usage.output` corrects it at the end of the + * message. Tool-call JSON is not counted (bursty, expensive to measure). + */ +fun estimateStreamTokens(blocks: List): Double { + var ascii = 0 + var wide = 0 + for (block in blocks) { + val text = when (block) { + is AssistantBlock.Text -> block.text + is AssistantBlock.Thinking -> block.thinking + else -> null + } ?: continue + var index = 0 + while (index < text.length) { + val codePoint = text.codePointAt(index) + if (codePoint <= 0x7F) ascii++ else wide++ + index += Character.charCount(codePoint) + } + } + return ascii / 4.0 + wide +} + +/** + * Turns the growing token estimate of one streaming message into a rate. + * + * The first observation starts the window, so the time to first token never + * dilutes the number. A regression (new message, retry, reconnect) starts + * over from the new value. + */ +class StreamRateTracker( + private val windowMs: Long = STREAM_WINDOW_MS, + private val minSpanMs: Long = STREAM_MIN_SPAN_MS, + private val minTokens: Double = STREAM_MIN_TOKENS, + private val staleMs: Long = STREAM_STALE_MS, +) { + private data class Sample(val at: Long, val tokens: Double) + + /** Cumulative estimates, oldest first; bounded to [STREAM_MAX_SAMPLES]. */ + private val samples = ArrayDeque() + + /** Feed one snapshot; [totalTokens] counts from the start of the message. */ + fun observe(totalTokens: Double, at: Long) { + if (!totalTokens.isFinite() || totalTokens <= 0.0) return + val last = samples.lastOrNull() + if (last != null && at <= last.at) return + // No new tokens: keep the original delta timestamp, so a stalled + // stream ages out instead of being kept alive by repeated polls + // (web `StreamRateTracker.observe`). + if (last != null && totalTokens == last.tokens) return + if (last != null && totalTokens < last.tokens) samples.clear() + samples.addLast(Sample(at, totalTokens)) + while (samples.size > STREAM_MAX_SAMPLES) samples.removeFirst() + } + + /** The rate at [at], or null while it is not measurable (yet/anymore). */ + fun rate(at: Long): Double? { + val newest = samples.lastOrNull() ?: return null + if (at - newest.at > staleMs) return null + val cutoff = at - windowMs + var anchor = samples.first() + for (sample in samples) { + if (sample.at > cutoff) break + anchor = sample + } + val span = newest.at - anchor.at + if (span < minSpanMs) return null + val delta = newest.tokens - anchor.tokens + if (delta < minTokens) return null + val rate = delta * 1000.0 / span + if (!rate.isFinite() || rate <= 0.0) return null + return rate + } + + fun reset() { + samples.clear() + } +} + +/** `39.7627` → `"39.8"`; the locale-independent one-decimal form of a rate. */ +fun formatGenerationRate(rate: Double): String = String.format(Locale.US, "%.1f", rate) diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/ScrollLogic.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ScrollLogic.kt new file mode 100644 index 000000000..63e3cfa8c --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/ScrollLogic.kt @@ -0,0 +1,154 @@ +package io.github.kahme247.ompweb.chat + +/** + * Paging state of the transcript's "load earlier" trigger (web + * `lib/chat-lazy-load.ts` and the `ChatWindow` IntersectionObserver). + * + * Auto-paging starts only after the reader reached the tail once, and only + * from a re-armed state: prepending one window moves the first visible row + * away from index 0, which re-arms the trigger. Without that latch every + * content change at the top would prepend another window (cascade). + */ +data class LoadEarlierState( + val tailReached: Boolean = false, + val armed: Boolean = false, +) + +data class LoadEarlierDecision(val expand: Boolean, val state: LoadEarlierState) + +/** + * One measurement of the laid-out transcript tail, all values in pixels: the + * lazy item count, the bottom-most visible item and the viewport's end. + * Produced after a measure pass, so it describes what is really on screen — + * unlike the message list, which changes before the list has laid out. + */ +data class TailMeasure( + val totalItems: Int, + val lastVisibleIndex: Int, + val lastVisibleEnd: Int, + val viewportEnd: Int, +) + +/** + * Pure decisions behind the transcript scroll: the load-earlier latch, the + * lazy-item mapping used to keep the reader's row in place when a window is + * prepended, and the follow-the-tail latch. + */ +object ScrollLogic { + + /** + * One observation of the first visible lazy item. + * + * @param firstVisibleItemIndex 0-based lazy item index (`0` = top). + * @param windowStart first transcript row that is currently rendered. + * @param nearBottom the viewport sits at the tail. + */ + fun decideLoadEarlier( + firstVisibleItemIndex: Int, + windowStart: Int, + nearBottom: Boolean, + state: LoadEarlierState, + ): LoadEarlierDecision { + val tailReached = state.tailReached || nearBottom + if (firstVisibleItemIndex != 0) { + return LoadEarlierDecision( + expand = false, + state = LoadEarlierState(tailReached = tailReached, armed = true), + ) + } + val expand = tailReached && state.armed && !nearBottom && windowStart > 0 + return LoadEarlierDecision( + expand = expand, + state = LoadEarlierState( + tailReached = tailReached, + armed = if (expand) false else state.armed, + ), + ) + } + + /** + * Lazy item index of absolute row [rowIndex] for a window starting at + * [windowStart], below [leadItems] header items that are always on top (the + * extension status band, the compacted-history notice). + */ + fun itemIndexForRow(rowIndex: Int, windowStart: Int, leadItems: Int = 0): Int = + (rowIndex - windowStart).coerceAtLeast(0) + (if (windowStart > 0) 1 else 0) + leadItems + + /** Item index that shows the first row of a freshly loaded earlier window. */ + fun revealLoadedItem(windowStart: Int, leadItems: Int = 0): Int = + if (windowStart > 0) 1 + leadItems else leadItems + + /** + * The absolute row shown by lazy item [itemIndex] — the inverse of + * [itemIndexForRow], used to persist the reading position. A header item + * (the leading band/notice or the "load earlier" row) maps to the first row + * of the window. + */ + fun rowIndexForItem(itemIndex: Int, windowStart: Int, leadItems: Int = 0): Int { + val firstRowItem = leadItems + if (windowStart > 0) 1 else 0 + return (itemIndex - firstRowItem).coerceAtLeast(0) + windowStart + } + + /** + * The viewport shows the transcript's last item (the tail anchor) with its + * bottom at or above the viewport end — the reader is at the very bottom. + */ + fun tailAtBottom(measure: TailMeasure): Boolean = + measure.totalItems > 0 && + measure.lastVisibleIndex == measure.totalItems - 1 && + measure.lastVisibleEnd <= measure.viewportEnd + + /** + * The measured layout left the tail outside the viewport (new content grew + * below, a taller row was laid out, the keyboard or a panel shrank the + * viewport). Only a measured layout can answer this: the composable's + * state changes before the list has measured the new content. + */ + fun needsTailScroll(measure: TailMeasure): Boolean = + measure.totalItems > 0 && !tailAtBottom(measure) + + /** + * Whether the reader sits within [thresholdPx] of the tail. The tail + * anchor is the last item; while the reader is above the bottom the anchor + * is out of view and the item above it must end within the threshold of the + * viewport, either above it (the anchor is just below) or below it (the + * item is tall and crosses the viewport end). A list without a measured + * layout counts as the tail, exactly like the viewport's initial state. + */ + fun isNearBottom(measure: TailMeasure, thresholdPx: Int): Boolean { + if (measure.totalItems == 0 || measure.lastVisibleIndex < 0) return true + if (measure.lastVisibleIndex >= measure.totalItems - 1) return true + if (measure.lastVisibleIndex != measure.totalItems - 2) return false + val gap = measure.lastVisibleEnd - measure.viewportEnd + return gap <= thresholdPx && gap >= -thresholdPx + } + + /** + * Whether following the tail resumes after a saved reading position + * ([restoredRow], counted over [rowCount] rows) was restored: only when the + * position is the newest row or the measured layout already sits at the + * tail. Any other position belongs to the reader and must stay put while + * the answer streams. + */ + fun followAfterRestore( + restoredRow: Int, + rowCount: Int, + measure: TailMeasure, + thresholdPx: Int, + ): Boolean = restoredRow >= rowCount - 1 || isNearBottom(measure, thresholdPx) + + /** + * Render window a rebuilt transcript surface starts in. + * + * A configuration change (rotation, fold, window resize) recreates the + * composable: the lazy list restores its own first item and offset from the + * saved instance state, so the window those indices were rendered from must + * survive with them or the same index addresses a different row. A window + * saved for a longer transcript (branch switch, compaction) is clamped to + * the rows that still exist. + */ + fun clampWindow(windowStart: Int, rowCount: Int): Int { + if (rowCount <= 0) return 0 + return windowStart.coerceIn(0, rowCount - 1) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/SubagentAsyncResults.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/SubagentAsyncResults.kt new file mode 100644 index 000000000..3de990bc8 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/SubagentAsyncResults.kt @@ -0,0 +1,69 @@ +package io.github.kahme247.ompweb.chat + +import io.github.kahme247.ompweb.domain.SubagentInfo +import io.github.kahme247.ompweb.domain.SubagentProgress +import io.github.kahme247.ompweb.domain.SubagentStatus +import kotlinx.serialization.json.JsonObject + +/** + * Settle roster entries from the transcript's background-job deliveries. + * + * A detached `task` is recorded as `started` when it is spawned and never gets + * a lifecycle frame once it outlives its turn, so the on-disk roster would keep + * calling the finished run "Running" (and show no duration when no progress + * frame was recorded). The `async-result` message that delivers the job's + * output carries its terminal state and duration — that is the proof the run + * ended. + */ +fun settleSubagentsFromAsyncResults( + subagents: List, + messages: List, +): List { + if (subagents.isEmpty()) return subagents + val outcomes = asyncJobOutcomes(messages) + if (outcomes.isEmpty()) return subagents + var changed = false + val settled = subagents.map { entry -> + val outcome = outcomes[entry.id] ?: return@map entry + // Never regress an entry a live frame already settled. + if (entry.status != SubagentStatus.STARTED) return@map entry + changed = true + entry.copy( + status = outcome.status, + progress = (entry.progress ?: SubagentProgress()).copy( + status = outcome.status, + durationMs = outcome.durationMs ?: entry.progress?.durationMs, + ), + ) + } + return if (changed) settled else subagents +} + +/** Terminal state of one delivered background job. */ +internal data class AsyncJobOutcome(val status: SubagentStatus, val durationMs: Long?) + +/** + * Terminal `task` jobs named by the transcript's `async-result` messages, + * keyed by the roster id the delivery names (`agentId`, else `jobId`). + * Running or failed deliveries without a terminal state are not outcomes. + */ +internal fun asyncJobOutcomes(messages: List): Map { + val outcomes = LinkedHashMap() + for (message in messages) { + if (message !is ChatMessage.Custom || message.customType != "async-result") continue + val jobs = message.details?.array("jobs") ?: continue + for (element in jobs) { + val job = element as? JsonObject ?: continue + if (job.string("type") != "task") continue + val status = when (job.string("status")) { + "completed" -> SubagentStatus.COMPLETED + "failed" -> SubagentStatus.FAILED + "aborted" -> SubagentStatus.ABORTED + else -> continue + } + val id = job.string("agentId") ?: job.string("jobId") ?: continue + outcomes[id] = AsyncJobOutcome(status, job.long("durationMs")) + } + } + return outcomes +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/TranscriptFormat.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/TranscriptFormat.kt new file mode 100644 index 000000000..5ad9ba584 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/TranscriptFormat.kt @@ -0,0 +1,327 @@ +package io.github.kahme247.ompweb.chat + +import io.github.kahme247.ompweb.domain.shortModel +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId + +/** Which of the four date shapes a transcript timestamp needs (web `formatTime`). */ +enum class TimeBucket { Today, Yesterday, SameYear, OlderYear } + +/** Which label a transcript day separator carries. */ +enum class DayBucket { Today, Yesterday, Date } + +/** Who wrote a transcript block; drives the compact message header. */ +enum class SpeakerRole { You, Assistant, Subagent } + +/** + * The speaker of one transcript block. [name] is the model's short name (web + * `modelNames`) or the subagent's agent name; `null` means the UI falls back to + * the role's localized default. + */ +data class Speaker(val role: SpeakerRole, val name: String? = null) { + /** The single character of the round role badge. */ + val initial: Char + get() = name?.firstOrNull()?.uppercaseChar() ?: when (role) { + SpeakerRole.You -> 'Y' + SpeakerRole.Assistant -> 'A' + SpeakerRole.Subagent -> 'S' + } +} + +/** One transcript block that can carry a header, in render order. */ +data class HeaderBlock( + val key: String, + val speaker: Speaker?, + val timestamp: Long?, +) + +/** Where a block's header goes. Null speakers never show one. */ +data class HeaderSlot( + val key: String, + val showHeader: Boolean, + val speaker: Speaker?, + val timestamp: Long?, +) + +/** + * Pure transcript formatting: time buckets, day separators and speaker header + * grouping. Everything that needs the platform's locale-aware formatters stays + * in `ui.chat` (`ChatTimeFormatter`); this object only decides *which* shape a + * timestamp needs and *whether* a header or separator is drawn, so the rules + * are JVM-testable. + */ +object TranscriptFormat { + + /** Suffix of a turn group's assistant header block key. */ + const val ASSISTANT_HEADER_SUFFIX = ":assistant" + + /** The model name a header falls back to when the message has no model. */ + fun speakerOf(message: ChatMessage): Speaker? = when (message) { + is ChatMessage.User -> if (message.branchSummary) null else Speaker(SpeakerRole.You) + is ChatMessage.Assistant -> Speaker( + SpeakerRole.Assistant, + shortModel(message.model ?: message.provider), + ) + else -> null + } + + /** Every header-capable block of the rendered rows, in render order. */ + fun headerBlocks(rows: List): List = buildList { + for (row in rows) { + when (row) { + is TranscriptRow.Message -> + add(HeaderBlock(row.key, speakerOf(row.message), row.message.timestamp)) + is TranscriptRow.Group -> { + // A turn group renders its user anchor, then one assistant block. + add(HeaderBlock(row.key, speakerOf(row.anchor), row.anchor.timestamp)) + add(assistantHeaderBlock(row)) + } + } + } + } + + private fun assistantHeaderBlock(group: TranscriptRow.Group): HeaderBlock { + var firstAssistant: ChatMessage.Assistant? = null + for (segment in group.segments) { + when (segment) { + is TurnSegment.Text -> { + firstAssistant = segment.message + } + is TurnSegment.Activity -> { + firstAssistant = segment.pieces + .map { it.message } + .filterIsInstance() + .firstOrNull() + } + } + if (firstAssistant != null) break + } + val message = firstAssistant + return HeaderBlock( + key = group.key + ASSISTANT_HEADER_SUFFIX, + speaker = message?.let(::speakerOf) ?: Speaker(SpeakerRole.Assistant), + timestamp = message?.timestamp ?: group.anchor.timestamp, + ) + } + + /** + * A header is drawn once per run of blocks by the same speaker, and a new + * calendar day always starts a new header. Day separators are a per-element + * concern of the rendered transcript ([daySeparators]), so a change of day + * inside one turn or in the live tail is never lost. + */ + fun headerPlan( + blocks: List, + zone: ZoneId = ZoneId.systemDefault(), + ): List { + var previousSpeaker: Speaker? = null + var previousDay: LocalDate? = null + return blocks.mapIndexed { index, block -> + val day = block.timestamp?.let { dayOf(it, zone) } + val startsDay = index > 0 && day != null && previousDay != null && day != previousDay + val showHeader = block.speaker != null && + (previousSpeaker == null || block.speaker != previousSpeaker || startsDay) + if (block.speaker != null) previousSpeaker = block.speaker + if (day != null) previousDay = day + HeaderSlot( + key = block.key, + showHeader = showHeader, + speaker = block.speaker, + timestamp = block.timestamp, + ) + } + } + + /** [headerPlan] keyed by block, ready for a keyed lookup from the renderer. */ + fun headerSlots( + blocks: List, + zone: ZoneId = ZoneId.systemDefault(), + ): Map = headerPlan(blocks, zone).associateBy { it.key } + + // ------------------------------------------------------------ visibility + + /** + * Whether the transcript draws [message] at all. A hidden assistant + * message (thinking-only with "hide thinking", an empty reply, a tool + * result) must not consume a speaker header or a day separator. + */ + fun isVisible(message: ChatMessage, hideThinking: Boolean = false): Boolean = when (message) { + is ChatMessage.Assistant -> assistantVisible(message, hideThinking) + is ChatMessage.ToolResult, is ChatMessage.Other -> false + is ChatMessage.Custom -> when { + message.customType == PASSIVE_TOOL_CONTEXT || message.customType == MOUNT_NOTICE -> false + message.customType == "compaction" -> true + else -> message.display + } + is ChatMessage.User, is ChatMessage.BashExecution -> true + } + + /** + * Whether the assistant row renders: `AssistantRow` returns before its + * column when there is no displayable block, no error and no stream. + */ + fun assistantVisible(message: ChatMessage.Assistant, hideThinking: Boolean = false): Boolean { + if (message.errorMessage != null || message.streaming) return true + return TranscriptPlan.displayableBlocks(message).any { block -> + !(hideThinking && block is AssistantBlock.Thinking) + } + } + + /** + * The first assistant message in `[startIndex, endIndex)` that actually + * draws something. The live tail must hang its speaker header on that + * message, not on an invisible one above it. + */ + fun firstVisibleAssistantIndex( + messages: List, + startIndex: Int, + endIndex: Int, + hideThinking: Boolean = false, + ): Int? { + for (index in startIndex until endIndex.coerceAtMost(messages.size)) { + val message = messages[index] as? ChatMessage.Assistant ?: continue + if (assistantVisible(message, hideThinking)) return index + } + return null + } + + // -------------------------------------------------------- day separators + + /** + * Every place the folded transcript changes calendar day, keyed by the + * element that draws the separator: a row key, a group's assistant header + * key, or a turn segment key. Derived from the rendered sequence, not from + * the header blocks, so a change inside one turn is a separator of its own. + * The first element of the window never draws one. + */ + fun daySeparators( + rows: List, + hideThinking: Boolean = false, + zone: ZoneId = ZoneId.systemDefault(), + ): Map { + val elements = LinkedHashMap() + var previousDay: LocalDate? = null + + fun visit(key: String, timestamp: Long?) { + if (timestamp == null) return + val day = dayOf(timestamp, zone) + if (previousDay != null && previousDay != day) elements[key] = timestamp + previousDay = day + } + + for (row in rows) { + when (row) { + is TranscriptRow.Message -> { + if (isVisible(row.message, hideThinking)) visit(row.key, row.message.timestamp) + } + is TranscriptRow.Group -> { + visit(row.key, row.anchor.timestamp) + val header = assistantHeaderBlock(row) + visit(row.key + ASSISTANT_HEADER_SUFFIX, header.timestamp) + for (segment in row.segments) { + when (segment) { + is TurnSegment.Text -> visit(segment.key, segment.message.timestamp) + is TurnSegment.Activity -> { + // The fold is one element: a day change inside it + // is a separator above the fold. + var breakAt: Long? = null + for (piece in segment.pieces) { + val timestamp = piece.message.timestamp ?: continue + val day = dayOf(timestamp, zone) + if (breakAt == null && previousDay != null && previousDay != day) { + breakAt = timestamp + } + previousDay = day + } + if (breakAt != null) elements[segment.key] = breakAt + } + } + } + } + } + } + return elements + } + + /** + * Day separators of the flat live tail, keyed by message key. The run + * starts at [startIndex]'s day, so its anchor never repeats a separator the + * row above it already drew; invisible messages are skipped like the tail + * does. + */ + fun messageDaySeparators( + messages: List, + startIndex: Int, + endIndex: Int, + hideThinking: Boolean = false, + zone: ZoneId = ZoneId.systemDefault(), + ): Map { + val breaks = LinkedHashMap() + var previousDay: LocalDate? = messages.getOrNull(startIndex)?.timestamp?.let { dayOf(it, zone) } + for (index in (startIndex + 1) until endIndex.coerceAtMost(messages.size)) { + val message = messages[index] + if (!isVisible(message, hideThinking)) continue + val timestamp = message.timestamp ?: continue + val day = dayOf(timestamp, zone) + if (previousDay != null && previousDay != day) breaks[message.key] = timestamp + previousDay = day + } + return breaks + } + + fun timeBucket( + timestamp: Long, + now: Long, + zone: ZoneId = ZoneId.systemDefault(), + ): TimeBucket { + val day = dayOf(timestamp, zone) + val today = dayOf(now, zone) + return when { + day == today -> TimeBucket.Today + day == today.minusDays(1) -> TimeBucket.Yesterday + day.year == today.year -> TimeBucket.SameYear + else -> TimeBucket.OlderYear + } + } + + fun dayBucket( + timestamp: Long, + now: Long, + zone: ZoneId = ZoneId.systemDefault(), + ): DayBucket = when (timeBucket(timestamp, now, zone)) { + TimeBucket.Today -> DayBucket.Today + TimeBucket.Yesterday -> DayBucket.Yesterday + TimeBucket.SameYear, TimeBucket.OlderYear -> DayBucket.Date + } + + /** + * `12 s`, `3 min 05 s`, `1 h 04 min`: the short duration an activity row + * shows for a running call and, once it settles, its final duration. + */ + fun formatElapsedShort(elapsedMs: Long): String { + val total = elapsedMs.coerceAtLeast(0) / 1000 + val hours = total / 3600 + val minutes = (total % 3600) / 60 + val seconds = total % 60 + return when { + hours > 0 -> "$hours h ${minutes.toString().padStart(2, '0')} min" + minutes > 0 -> "$minutes min ${seconds.toString().padStart(2, '0')} s" + else -> "$seconds s" + } + } + + /** + * When the running turn started. A run this client sent knows its prompt + * time, so a tool start or a steering message cannot reset the timer; a + * run observed from outside falls back to its earliest running tool, then + * to the newest prompt. + */ + fun turnStartedAt(state: ChatState): Long? = + state.runStartedAt + ?: state.liveTools.filter { it.endedAt == null }.minOfOrNull { it.startedAt } + ?: state.messages.lastOrNull { it is ChatMessage.User }?.timestamp + + private fun dayOf(timestamp: Long, zone: ZoneId): LocalDate = + Instant.ofEpochMilli(timestamp).atZone(zone).toLocalDate() +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/chat/TranscriptPlan.kt b/android/app/src/main/java/io/github/kahme247/ompweb/chat/TranscriptPlan.kt new file mode 100644 index 000000000..da665878a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/chat/TranscriptPlan.kt @@ -0,0 +1,255 @@ +package io.github.kahme247.ompweb.chat + +/** + * Lightweight transcript row planner (web: `lib/chat-transcript-plan.ts`). + * + * A user message (or a compaction summary) anchors a turn. The turn renders + * as its anchor followed by text segments; runs of activity between text + * (thinking, tool calls, notices) fold into one collapsed "Process details" + * row. Text is never hidden, so an answer stays visible while its tools fold + * away. + */ +sealed interface TranscriptRow { + val key: String + + /** A message rendered on its own (before the first anchor, or a turn with nothing to fold). */ + data class Message(val message: ChatMessage) : TranscriptRow { + override val key: String = "message:${message.key}" + } + + data class Group( + val anchor: ChatMessage, + val anchorIndex: Int, + val endIndex: Int, + val segments: List, + ) : TranscriptRow { + override val key: String = "turn:${anchor.key}" + } +} + +sealed interface TurnSegment { + /** Visible assistant text/images from one message; `last` carries usage and the error. */ + data class Text( + val message: ChatMessage.Assistant, + val messageIndex: Int, + val blocks: List, + val last: Boolean, + val key: String, + ) : TurnSegment + + data class Activity( + val pieces: List, + val stepCount: Int, + val toolCallCount: Int, + val key: String, + ) : TurnSegment +} + +/** + * A folded activity entry: the listed blocks of an assistant message, or a + * whole non-assistant message. + */ +data class ActivityPiece( + val message: ChatMessage, + val index: Int, + val blocks: List? = null, +) + +const val PASSIVE_TOOL_CONTEXT = "passive-tool-context" +const val MOUNT_NOTICE = "xdev-mount-notice" + +object TranscriptPlan { + + fun isGroupAnchor(message: ChatMessage): Boolean = + message is ChatMessage.User || + (message is ChatMessage.Custom && message.customType == "compaction") + + fun build(messages: List, hideThinking: Boolean = false): List { + val rows = ArrayList() + var index = 0 + while (index < messages.size) { + val anchor = messages[index] + if (!isGroupAnchor(anchor)) { + rows += TranscriptRow.Message(anchor) + index += 1 + continue + } + val end = turnEnd(messages, index) + val segments = planTurnSegments(messages, index, end, hideThinking) + if (segments.any { it is TurnSegment.Activity }) { + rows += TranscriptRow.Group(anchor, index, end, segments) + } else { + // Nothing to fold: render anchor and the rest as standalone messages. + for (renderIndex in index until end) rows += TranscriptRow.Message(messages[renderIndex]) + } + index = end + } + return rows + } + + private fun turnEnd(messages: List, start: Int): Int { + var end = start + 1 + while (end < messages.size && !isGroupAnchor(messages[end])) end += 1 + return end + } + + fun planTurnSegments( + messages: List, + anchorIndex: Int, + endIndex: Int, + hideThinking: Boolean = false, + ): List { + val segments = ArrayList() + val anchorKey = messages[anchorIndex].key + var activityIndex = -1 + + fun addActivity(piece: ActivityPiece, toolCalls: Int) { + if (activityIndex < 0) { + activityIndex = segments.size + segments += TurnSegment.Activity( + pieces = emptyList(), + stepCount = 0, + toolCallCount = 0, + key = "activity:$anchorKey:$activityIndex", + ) + } + val current = segments[activityIndex] as TurnSegment.Activity + segments[activityIndex] = current.copy( + pieces = current.pieces + piece, + stepCount = current.stepCount + 1, + toolCallCount = current.toolCallCount + toolCalls, + ) + } + + for (index in anchorIndex + 1 until endIndex) { + val message = messages[index] + // Tool results render inline under their tool call inside the fold. + if (message is ChatMessage.ToolResult) continue + // Mount notices never render; passive tool context rides on its tool card. + if (message is ChatMessage.Custom && (message.customType == MOUNT_NOTICE || message.customType == PASSIVE_TOOL_CONTEXT)) continue + if (message !is ChatMessage.Assistant) { + addActivity(ActivityPiece(message, index), 0) + continue + } + var blocks = displayableBlocks(message) + if (hideThinking) blocks = blocks.filter { it !is AssistantBlock.Thinking } + var lastTextIndex = -1 + var text = ArrayList() + var other = ArrayList() + + fun flushText() { + if (text.isEmpty()) return + lastTextIndex = segments.size + segments += TurnSegment.Text( + message = message, + messageIndex = index, + blocks = text, + last = false, + key = "text:${message.key}:$lastTextIndex", + ) + activityIndex = -1 + text = ArrayList() + } + + fun flushOther() { + if (other.isEmpty()) return + addActivity( + ActivityPiece(message, index, other), + other.count { it is AssistantBlock.ToolCall }, + ) + other = ArrayList() + } + + for (block in blocks) { + when { + block is AssistantBlock.Image || + (block is AssistantBlock.Text && block.text.trim().isNotEmpty()) -> { + flushOther() + text += block + } + block !is AssistantBlock.Text -> { + flushText() + other += block + } + } + } + flushText() + flushOther() + // A provider error must stay visible even when the message ended in activity. + if (!message.errorMessage.isNullOrBlank() && segments.lastIndex != lastTextIndex) { + lastTextIndex = segments.size + segments += TurnSegment.Text( + message = message, + messageIndex = index, + blocks = emptyList(), + last = false, + key = "text:${message.key}:$lastTextIndex", + ) + activityIndex = -1 + } + if (lastTextIndex >= 0 && segments.lastIndex == lastTextIndex) { + val segment = segments[lastTextIndex] as TurnSegment.Text + segments[lastTextIndex] = segment.copy(last = true) + } + } + return segments + } + + /** Drop placeholder thinking blocks the server did not send with content. */ + fun displayableBlocks(message: ChatMessage.Assistant): List = + message.blocks.filterNot { block -> + block is AssistantBlock.Thinking && !block.deferred && !message.streaming && block.thinking.isBlank() + } + + /** + * Index committed tool results by tool call id and attach each passive + * developer context to the last tool call of the batch it follows. + */ + fun collectToolResults(messages: List): Map { + val results = LinkedHashMap() + var lastToolCallId: String? = null + for (message in messages) { + if (message is ChatMessage.ToolResult) { + results[message.toolCallId] = message + continue + } + if (message is ChatMessage.Assistant) { + lastToolCallId = null + for (block in message.blocks) { + if (block is AssistantBlock.ToolCall) lastToolCallId = block.toolCallId + } + continue + } + val attached = lastToolCallId + val result = attached?.let(results::get) + lastToolCallId = null + if (result == null || result.passiveContext != null) continue + if (message !is ChatMessage.Custom || message.customType != PASSIVE_TOOL_CONTEXT) continue + val text = sanitizePassiveToolContext(message.text) + if (text.isEmpty()) continue + results[result.toolCallId] = result.copy(passiveContext = text) + } + return results + } + + private const val PASSIVE_TOOL_CONTEXT_MAX = 2000 + + /** Display text without ANSI/control/zero-width/bidi characters. */ + fun sanitizePassiveToolContext(raw: String): String { + val stripped = raw + .replace(ANSI_ESCAPE, "") + .replace(CONTROL_CHARS, "") + return stripped + .split("\n") + .map { it.trimEnd() } + .joinToString("\n") + .replace(Regex("\n{3,}"), "\n\n") + .trimStart('\n') + .trimEnd() + .take(PASSIVE_TOOL_CONTEXT_MAX) + .trimEnd() + } + + private val ANSI_ESCAPE = Regex("\u001B\\[[0-9;?]*[ -/]*[@-~]|\u001B\\][^\u0007]*\u0007") + private val CONTROL_CHARS = Regex("[\\x00-\\x08\\x0B-\\x1F\\x7F-\\x9F\\u200B-\\u200F\\u202A-\\u202E\\u2066-\\u2069]") +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/data/ChatSettings.kt b/android/app/src/main/java/io/github/kahme247/ompweb/data/ChatSettings.kt new file mode 100644 index 000000000..99ab780dd --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/data/ChatSettings.kt @@ -0,0 +1,65 @@ +package io.github.kahme247.ompweb.data + +import io.github.kahme247.ompweb.domain.ActiveGoal +import io.github.kahme247.ompweb.domain.SubmitDuringRunBehavior +import io.github.kahme247.ompweb.domain.ToolPreset +import kotlinx.coroutines.flow.StateFlow + +/** + * The settings the chat view model reads and writes, separated from the + * Android DataStore implementation so the view model stays JVM-testable. + */ +interface ChatSettings { + + val snapshot: StateFlow + + suspend fun setChatScrollPosition(sessionId: String, rowIndex: Int) + + /** + * Persists a scroll position from a non-coroutine context (`onCleared`). + * The implementation updates its snapshot synchronously and dispatches + * the DataStore write itself. + */ + fun persistChatScrollPosition(sessionId: String, rowIndex: Int) + + /** `omp-web:submit-during-run`: what Enter does while the agent runs. */ + suspend fun setSubmitDuringRun(behavior: SubmitDuringRunBehavior) + + /** `omp-web:word-completion` wire value: `auto`, `on` or `off`. */ + suspend fun setWordCompletion(mode: String) + + /** `omp-sound-enabled`: the completion tone. */ + suspend fun setSoundEnabled(enabled: Boolean) + + /** The session's active `/goal`, or null to clear it. */ + suspend fun setActiveGoal(sessionId: String, goal: ActiveGoal?) + + /** `omp-web:composer-todo-collapsed`: the attached todo panel's body. */ + suspend fun setComposerTodoCollapsed(collapsed: Boolean) + + /** `omp-web:composer-subagents-collapsed`: the attached roster's body. */ + suspend fun setComposerSubagentsCollapsed(collapsed: Boolean) + + /** `omp-web:open-url-automatically`: open the viewed session's links directly. */ + suspend fun setOpenUrlAutomatically(enabled: Boolean) + + /** Spawn-time tool preset (shared with the new-session pane). */ + suspend fun setToolPreset(preset: ToolPreset) + + /** `omp-advisor-enabled:` for an existing session. */ + suspend fun setAdvisorEnabled(sessionId: String, enabled: Boolean) + + /** + * A session this process just created, with the advisor choice it was + * spawned with. The snapshot must carry the choice *synchronously*: the + * chat composer binds the new id right after creation, and the prefilled + * store (cold start) is the only source that can tell it the truth then. + */ + fun markSessionCreated(sessionId: String, advisor: Boolean) + + /** + * `omp-composer-models`: the models the composer offers. `null` clears the + * stored selection, which means every model is offered. + */ + suspend fun setComposerModels(models: Set?) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/data/CookieCipher.kt b/android/app/src/main/java/io/github/kahme247/ompweb/data/CookieCipher.kt new file mode 100644 index 000000000..300390ef3 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/data/CookieCipher.kt @@ -0,0 +1,137 @@ +package io.github.kahme247.ompweb.data + +import java.util.Base64 +import java.security.GeneralSecurityException +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +/** + * Encrypts the stored session cookie. + * + * Behind an interface so the framing can be unit-tested on the JVM with a + * software key while the app uses an Android Keystore key that never leaves + * the device (and is excluded from cloud backup with `allowBackup="false"`). + */ +interface CookieCipher { + fun encrypt(plaintext: ByteArray): ByteArray + fun decrypt(payload: ByteArray): ByteArray +} + +/** + * AES-GCM with a random IV per message. The payload is `iv || ciphertext || + * tag`; GCM's tag makes a tampered or foreign-key payload fail loudly instead + * of decrypting into garbage. + */ +class AesGcmCipher(private val key: () -> SecretKey) : CookieCipher { + + override fun encrypt(plaintext: ByteArray): ByteArray { + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, key()) + return cipher.iv + cipher.doFinal(plaintext) + } + + override fun decrypt(payload: ByteArray): ByteArray { + require(payload.size > IV_BYTES) { "Cookie payload is too short" } + val iv = payload.copyOfRange(0, IV_BYTES) + val body = payload.copyOfRange(IV_BYTES, payload.size) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, key(), GCMParameterSpec(TAG_BITS, iv)) + return cipher.doFinal(body) + } + + companion object { + const val TRANSFORMATION = "AES/GCM/NoPadding" + + /** Android Keystore and JDK both generate 12-byte GCM IVs by default. */ + const val IV_BYTES = 12 + const val TAG_BITS = 128 + } +} + +/** + * One cookie as it is persisted, together with the origin that set it + * (`scheme://host:port`). The origin tag is what keeps a cookie from a server + * the user left from ever being sent to the next one. + */ +@Serializable +data class StoredCookie( + val name: String, + val value: String, + val expiresAtMillis: Long, + val domain: String, + val path: String, + val secure: Boolean, + val httpOnly: Boolean, + /** Empty for records written before origins were stored; such rows never match. */ + val origin: String = "", +) + +/** Where [OriginBoundCookieJar] keeps its cookies between requests and launches. */ +interface CookieRecords { + fun load(): List + fun save(cookies: List) +} + +/** + * Persists cookies as one AES-GCM-encrypted JSON document. A missing file, a + * corrupt payload or a payload encrypted with another key (for example after + * the Keystore key was invalidated) is treated as "signed out", never as an + * error the user has to resolve. + */ +class EncryptedCookieRecords( + private val file: java.io.File, + private val cipher: CookieCipher, +) : CookieRecords { + + private val json = Json { ignoreUnknownKeys = true } + + @Synchronized + override fun load(): List { + if (!file.isFile) return emptyList() + return try { + val plain = cipher.decrypt(Base64.getDecoder().decode(file.readBytes())) + json.decodeFromString>(plain.toString(Charsets.UTF_8)) + } catch (_: GeneralSecurityException) { + emptyList() + } catch (_: IllegalArgumentException) { + emptyList() + } catch (_: java.io.IOException) { + emptyList() + } catch (_: kotlinx.serialization.SerializationException) { + emptyList() + } + } + + @Synchronized + override fun save(cookies: List) { + if (cookies.isEmpty()) { + file.delete() + if (file.exists()) { + // A failed delete must not resurrect a signed-out session + // after the next launch: overwrite with an encrypted empty list. + file.writeBytes(Base64.getEncoder().encode(cipher.encrypt(EMPTY_LIST))) + } + return + } + val plain = json.encodeToString(cookies).toByteArray(Charsets.UTF_8) + file.writeBytes(Base64.getEncoder().encode(cipher.encrypt(plain))) + } + + private companion object { + val EMPTY_LIST = "[]".toByteArray(Charsets.UTF_8) + } +} + +/** Cookie storage backed by a map; used by JVM tests. */ +class InMemoryCookieRecords(initial: List = emptyList()) : CookieRecords { + private var cookies: List = initial + + override fun load(): List = cookies + + override fun save(cookies: List) { + this.cookies = cookies + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/data/DraftStore.kt b/android/app/src/main/java/io/github/kahme247/ompweb/data/DraftStore.kt new file mode 100644 index 000000000..d171cfdff --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/data/DraftStore.kt @@ -0,0 +1,339 @@ +package io.github.kahme247.ompweb.data + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import io.github.kahme247.ompweb.domain.AttachedTextFile +import io.github.kahme247.ompweb.domain.ComposerImage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.contentOrNull + +private val Context.draftsDataStore: DataStore by preferencesDataStore(name = "ompweb-drafts") + +/** Everything the composer holds for one draft key (session id or `new:`). */ +data class ChatDraft( + val value: String = "", + val images: List = emptyList(), + val files: List = emptyList(), +) { + val hasAttachments: Boolean get() = images.isNotEmpty() || files.isNotEmpty() + + fun isEmpty(): Boolean = value.isEmpty() && !hasAttachments +} + +/** + * Content to put back into a composer. `replace` merges a later recovery with + * an earlier one in order: while the draft still starts with `lead` (the + * earlier block), it becomes `text`; otherwise only `fallback` is prepended + * (web `DraftRecovery`). + */ +data class DraftRecovery( + val text: String, + val replace: Replace? = null, + val images: List = emptyList(), +) { + data class Replace(val lead: String, val fallback: String) + + val isEmpty: Boolean get() = text.isEmpty() && images.isEmpty() +} + +data class DraftRecoveryEvent(val key: String, val recovery: DraftRecovery) + +/** + * The composer-facing surface of [DraftStore]. View models depend on this so + * JVM tests can keep drafts in memory without DataStore/Android. + */ +interface ComposerDraftStore { + val state: StateFlow> + val recoveries: SharedFlow + + fun getDraft(key: String): ChatDraft? + fun setDraft(key: String, draft: ChatDraft) + fun clearDraft(key: String) + fun recoverDraft(key: String, recovery: DraftRecovery) + + /** Drop every draft, e.g. on logout or a server switch. */ + fun clearAll() +} + +/** Web `mergeRecoveredText`. */ +fun mergeRecoveredText(current: String, recovery: DraftRecovery): String { + if (recovery.text.isEmpty()) return current + val replace = recovery.replace + if (replace != null && current.startsWith(replace.lead)) { + return recovery.text + current.substring(replace.lead.length) + } + val lead = replace?.fallback ?: recovery.text + return if (current.isEmpty()) lead else "$lead\n\n$current" +} + +/** + * In-memory draft table with the web's eviction rules: at most [maxDrafts] + * drafts, the first-inserted one is evicted when a new key arrives, and only + * text is persisted (attachment payloads would exhaust storage quotas). + * + * Insertion order is deliberately kept (a re-set keeps its slot), matching + * JavaScript `Map` semantics of `lib/draft-store.ts`. + */ +class DraftRegistry(initial: List> = emptyList(), private val maxDrafts: Int = MAX_DRAFTS) { + private val drafts = LinkedHashMap() + + init { + for ((key, text) in initial) { + if (key.isEmpty() || text.isEmpty()) continue + if (drafts.size >= maxDrafts) { + // Overflow from an older/foreign store: drop it, like the web reader. + continue + } + drafts[key] = ChatDraft(value = text) + } + } + + fun get(key: String): ChatDraft? = drafts[key]?.let(::cloneDraft) + + fun set(key: String, draft: ChatDraft) { + if (key.isEmpty()) return + if (draft.isEmpty()) { + clear(key) + return + } + if (!drafts.containsKey(key) && drafts.size >= maxDrafts) { + drafts.keys.firstOrNull()?.let { drafts.remove(it) } + } + drafts[key] = cloneDraft(draft) + } + + fun clear(key: String): Boolean = drafts.remove(key) != null + + fun clearAll() = drafts.clear() + + fun isEmpty(): Boolean = drafts.isEmpty() + + /** Persisted form: insertion-ordered `(key, text)` pairs, text-only. */ + fun textEntries(): List> = + drafts.entries.mapNotNull { (key, draft) -> draft.value.takeIf { it.isNotEmpty() }?.let { key to it } } + + fun snapshot(): Map = drafts.mapValues { (_, draft) -> cloneDraft(draft) } + + private fun cloneDraft(draft: ChatDraft): ChatDraft = ChatDraft( + value = draft.value, + images = draft.images.toList(), + files = draft.files.toList(), + ) + + companion object { + const val MAX_DRAFTS: Int = 50 + } +} + +/** + * Per-session composer drafts. Text survives process death (DataStore), while + * images and attached files stay in memory exactly like the web's + * `sessionStorage`-text-only rule. + */ +class DraftStore(context: Context, private val scope: CoroutineScope) : ComposerDraftStore { + + private val dataStore = context.applicationContext.draftsDataStore + private val registry = DraftRegistry() + private val _state = MutableStateFlow>(emptyMap()) + private val _recoveries = MutableSharedFlow(extraBufferCapacity = 8) + private val persistenceRequests = Channel(Channel.CONFLATED) + /** A clear that arrived before the disk read finished must not be undone by it. */ + private var clearedBeforeHydration = false + + /** The draft table, for hosts that restore a draft when it arrives. */ + override val state: StateFlow> = _state.asStateFlow() + + /** Recovery intents (stop/abort restoring queue entries); not replayed. */ + override val recoveries: SharedFlow = _recoveries.asSharedFlow() + + init { + scope.launch { + val stored = loadStored() + if (!clearedBeforeHydration) registry.mergeStored(stored) + publish() + } + scope.launch { + for (request in persistenceRequests) saveStored(registry.textEntries()) + } + } + + override fun getDraft(key: String): ChatDraft? = registry.get(key) + + override fun setDraft(key: String, draft: ChatDraft) { + registry.set(key, draft) + publish() + persistenceRequests.trySend(Unit) + } + + override fun clearDraft(key: String) { + if (registry.clear(key)) { + publish() + persistenceRequests.trySend(Unit) + } + } + + override fun clearAll() { + clearedBeforeHydration = true + registry.clearAll() + publish() + persistenceRequests.trySend(Unit) + } + + override fun recoverDraft(key: String, recovery: DraftRecovery) { + if (key.isNotEmpty() && !recovery.isEmpty) { + val current = registry.get(key) ?: ChatDraft() + registry.set( + key, + current.copy( + value = mergeRecoveredText(current.value, recovery), + images = current.images + recovery.images, + ), + ) + publish() + persistenceRequests.trySend(Unit) + } + // Publish the intent separately: ordinary persistence must not reapply it, + // and the mounted composer may have state updates still queued. + _recoveries.tryEmit(DraftRecoveryEvent(key, recovery)) + } + + private fun publish() { + _state.value = registry.snapshot() + } + + private suspend fun loadStored(): List> = try { + val raw = dataStore.data.first()[DRAFTS] + parseDraftEntries(raw) + } catch (_: Exception) { + emptyList() + } + + private suspend fun saveStored(entries: List>) { + try { + dataStore.edit { preferences -> + if (entries.isEmpty()) preferences.remove(DRAFTS) else preferences[DRAFTS] = encodeDraftEntries(entries) + } + } catch (_: Exception) { + // Keep the in-memory drafts; the next change retries. + } + } + + private companion object { + val DRAFTS = stringPreferencesKey("drafts") + val json = Json { ignoreUnknownKeys = true } + + fun encodeDraftEntries(entries: List>): String = + buildJsonArray { + entries.forEach { (key, text) -> + add(JsonArray(listOf(JsonPrimitive(key), JsonPrimitive(text)))) + } + }.toString() + + fun parseDraftEntries(raw: String?): List> { + if (raw.isNullOrBlank()) return emptyList() + return try { + (json.parseToJsonElement(raw) as? JsonArray) + ?.mapNotNull { element -> + val pair = element as? JsonArray ?: return@mapNotNull null + val key = (pair.getOrNull(0) as? JsonPrimitive)?.contentOrNull + val text = (pair.getOrNull(1) as? JsonPrimitive)?.contentOrNull + if (key.isNullOrEmpty() || text.isNullOrEmpty()) null else key to text + } + ?: emptyList() + } catch (_: Exception) { + emptyList() + } + } + } +} + +/** + * Merge drafts read from disk into the live table without clobbering a draft + * the user already started typing: stored text only fills missing keys, and a + * live key keeps its slot (and its attachments). + */ +internal fun DraftRegistry.mergeStored(stored: List>) { + for ((key, text) in stored) { + if (get(key) == null) set(key, ChatDraft(value = text)) + } +} + +/** + * Scopes every draft key to the current server origin, so drafts, recovered + * queue text and the exit guard never leak into another installation. The + * origin is read on every call; the published [state] follows the origin, so + * a server switch never leaves another installation's keys visible. Recovery + * events are mapped back to plain keys and events from another origin are + * dropped. [clearAll] clears every origin (logout / server switch). + */ +class OriginScopedDraftStore( + private val delegate: ComposerDraftStore, + scope: CoroutineScope, + private val origin: StateFlow, +) : ComposerDraftStore { + + private val _state = MutableStateFlow>(emptyMap()) + override val state: StateFlow> = _state.asStateFlow() + + private val _recoveries = MutableSharedFlow(extraBufferCapacity = 8) + override val recoveries: SharedFlow = _recoveries.asSharedFlow() + + init { + scope.launch { + combine(delegate.state, origin) { drafts, current -> + if (current.isNullOrEmpty()) { + emptyMap() + } else { + val prefix = prefixOf(current) + drafts.entries + .filter { it.key.startsWith(prefix) } + .associate { it.key.removePrefix(prefix) to it.value } + } + }.collect { _state.value = it } + } + scope.launch { + delegate.recoveries.collect { event -> + val prefix = prefix() + if (event.key.startsWith(prefix)) { + _recoveries.tryEmit(event.copy(key = event.key.removePrefix(prefix))) + } + } + } + } + + private fun prefix(): String = prefixOf(origin.value) + + private fun prefixOf(value: String?): String = + value?.takeIf { it.isNotEmpty() }?.let { "$it\u0000" }.orEmpty() + + private fun scoped(key: String): String = prefix() + key + + override fun getDraft(key: String): ChatDraft? = delegate.getDraft(scoped(key)) + + override fun setDraft(key: String, draft: ChatDraft) = delegate.setDraft(scoped(key), draft) + + override fun clearDraft(key: String) = delegate.clearDraft(scoped(key)) + + override fun recoverDraft(key: String, recovery: DraftRecovery) = + delegate.recoverDraft(scoped(key), recovery) + + override fun clearAll() = delegate.clearAll() +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/data/KeystoreCookieCipher.kt b/android/app/src/main/java/io/github/kahme247/ompweb/data/KeystoreCookieCipher.kt new file mode 100644 index 000000000..df5328869 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/data/KeystoreCookieCipher.kt @@ -0,0 +1,44 @@ +package io.github.kahme247.ompweb.data + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import java.security.KeyStore +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey + +/** + * The app's only cookie key: AES-256-GCM in the Android Keystore. The key + * material never leaves the device and is not part of any backup, so a copied + * settings file is useless without this device. + */ +class KeystoreCookieCipher : CookieCipher { + + private val delegate = AesGcmCipher(::key) + + override fun encrypt(plaintext: ByteArray): ByteArray = delegate.encrypt(plaintext) + + override fun decrypt(payload: ByteArray): ByteArray = delegate.decrypt(payload) + + private fun key(): SecretKey { + val store = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) } + val existing = (store.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry)?.secretKey + if (existing != null) return existing + val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE) + generator.init( + KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build(), + ) + return generator.generateKey() + } + + private companion object { + const val ANDROID_KEY_STORE = "AndroidKeyStore" + const val KEY_ALIAS = "ompweb-session-cookie" + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/data/OriginBoundCookieJar.kt b/android/app/src/main/java/io/github/kahme247/ompweb/data/OriginBoundCookieJar.kt new file mode 100644 index 000000000..339962c42 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/data/OriginBoundCookieJar.kt @@ -0,0 +1,156 @@ +package io.github.kahme247.ompweb.data + +import java.io.IOException +import java.security.GeneralSecurityException +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl + +/** + * A cookie jar that only ever talks to the one server the user configured. + * + * ompweb's session cookie belongs to exactly one origin (scheme, host, port). + * Every stored cookie carries that origin, so cookies of a server the user + * left are never sent to the next one — not even when both happen to share a + * host and only the port differs. Sending them anywhere else — including a + * redirect target — would hand the user's sign-in to a different server. + * + * [clear] and an origin change bump [generation]; a `Set-Cookie` that arrives + * for a request started before that point is dropped instead of resurrecting + * the old session. + */ +class OriginBoundCookieJar( + private val configuredOrigin: () -> HttpUrl?, + private val records: CookieRecords, + private val clock: () -> Long = System::currentTimeMillis, +) : CookieJar { + + private val lock = Any() + private var cache: List? = null + private var generation = 0L + + /** + * The origin and generation of the last [loadForRequest] call. `null` + * means no request has run yet (cookies saved directly are accepted); + * [CLEARED] means a `clear()` happened and only a fresh request may save. + */ + private var loadTag: String? = null + private var loadTagGeneration = -1L + + override fun loadForRequest(url: HttpUrl): List = synchronized(lock) { + val origin = configuredOrigin() ?: return emptyList() + if (!sameOrigin(origin, url)) return emptyList() + val originKey = originKey(origin) + loadTag = originKey + loadTagGeneration = generation + val now = clock() + val stored = cached() + val live = stored.filter { it.origin == originKey && it.expiresAtMillis > now } + if (live != stored) { + cache = live + persist(live) + } + live.mapNotNull { storedCookie -> + storedCookie.toCookie()?.takeIf { cookie -> cookie.matches(url) } + } + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + if (cookies.isEmpty()) return + synchronized(lock) { + val origin = configuredOrigin() ?: return + if (!sameOrigin(origin, url)) return + val originKey = originKey(origin) + val tag = loadTag + // A response to a request that started before clear() must not + // put its cookie back. + if (tag != null && (tag != originKey || loadTagGeneration != generation)) return + // Records of another origin are dropped as well: after a server + // change nothing of the old server stays behind. + val merged = cached().filter { it.origin == originKey }.toMutableList() + for (cookie in cookies) { + val stored = cookie.toStoredCookie(originKey) + merged.removeAll { it.name == stored.name && it.domain == stored.domain && it.path == stored.path } + merged.add(stored) + } + cache = merged + persist(merged) + } + } + + /** Drops every stored cookie; used by sign-out and "change server". */ + fun clear() { + synchronized(lock) { + generation++ + loadTag = CLEARED + loadTagGeneration = -1L + cache = emptyList() + persist(emptyList()) + } + } + + /** Whether the configured origin currently has a usable cookie. */ + fun hasCookies(): Boolean = synchronized(lock) { + val origin = configuredOrigin() ?: return false + val originKey = originKey(origin) + val now = clock() + cached().any { it.origin == originKey && it.expiresAtMillis > now } + } + + private fun cached(): List = cache ?: records.load().also { cache = it } + + /** + * Persisting is best effort: a full disk or an invalidated Keystore key + * must not turn a successful sign-in into a network error or a crash. + * The in-memory cache keeps working for this process. + */ + private fun persist(cookies: List) { + try { + records.save(cookies) + } catch (_: IOException) { + // Not persisted; the next successful save tries again. + } catch (_: GeneralSecurityException) { + // The Keystore key is gone; the session ends on the next launch. + } + } + + private fun StoredCookie.toCookie(): Cookie? = try { + Cookie.Builder() + .name(name) + .value(value) + .domain(domain) + .path(path) + .expiresAt(expiresAtMillis) + .apply { + if (secure) secure() + if (httpOnly) httpOnly() + } + .build() + } catch (_: IllegalArgumentException) { + null + } + + private fun Cookie.toStoredCookie(originKey: String): StoredCookie = StoredCookie( + name = name, + value = value, + expiresAtMillis = expiresAt, + domain = domain, + path = path, + secure = secure, + httpOnly = httpOnly, + origin = originKey, + ) + + private fun sameOrigin(first: HttpUrl, second: HttpUrl): Boolean = + first.scheme == second.scheme && + first.host.equals(second.host, ignoreCase = true) && + first.port == second.port + + private fun originKey(url: HttpUrl): String = + "${url.scheme}://${url.host}:${url.port}" + + private companion object { + /** Sentinel: a `clear()` happened and no request has tagged a generation since. */ + const val CLEARED = "" + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/data/SessionCacheStore.kt b/android/app/src/main/java/io/github/kahme247/ompweb/data/SessionCacheStore.kt new file mode 100644 index 000000000..848cbaebd --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/data/SessionCacheStore.kt @@ -0,0 +1,330 @@ +package io.github.kahme247.ompweb.data + +import java.io.File +import java.security.MessageDigest +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +/** The cached session list of one origin together with its validator. */ +data class CachedSessionList(val body: String, val etag: String?) + +/** + * The workspace's cache seam (`SessionsViewModel`): the last session list and + * the last project list of one origin. Bodies are stored exactly as they came + * from the server; a body that no longer parses is dropped on read. + */ +interface SessionListCache { + + fun readSessions(origin: String): CachedSessionList? + + fun writeSessions(origin: String, body: String, etag: String?) + + fun readProjects(origin: String): String? + + fun writeProjects(origin: String, body: String) +} + +/** + * The chat's cache seam (`ChatViewModel`): the whole context response of the + * last opened sessions, keyed by the view they were fetched for (`leafId` and + * the compaction setting). A cached page is only ever used for exactly that + * view — seeding a transcript from another branch would show the wrong one. + */ +interface TranscriptCache { + + fun read(origin: String, sessionId: String, leafId: String?, includePreCompaction: Boolean): String? + + fun write(origin: String, sessionId: String, leafId: String?, includePreCompaction: Boolean, body: String) +} + +/** + * Cache-first storage for the app's own startup payloads, in `filesDir` + * (never in the backup set: `allowBackup` is false and both rules files + * exclude everything). + * + * Everything is scoped to one server origin by directory, so a server switch + * can never read another installation's list or transcript, and + * [clearLocalUserData][io.github.kahme247.ompweb.AppContainer.clearLocalUserData] + * deletes the whole tree on logout, a switch and a 401. + * + * Transcripts are an LRU with three bounds: at most [maxTranscripts] entries, + * [maxTotalBytes] in total and [maxTranscriptBytes] per response (a larger + * transcript is not cached at all — it would not fit the budget and is the + * one the phone can already re-fetch by opening it). Bodies are written + * through a temp file, so a killed process never leaves a half JSON body + * behind that would paint a broken transcript. + */ +class SessionCacheStore( + private val root: File, + private val clock: () -> Long = { System.currentTimeMillis() }, + private val maxTranscripts: Int = MAX_TRANSCRIPTS, + private val maxTotalBytes: Long = MAX_TOTAL_BYTES, + private val maxTranscriptBytes: Long = MAX_TRANSCRIPT_BYTES, +) : SessionListCache, TranscriptCache { + + private val lock = Any() + + private val json = Json { + ignoreUnknownKeys = true + explicitNulls = false + } + + // --- session list / projects ------------------------------------------- + + override fun readSessions(origin: String): CachedSessionList? = synchronized(lock) { + val envelope = readEnvelope(originDir(origin), LIST_FILE, ListEnvelope.serializer()) ?: return null + if (envelope.body.isEmpty()) return null + CachedSessionList(body = envelope.body, etag = envelope.etag) + } + + override fun writeSessions(origin: String, body: String, etag: String?) = synchronized(lock) { + if (body.isEmpty()) return + writeEnvelope( + originDir(origin), + LIST_FILE, + ListEnvelope.serializer(), + ListEnvelope(etag = etag, body = body, savedAt = clock()), + ) + } + + override fun readProjects(origin: String): String? = synchronized(lock) { + readEnvelope(originDir(origin), PROJECTS_FILE, BodyEnvelope.serializer()) + ?.body + ?.takeIf { it.isNotEmpty() } + } + + override fun writeProjects(origin: String, body: String) = synchronized(lock) { + if (body.isEmpty()) return + writeEnvelope( + originDir(origin), + PROJECTS_FILE, + BodyEnvelope.serializer(), + BodyEnvelope(body = body, savedAt = clock()), + ) + } + + // --- transcripts --------------------------------------------------------- + + override fun read( + origin: String, + sessionId: String, + leafId: String?, + includePreCompaction: Boolean, + ): String? = synchronized(lock) { + val dir = originDir(origin) + val index = readIndex(dir) ?: return null + val key = keyOf(sessionId) + val entry = index.entries.firstOrNull { it.key == key } ?: return null + // Another branch or the compaction view: the cached body does not + // answer this request, so it must not seed it. + if (entry.leafId != leafId || entry.includePreCompaction != includePreCompaction) return null + val file = File(dir, bodyName(key)) + val body = runCatching { if (file.isFile) file.readText() else null }.getOrNull() + if (body.isNullOrEmpty()) { + removeEntry(dir, index, entry) + return null + } + writeIndex(dir, index.copy(entries = index.entries.map { if (it.key == key) it.copy(lastUsedAt = clock()) else it })) + body + } + + override fun write( + origin: String, + sessionId: String, + leafId: String?, + includePreCompaction: Boolean, + body: String, + ) = synchronized(lock) { + val dir = originDir(origin) + val key = keyOf(sessionId) + val bytes = body.toByteArray(Charsets.UTF_8).size.toLong() + if (body.isEmpty() || bytes > maxTranscriptBytes) { + // Not cacheable: drop a previous copy so a later open cannot show a + // transcript that is no longer the session's state. + dropTranscript(dir, key) + return + } + val file = File(dir, bodyName(key)) + if (!writeAtomically(file, body)) return + val previous = readIndex(dir) ?: IndexEnvelope() + val now = clock() + val entry = IndexEntry( + key = key, + sessionId = sessionId, + bytes = bytes, + savedAt = now, + lastUsedAt = now, + leafId = leafId, + includePreCompaction = includePreCompaction, + ) + val kept = (previous.entries.filterNot { it.key == key } + entry).let(::evict) + val keptKeys = kept.mapTo(HashSet()) { it.key } + previous.entries.filter { it.key !in keptKeys }.forEach { stale -> + runCatching { File(dir, bodyName(stale.key)).delete() } + } + writeIndex(dir, IndexEnvelope(kept)) + // Bodies the index no longer lists are unreachable: a corrupt or lost + // index would otherwise pin them in `filesDir` forever, because the + // eviction above can only see entries it still reads. + sweepOrphanBodies(dir, keptKeys) + } + + // --- lifecycle ----------------------------------------------------------- + + /** Deletes every cached payload of every origin (logout, server switch, 401). */ + fun clearAll() = synchronized(lock) { + runCatching { root.listFiles()?.forEach { it.deleteRecursively() } } + } + + /** Number of cached transcripts of [origin]; the LRU's observable size. */ + fun transcriptCount(origin: String): Int = synchronized(lock) { + readIndex(originDir(origin))?.entries?.size ?: 0 + } + + // --- internals ----------------------------------------------------------- + + /** + * Keep the [maxTranscripts] most recently used entries and stop after + * [maxTotalBytes]: the caller deletes the bodies of everything this drops. + * Ordering is by last use, newest first; ties fall back to insertion age + * and then to the key so eviction is deterministic in tests. + */ + private fun evict(entries: List): List { + val ordered = entries.sortedWith( + compareByDescending { it.lastUsedAt } + .thenByDescending { it.savedAt } + .thenBy { it.key }, + ) + val kept = ArrayList(minOf(ordered.size, maxTranscripts)) + var total = 0L + for (entry in ordered) { + if (kept.size >= maxTranscripts) break + if (total + entry.bytes > maxTotalBytes) continue + kept += entry + total += entry.bytes + } + return kept + } + + /** Removes one entry and its body (used when a body went missing). */ + private fun removeEntry(dir: File, index: IndexEnvelope, entry: IndexEntry) { + runCatching { File(dir, bodyName(entry.key)).delete() } + writeIndex(dir, index.copy(entries = index.entries.filterNot { it.key == entry.key })) + } + + private fun dropTranscript(dir: File, key: String) { + val index = readIndex(dir) ?: return + val entry = index.entries.firstOrNull { it.key == key } ?: return + removeEntry(dir, index, entry) + } + + /** + * Deletes every body file whose key is not in [keptKeys]. Only an index + * entry can serve a body, so an unlisted file is dead weight — and after a + * corrupt index it would stay there without any route out. + */ + private fun sweepOrphanBodies(dir: File, keptKeys: Set) { + val files = dir.listFiles() ?: return + for (file in files) { + val name = file.name + if (!name.startsWith(BODY_PREFIX) || !name.endsWith(BODY_SUFFIX)) continue + val key = name.substring(BODY_PREFIX.length, name.length - BODY_SUFFIX.length) + if (key !in keptKeys) runCatching { file.delete() } + } + } + + private fun originDir(origin: String): File = File(root, hash(origin)) + + private fun keyOf(sessionId: String): String = hash(sessionId) + + private fun bodyName(key: String): String = "$BODY_PREFIX$key$BODY_SUFFIX" + + private fun readEnvelope(dir: File, name: String, serializer: kotlinx.serialization.KSerializer): T? { + val file = File(dir, name) + if (!file.isFile) return null + return runCatching { json.decodeFromString(serializer, file.readText()) } + .onFailure { runCatching { file.delete() } } + .getOrNull() + } + + private fun writeEnvelope(dir: File, name: String, serializer: kotlinx.serialization.KSerializer, value: T) { + runCatching { + dir.mkdirs() + writeAtomically(File(dir, name), json.encodeToString(serializer, value)) + } + } + + private fun readIndex(dir: File): IndexEnvelope? = readEnvelope(dir, INDEX_FILE, IndexEnvelope.serializer()) + + private fun writeIndex(dir: File, index: IndexEnvelope) { + writeEnvelope(dir, INDEX_FILE, IndexEnvelope.serializer(), index) + } + + /** Writes through `.tmp` and renames, so a reader never sees half a file. */ + private fun writeAtomically(file: File, content: String): Boolean { + val dir = file.parentFile ?: return false + dir.mkdirs() + val temp = File(dir, "${file.name}.tmp") + return runCatching { + temp.writeText(content) + // `renameTo` refuses an existing target on some filesystems. + if (file.exists()) file.delete() + temp.renameTo(file) + }.getOrDefault(false) + } + + private fun hash(value: String): String { + val digest = MessageDigest.getInstance("SHA-256").digest(value.toByteArray(Charsets.UTF_8)) + val hex = StringBuilder(2 * digest.size) + for (byte in digest) hex.append(HEX[(byte.toInt() shr 4) and 0x0f]).append(HEX[byte.toInt() and 0x0f]) + return hex.substring(0, 32) + } + + @Serializable + private data class ListEnvelope( + val etag: String? = null, + val body: String = "", + val savedAt: Long = 0, + ) + + @Serializable + private data class BodyEnvelope( + val body: String = "", + val savedAt: Long = 0, + ) + + @Serializable + private data class IndexEnvelope( + val entries: List = emptyList(), + ) + + @Serializable + private data class IndexEntry( + val key: String, + val sessionId: String, + val bytes: Long = 0, + val savedAt: Long = 0, + val lastUsedAt: Long = 0, + val leafId: String? = null, + val includePreCompaction: Boolean = false, + ) + + companion object { + /** Most recently opened sessions kept for the offline first paint. */ + const val MAX_TRANSCRIPTS: Int = 20 + + /** Total budget of the transcript cache. */ + const val MAX_TOTAL_BYTES: Long = 50L * 1024 * 1024 + + /** One response larger than this is not cached at all. */ + const val MAX_TRANSCRIPT_BYTES: Long = 8L * 1024 * 1024 + + private const val LIST_FILE = "sessions.json" + private const val PROJECTS_FILE = "projects.json" + private const val INDEX_FILE = "transcripts.json" + private const val BODY_PREFIX = "t-" + private const val BODY_SUFFIX = ".json" + + private val HEX = "0123456789abcdef".toCharArray() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/data/SettingsStore.kt b/android/app/src/main/java/io/github/kahme247/ompweb/data/SettingsStore.kt new file mode 100644 index 000000000..aa787100e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/data/SettingsStore.kt @@ -0,0 +1,819 @@ +package io.github.kahme247.ompweb.data + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import io.github.kahme247.ompweb.domain.ActiveGoal +import io.github.kahme247.ompweb.domain.AndroidUpdateCache +import io.github.kahme247.ompweb.domain.DEFAULT_SOUND_ENABLED +import io.github.kahme247.ompweb.domain.DictationEnginePreference +import io.github.kahme247.ompweb.domain.FontSizePreference +import io.github.kahme247.ompweb.domain.NotifyIdentity +import io.github.kahme247.ompweb.domain.NotifyPrefs +import io.github.kahme247.ompweb.domain.SubmitDuringRunBehavior +import io.github.kahme247.ompweb.domain.ToolPreset +import io.github.kahme247.ompweb.domain.TouchTargetsPreference +import io.github.kahme247.ompweb.domain.UiScalePreference +import io.github.kahme247.ompweb.domain.UpdateKind +import io.github.kahme247.ompweb.domain.encodeActiveGoal +import io.github.kahme247.ompweb.domain.isValidNotificationId +import io.github.kahme247.ompweb.domain.parseActiveGoal +import io.github.kahme247.ompweb.domain.randomNotificationId +import io.github.kahme247.ompweb.net.ServerAddress +import io.github.kahme247.ompweb.net.ServerAddressResult +import io.github.kahme247.ompweb.ui.theme.DEFAULT_THEME_ID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.contentOrNull + +private val Context.settingsDataStore: DataStore by preferencesDataStore(name = "ompweb-settings") + +/** What the UI reads synchronously once DataStore emitted its first value. */ +data class SettingsSnapshot( + val origin: ServerAddress? = null, + val themeId: String = DEFAULT_THEME_ID, + /** Whether the server asked for a password; false for an open server. */ + val needsPassword: Boolean = true, + /** + * `null` means the user has never toggled a project: the sidebar then + * expands only the active project (web `loadExpandedProjects`). + */ + val expandedProjects: Set? = null, + val unreadSessionIds: Set = emptySet(), + /** + * Sessions the user pinned, keyed by server origin (`scheme://host:port`). + * A pin must never leak to another installation, whose ids mean nothing. + */ + val pinnedSessionsByOrigin: Map> = emptyMap(), + /** `dictation_engine` wire value (`auto`/`on_device`/`server`). */ + val dictationEngine: String = "auto", + /** `dictation_language` BCP-47 tag; null uses the device locale. */ + val dictationLanguage: String? = null, + val toolPreset: String = ToolPreset.FULL.wireName, + /** Web default: finished and running tool calls start folded. */ + val toolCallsCollapsed: Boolean = true, + /** Web default: the running turn folds its activity like a finished one. */ + val hideToolActivity: Boolean = true, + /** Web default: reasoning stays folded until the user opens it. */ + val expandThinking: Boolean = false, + /** Web default: reasoning blocks are shown (folded), not dropped. */ + val hideThinking: Boolean = false, + /** Web default: the newest reply is read aloud only on request. */ + val ttsAutoplay: Boolean = false, + /** A TextToSpeech voice name; null means the engine default. */ + val ttsVoice: String? = null, + /** The session that was open last; restored on a cold start. */ + val lastSessionId: String? = null, + /** The new-session pane's workspace when that was the last view; validated on restore. */ + val lastNewSessionCwd: String? = null, + /** First visible transcript row per session, for restoring the scroll position. */ + val chatScrollPositions: Map = emptyMap(), + /** `omp-web:submit-during-run`; queuing is the resilient default. */ + val submitDuringRun: SubmitDuringRunBehavior = SubmitDuringRunBehavior.DEFAULT, + /** `omp-web:word-completion` wire value (`auto`/`on`/`off`). */ + val wordCompletion: String = "auto", + /** `omp-sound-enabled`: play the completion tone. */ + val soundEnabled: Boolean = DEFAULT_SOUND_ENABLED, + /** Per-session `/goal` records, keyed by session id. */ + val activeGoals: Map = emptyMap(), + /** `omp-web:composer-todo-collapsed`: the attached todo panel starts collapsed. */ + val composerTodoCollapsed: Boolean = true, + /** `omp-web:composer-subagents-collapsed`: the attached roster starts collapsed. */ + val composerSubagentsCollapsed: Boolean = true, + /** `omp-web:open-url-automatically`: open the agent's links without asking. */ + val openUrlAutomatically: Boolean = false, + /** Sessions with the advisor enabled (`omp-advisor-enabled:`). */ + val advisorSessions: Set = emptySet(), + /** `omp-composer-models`: null means every model is offered. */ + val composerModels: Set? = null, + /** `omp-font-size`: chat font preference (sm/md/lg/xl). */ + val fontSize: String = "md", + /** `omp-ui-scale`: overall UI zoom (compact/standard/comfortable/large). */ + val uiScale: String = "standard", + /** `omp-touch-targets`: minimum interactive target (auto/compact/accessible). */ + val touchTargets: String = "auto", + /** `omp-web:dismissed-app-update`: the app version the banner was dismissed for. */ + val dismissedAppUpdateVersion: String? = null, + /** `omp-web:dismissed-omp-update`: the omp version the banner was dismissed for. */ + val dismissedOmpUpdateVersion: String? = null, + /** `omp-web:completed-app-update`: the version whose install finished. */ + val completedAppUpdateVersion: String? = null, + /** `ompweb:dismissed-android-update`: the Android release version the banner was dismissed for. */ + val dismissedAndroidUpdateVersion: String? = null, + /** Last successful update check, cached for six hours. */ + val androidUpdateCache: AndroidUpdateCache? = null, + /** `ompweb:android-update-feed`: custom feed URL; null means GitHub Releases. */ + val androidUpdateFeedUrl: String? = null, + /** `omp-notify-prefs`; native defaults follow PLAN §4. */ + val notifyPrefs: NotifyPrefs = NotifyPrefs.DEFAULT, + /** Per-installation identity for `PUT /api/notifications/devices` and presence. */ + val notifyDeviceId: String? = null, + val notifyClientId: String? = null, + /** The one-time onboarding prompt for the notification permission. */ + val notifyPermissionPrompted: Boolean = false, + /** False until the first DataStore emission arrived; the app waits for it. */ + val loaded: Boolean = false, +) + +/** + * The client-side preferences the settings screens read and write. Split off + * [SettingsStore] the same way [WorkspacePreferences] and [ChatSettings] are, + * so `SettingsViewModel` runs in JVM tests with a fake instead of DataStore. + */ +interface SettingsViewPreferences { + val snapshot: StateFlow + + /** `omp-web:dismissed-app-update` / `omp-web:dismissed-omp-update`; null clears. */ + suspend fun setDismissedUpdate(kind: UpdateKind, version: String?) + + /** `omp-web:completed-app-update`: suppresses the banner for that version. */ + suspend fun setCompletedAppUpdate(version: String?) + + /** Dictation engine choice (`auto`/`on_device`/`server`). */ + suspend fun setDictationEngine(wire: String) + + /** Recognition language tag; null follows the device locale. */ + suspend fun setDictationLanguage(tag: String?) +} + +/** The Android self-update banner's persistable facts. */ +data class AndroidUpdatePreferencesSnapshot( + /** The release version the user dismissed; a newer one shows again. */ + val dismissedVersion: String? = null, + /** The last successful check; null until one succeeded. */ + val cache: AndroidUpdateCache? = null, + /** A custom feed URL; null means GitHub Releases. */ + val feedUrl: String? = null, +) + +/** + * Persistence seam of the app-update check. Kept apart from + * [SettingsViewPreferences] because the app update belongs to the installed + * APK, not to the server installation, and because its cache is rewritten on + * every check. + */ +interface AndroidUpdatePreferences { + val androidUpdatePreferences: StateFlow + + /** Persists the dismissed release version; null clears it. */ + suspend fun setDismissedAndroidUpdate(version: String?) + + suspend fun setAndroidUpdateCache(cache: AndroidUpdateCache) + + /** Persists the selected update source; null selects GitHub Releases. */ + suspend fun setAndroidUpdateFeedUrl(url: String?) +} + +/** + * Client-side settings: the server origin, the theme preference, and the + * sidebar's persisted workspace state (expanded projects, unread sessions, + * tool preset — the web's `omp-web:*` localStorage keys). + * + * The session cookie is deliberately not stored here — it lives encrypted on + * its own (see `EncryptedCookieRecords`). The password is never stored at all. + * [originNow] is a plain in-memory read so the HTTP cookie jar (called on + * OkHttp threads, with no coroutine context) can bind cookies to the origin. + * Origin and password requirement are written through to the snapshot before + * the DataStore write finishes, so a request started right after "Connect" + * cannot see the previous origin. + */ +class SettingsStore(context: Context, private val scope: CoroutineScope) : + WorkspacePreferences, ChatSettings, SettingsViewPreferences, AndroidUpdatePreferences { + + private val dataStore = context.applicationContext.settingsDataStore + private val _snapshot = MutableStateFlow(SettingsSnapshot()) + override val snapshot: StateFlow = _snapshot.asStateFlow() + + /** A derived view so the update checker does not depend on the whole settings surface. */ + override val androidUpdatePreferences: StateFlow = + snapshot.map { current -> + AndroidUpdatePreferencesSnapshot( + dismissedVersion = current.dismissedAndroidUpdateVersion, + cache = current.androidUpdateCache, + feedUrl = current.androidUpdateFeedUrl, + ) + }.stateIn(scope, SharingStarted.Eagerly, AndroidUpdatePreferencesSnapshot()) + private val notifyIdentityMutex = Mutex() + + init { + scope.launch { + dataStore.data.collect { preferences -> + val origin = parseOrigin(preferences[ORIGIN]) + _snapshot.value = SettingsSnapshot( + origin = origin, + themeId = preferences[THEME]?.takeIf { it.isNotBlank() } ?: DEFAULT_THEME_ID, + needsPassword = preferences[NEEDS_PASSWORD] ?: true, + expandedProjects = parseStringSet(preferences[EXPANDED_PROJECTS]), + unreadSessionIds = parseStringSet(preferences[UNREAD_SESSIONS]) ?: emptySet(), + pinnedSessionsByOrigin = parsePinnedSessions(preferences[PINNED_SESSIONS]), + dictationEngine = preferences[DICTATION_ENGINE]?.takeIf { it.isNotBlank() } ?: "auto", + dictationLanguage = preferences[DICTATION_LANGUAGE]?.takeIf { it.isNotBlank() }, + toolPreset = preferences[TOOL_PRESET]?.takeIf { it.isNotBlank() } ?: ToolPreset.FULL.wireName, + toolCallsCollapsed = preferences[TOOL_CALLS_COLLAPSED] ?: true, + hideToolActivity = preferences[HIDE_TOOL_ACTIVITY] ?: true, + expandThinking = preferences[EXPAND_THINKING] ?: false, + hideThinking = preferences[HIDE_THINKING] ?: false, + ttsAutoplay = preferences[TTS_AUTOPLAY] ?: false, + ttsVoice = preferences[TTS_VOICE]?.takeIf { it.isNotBlank() }, + lastSessionId = preferences[LAST_SESSION_ID]?.takeIf { it.isNotBlank() }, + lastNewSessionCwd = preferences[LAST_NEW_SESSION_CWD]?.takeIf { it.isNotBlank() }, + chatScrollPositions = parseScrollPositions(preferences[CHAT_SCROLL_POSITIONS]), + submitDuringRun = SubmitDuringRunBehavior.fromWire(preferences[SUBMIT_DURING_RUN]), + wordCompletion = preferences[WORD_COMPLETION]?.takeIf { it.isNotBlank() } ?: "auto", + soundEnabled = preferences[SOUND_ENABLED] ?: DEFAULT_SOUND_ENABLED, + activeGoals = parseGoals(preferences[ACTIVE_GOALS]), + composerTodoCollapsed = preferences[COMPOSER_TODO_COLLAPSED] ?: true, + composerSubagentsCollapsed = preferences[COMPOSER_SUBAGENTS_COLLAPSED] ?: true, + openUrlAutomatically = preferences[OPEN_URL_AUTOMATICALLY] ?: false, + advisorSessions = parseStringSet(preferences[ADVISOR_SESSIONS]) ?: emptySet(), + composerModels = parseStringSet(preferences[COMPOSER_MODELS]), + fontSize = preferences[FONT_SIZE]?.takeIf { it.isNotBlank() } ?: "md", + uiScale = preferences[UI_SCALE]?.takeIf { it.isNotBlank() } ?: "standard", + touchTargets = preferences[TOUCH_TARGETS]?.takeIf { it.isNotBlank() } ?: "auto", + dismissedAppUpdateVersion = preferences[DISMISSED_APP_UPDATE]?.takeIf { it.isNotBlank() }, + dismissedOmpUpdateVersion = preferences[DISMISSED_OMP_UPDATE]?.takeIf { it.isNotBlank() }, + completedAppUpdateVersion = preferences[COMPLETED_APP_UPDATE]?.takeIf { it.isNotBlank() }, + dismissedAndroidUpdateVersion = preferences[DISMISSED_ANDROID_UPDATE]?.takeIf { it.isNotBlank() }, + androidUpdateCache = parseAndroidUpdateCache(preferences[ANDROID_UPDATE_CACHE]), + androidUpdateFeedUrl = preferences[ANDROID_UPDATE_FEED]?.takeIf { it.isNotBlank() }, + notifyPrefs = parseNotifyPrefs(preferences[NOTIFY_PREFS]), + notifyDeviceId = preferences[NOTIFY_DEVICE_ID]?.takeIf(::isValidNotificationId), + notifyClientId = preferences[NOTIFY_CLIENT_ID]?.takeIf(::isValidNotificationId), + notifyPermissionPrompted = preferences[NOTIFY_PERMISSION_PROMPTED] ?: false, + loaded = true, + ) + } + } + } + + fun originNow(): ServerAddress? = _snapshot.value.origin + + /** + * Suspends until the first DataStore emission filled the snapshot. + * + * A broadcaster started from a notification action and the notification + * service's own start run before that emission in a cold process; reading + * the snapshot there would see "no origin configured" and misjudge both + * the origin and the enabled flag. + */ + suspend fun awaitLoaded(): SettingsSnapshot = awaitLoadedSnapshot(snapshot) + + suspend fun setOrigin(origin: String?) { + val parsed = parseOrigin(origin) + synchronized(this) { _snapshot.value = _snapshot.value.copy(origin = parsed) } + dataStore.edit { preferences -> + if (origin == null) { + preferences.remove(ORIGIN) + } else { + preferences[ORIGIN] = origin + } + } + } + + suspend fun setNeedsPassword(needsPassword: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(needsPassword = needsPassword) } + dataStore.edit { preferences -> preferences[NEEDS_PASSWORD] = needsPassword } + } + + /** + * The stable per-installation notification identity. Generated once and + * kept across restarts; `clearNotifyIdentity()` drops it with the rest of + * the signed-in user's local data. + */ + suspend fun ensureNotifyIdentity(): NotifyIdentity = notifyIdentityMutex.withLock { + val current = _snapshot.value + if (isValidNotificationId(current.notifyDeviceId) && isValidNotificationId(current.notifyClientId)) { + return@withLock NotifyIdentity(current.notifyDeviceId!!, current.notifyClientId!!) + } + val created = NotifyIdentity( + deviceId = current.notifyDeviceId?.takeIf(::isValidNotificationId) ?: randomNotificationId(), + clientId = current.notifyClientId?.takeIf(::isValidNotificationId) ?: randomNotificationId(), + ) + synchronized(this) { + _snapshot.value = _snapshot.value.copy( + notifyDeviceId = created.deviceId, + notifyClientId = created.clientId, + ) + } + dataStore.edit { preferences -> + if (!isValidNotificationId(preferences[NOTIFY_DEVICE_ID])) preferences[NOTIFY_DEVICE_ID] = created.deviceId + if (!isValidNotificationId(preferences[NOTIFY_CLIENT_ID])) preferences[NOTIFY_CLIENT_ID] = created.clientId + } + val stored = _snapshot.value + NotifyIdentity( + deviceId = stored.notifyDeviceId ?: created.deviceId, + clientId = stored.notifyClientId ?: created.clientId, + ) + } + + suspend fun clearNotifyIdentity() { + synchronized(this) { + _snapshot.value = _snapshot.value.copy(notifyDeviceId = null, notifyClientId = null) + } + dataStore.edit { preferences -> + preferences.remove(NOTIFY_DEVICE_ID) + preferences.remove(NOTIFY_CLIENT_ID) + } + } + + suspend fun setNotifyPrefs(prefs: NotifyPrefs) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(notifyPrefs = prefs) } + dataStore.edit { preferences -> preferences[NOTIFY_PREFS] = prefs.toJson().toString() } + } + + suspend fun setNotifyPermissionPrompted(prompted: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(notifyPermissionPrompted = prompted) } + dataStore.edit { preferences -> preferences[NOTIFY_PERMISSION_PROMPTED] = prompted } + } + + suspend fun setTheme(themeId: String) { + dataStore.edit { preferences -> preferences[THEME] = themeId } + } + + /** Persisted as one JSON array; an empty set is still a stored choice. */ + override suspend fun setExpandedProjects(projects: Set) { + val encoded = encodeStringSet(projects) + synchronized(this) { _snapshot.value = _snapshot.value.copy(expandedProjects = projects.toSet()) } + dataStore.edit { preferences -> preferences[EXPANDED_PROJECTS] = encoded } + } + + /** Removed entirely when empty, like `saveUnreadSessionIds` on the web. */ + override suspend fun setUnreadSessionIds(ids: Set) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(unreadSessionIds = ids.toSet()) } + dataStore.edit { preferences -> + if (ids.isEmpty()) { + preferences.remove(UNREAD_SESSIONS) + } else { + preferences[UNREAD_SESSIONS] = encodeStringSet(ids) + } + } + } + + /** + * Pins are scoped to one server origin: the same id on another + * installation is a different session. The whole map is rewritten in one + * edit so an origin's list can never be half-updated. + */ + override suspend fun setPinnedSessions(origin: String, ids: Set) { + val key = origin.trim() + if (key.isEmpty()) return + val next = _snapshot.value.pinnedSessionsByOrigin.toMutableMap().apply { + if (ids.isEmpty()) remove(key) else put(key, ids.toSet()) + } + synchronized(this) { _snapshot.value = _snapshot.value.copy(pinnedSessionsByOrigin = next) } + dataStore.edit { preferences -> + if (next.isEmpty()) { + preferences.remove(PINNED_SESSIONS) + } else { + preferences[PINNED_SESSIONS] = encodePinnedSessions(next) + } + } + } + + override suspend fun setToolPreset(preset: ToolPreset) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(toolPreset = preset.wireName) } + dataStore.edit { preferences -> preferences[TOOL_PRESET] = preset.wireName } + } + + /** + * One `edit` for both markers: the pane's workspace is stored and the + * session marker removed together (a blank cwd clears the pane marker). + */ + override suspend fun setLastNewSessionView(cwd: String) { + val normalized = cwd.takeIf { it.isNotBlank() } + synchronized(this) { + _snapshot.value = _snapshot.value.copy(lastNewSessionCwd = normalized, lastSessionId = null) + } + dataStore.edit { preferences -> + if (normalized == null) { + preferences.remove(LAST_NEW_SESSION_CWD) + } else { + preferences[LAST_NEW_SESSION_CWD] = normalized + } + preferences.remove(LAST_SESSION_ID) + } + } + + override suspend fun clearLastNewSessionView() { + synchronized(this) { _snapshot.value = _snapshot.value.copy(lastNewSessionCwd = null) } + dataStore.edit { preferences -> preferences.remove(LAST_NEW_SESSION_CWD) } + } + + /** + * Mirror of [setLastNewSessionView] for the session side: one edit stores + * the session marker and removes the pane marker together. + */ + override suspend fun setLastSessionView(sessionId: String) { + synchronized(this) { + _snapshot.value = _snapshot.value.copy(lastSessionId = sessionId, lastNewSessionCwd = null) + } + dataStore.edit { preferences -> + preferences[LAST_SESSION_ID] = sessionId + preferences.remove(LAST_NEW_SESSION_CWD) + } + } + + override suspend fun clearLastSession() { + synchronized(this) { _snapshot.value = _snapshot.value.copy(lastSessionId = null) } + dataStore.edit { preferences -> preferences.remove(LAST_SESSION_ID) } + } + + /** Remember the first visible transcript row of [sessionId] (top row = 0). */ + override suspend fun setChatScrollPosition(sessionId: String, rowIndex: Int) { + val next = scrollPositionsWith(sessionId, rowIndex) + synchronized(this) { _snapshot.value = _snapshot.value.copy(chatScrollPositions = next) } + dataStore.edit { preferences -> writeScrollPositions(preferences, next) } + } + + /** Same as [setChatScrollPosition], usable from `onCleared` (no caller scope). */ + override fun persistChatScrollPosition(sessionId: String, rowIndex: Int) { + val next = scrollPositionsWith(sessionId, rowIndex) + synchronized(this) { _snapshot.value = _snapshot.value.copy(chatScrollPositions = next) } + scope.launch { dataStore.edit { preferences -> writeScrollPositions(preferences, next) } } + } + + override suspend fun setSubmitDuringRun(behavior: SubmitDuringRunBehavior) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(submitDuringRun = behavior) } + dataStore.edit { preferences -> preferences[SUBMIT_DURING_RUN] = behavior.wireName } + } + + override suspend fun setWordCompletion(mode: String) { + val normalized = when (mode) { + "on", "off" -> mode + else -> "auto" + } + synchronized(this) { _snapshot.value = _snapshot.value.copy(wordCompletion = normalized) } + dataStore.edit { preferences -> preferences[WORD_COMPLETION] = normalized } + } + + override suspend fun setDictationEngine(wire: String) { + val normalized = DictationEnginePreference.fromWire(wire).wire + synchronized(this) { _snapshot.value = _snapshot.value.copy(dictationEngine = normalized) } + dataStore.edit { preferences -> preferences[DICTATION_ENGINE] = normalized } + } + + override suspend fun setDictationLanguage(tag: String?) { + val normalized = tag?.trim()?.takeIf { it.isNotEmpty() } + synchronized(this) { _snapshot.value = _snapshot.value.copy(dictationLanguage = normalized) } + dataStore.edit { preferences -> + if (normalized == null) preferences.remove(DICTATION_LANGUAGE) + else preferences[DICTATION_LANGUAGE] = normalized + } + } + + override suspend fun setSoundEnabled(enabled: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(soundEnabled = enabled) } + dataStore.edit { preferences -> preferences[SOUND_ENABLED] = enabled } + } + + override suspend fun setActiveGoal(sessionId: String, goal: ActiveGoal?) { + val next = _snapshot.value.activeGoals.toMutableMap().apply { + if (goal == null) remove(sessionId) else put(sessionId, goal) + } + synchronized(this) { _snapshot.value = _snapshot.value.copy(activeGoals = next) } + dataStore.edit { preferences -> + if (next.isEmpty()) { + preferences.remove(ACTIVE_GOALS) + } else { + preferences[ACTIVE_GOALS] = encodeGoals(next) + } + } + } + + override suspend fun setComposerTodoCollapsed(collapsed: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(composerTodoCollapsed = collapsed) } + dataStore.edit { preferences -> preferences[COMPOSER_TODO_COLLAPSED] = collapsed } + } + + override suspend fun setComposerSubagentsCollapsed(collapsed: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(composerSubagentsCollapsed = collapsed) } + dataStore.edit { preferences -> preferences[COMPOSER_SUBAGENTS_COLLAPSED] = collapsed } + } + + override suspend fun setOpenUrlAutomatically(enabled: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(openUrlAutomatically = enabled) } + dataStore.edit { preferences -> preferences[OPEN_URL_AUTOMATICALLY] = enabled } + } + + suspend fun setToolCallsCollapsed(collapsed: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(toolCallsCollapsed = collapsed) } + dataStore.edit { preferences -> preferences[TOOL_CALLS_COLLAPSED] = collapsed } + } + + suspend fun setHideToolActivity(hidden: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(hideToolActivity = hidden) } + dataStore.edit { preferences -> preferences[HIDE_TOOL_ACTIVITY] = hidden } + } + + suspend fun setExpandThinking(expand: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(expandThinking = expand) } + dataStore.edit { preferences -> preferences[EXPAND_THINKING] = expand } + } + + suspend fun setTtsAutoplay(enabled: Boolean) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(ttsAutoplay = enabled) } + dataStore.edit { preferences -> preferences[TTS_AUTOPLAY] = enabled } + } + + suspend fun setTtsVoice(name: String?) { + val voice = name?.takeIf { it.isNotBlank() } + synchronized(this) { _snapshot.value = _snapshot.value.copy(ttsVoice = voice) } + dataStore.edit { preferences -> + if (voice == null) preferences.remove(TTS_VOICE) else preferences[TTS_VOICE] = voice + } + } + + suspend fun setFontSize(preference: FontSizePreference) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(fontSize = preference.wire) } + dataStore.edit { preferences -> preferences[FONT_SIZE] = preference.wire } + } + + suspend fun setUiScale(preference: UiScalePreference) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(uiScale = preference.wire) } + dataStore.edit { preferences -> preferences[UI_SCALE] = preference.wire } + } + + suspend fun setTouchTargets(preference: TouchTargetsPreference) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(touchTargets = preference.wire) } + dataStore.edit { preferences -> preferences[TOUCH_TARGETS] = preference.wire } + } + + override suspend fun setDismissedUpdate(kind: UpdateKind, version: String?) { + val normalized = version?.takeIf { it.isNotBlank() } + synchronized(this) { + _snapshot.value = when (kind) { + UpdateKind.APP -> _snapshot.value.copy(dismissedAppUpdateVersion = normalized) + UpdateKind.OMP -> _snapshot.value.copy(dismissedOmpUpdateVersion = normalized) + } + } + dataStore.edit { preferences -> + val key = when (kind) { + UpdateKind.APP -> DISMISSED_APP_UPDATE + UpdateKind.OMP -> DISMISSED_OMP_UPDATE + } + if (normalized == null) preferences.remove(key) else preferences[key] = normalized + } + } + + override suspend fun setCompletedAppUpdate(version: String?) { + val normalized = version?.takeIf { it.isNotBlank() } + synchronized(this) { _snapshot.value = _snapshot.value.copy(completedAppUpdateVersion = normalized) } + dataStore.edit { preferences -> + if (normalized == null) { + preferences.remove(COMPLETED_APP_UPDATE) + } else { + preferences[COMPLETED_APP_UPDATE] = normalized + } + } + } + + override suspend fun setDismissedAndroidUpdate(version: String?) { + val normalized = version?.takeIf { it.isNotBlank() } + synchronized(this) { _snapshot.value = _snapshot.value.copy(dismissedAndroidUpdateVersion = normalized) } + dataStore.edit { preferences -> + if (normalized == null) { + preferences.remove(DISMISSED_ANDROID_UPDATE) + } else { + preferences[DISMISSED_ANDROID_UPDATE] = normalized + } + } + } + + override suspend fun setAndroidUpdateCache(cache: AndroidUpdateCache) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(androidUpdateCache = cache) } + dataStore.edit { preferences -> preferences[ANDROID_UPDATE_CACHE] = json.encodeToString(cache) } + } + + override suspend fun setAndroidUpdateFeedUrl(url: String?) { + val normalized = url?.trim()?.takeIf { it.isNotEmpty() } + synchronized(this) { _snapshot.value = _snapshot.value.copy(androidUpdateFeedUrl = normalized) } + dataStore.edit { preferences -> + if (normalized == null) { + preferences.remove(ANDROID_UPDATE_FEED) + } else { + preferences[ANDROID_UPDATE_FEED] = normalized + } + } + } + + override suspend fun setComposerModels(models: Set?) { + synchronized(this) { _snapshot.value = _snapshot.value.copy(composerModels = models) } + dataStore.edit { preferences -> + if (models == null) { + preferences.remove(COMPOSER_MODELS) + } else { + preferences[COMPOSER_MODELS] = encodeStringSet(models) + } + } + } + + override suspend fun setAdvisorEnabled(sessionId: String, enabled: Boolean) { + val next = _snapshot.value.advisorSessions.toMutableSet().apply { + if (enabled) add(sessionId) else remove(sessionId) + } + synchronized(this) { _snapshot.value = _snapshot.value.copy(advisorSessions = next) } + dataStore.edit { preferences -> + if (next.isEmpty()) { + preferences.remove(ADVISOR_SESSIONS) + } else { + preferences[ADVISOR_SESSIONS] = encodeStringSet(next) + } + } + } + + override fun markSessionCreated(sessionId: String, advisor: Boolean) { + val next = _snapshot.value.advisorSessions.toMutableSet().apply { + if (advisor) add(sessionId) else remove(sessionId) + } + synchronized(this) { _snapshot.value = _snapshot.value.copy(advisorSessions = next) } + scope.launch { setAdvisorEnabled(sessionId, advisor) } + } + + private fun scrollPositionsWith(sessionId: String, rowIndex: Int): Map = + _snapshot.value.chatScrollPositions.toMutableMap().apply { + if (rowIndex <= 0) remove(sessionId) else put(sessionId, rowIndex) + } + + private fun writeScrollPositions(preferences: MutablePreferences, positions: Map) { + if (positions.isEmpty()) { + preferences.remove(CHAT_SCROLL_POSITIONS) + } else { + preferences[CHAT_SCROLL_POSITIONS] = encodeScrollPositions(positions) + } + } + + private companion object { + val ORIGIN = stringPreferencesKey("server_origin") + val THEME = stringPreferencesKey("theme") + val NEEDS_PASSWORD = booleanPreferencesKey("needs_password") + val EXPANDED_PROJECTS = stringPreferencesKey("expanded_projects") + val UNREAD_SESSIONS = stringPreferencesKey("unread_sessions") + val PINNED_SESSIONS = stringPreferencesKey("pinned_sessions_by_origin") + val DICTATION_ENGINE = stringPreferencesKey("dictation_engine") + val DICTATION_LANGUAGE = stringPreferencesKey("dictation_language") + val TOOL_PRESET = stringPreferencesKey("tool_preset") + val TOOL_CALLS_COLLAPSED = booleanPreferencesKey("tool_calls_collapsed") + val HIDE_TOOL_ACTIVITY = booleanPreferencesKey("hide_tool_activity") + val EXPAND_THINKING = booleanPreferencesKey("expand_thinking") + val HIDE_THINKING = booleanPreferencesKey("hide_thinking") + val TTS_AUTOPLAY = booleanPreferencesKey("tts_autoplay") + val TTS_VOICE = stringPreferencesKey("tts_voice") + val LAST_SESSION_ID = stringPreferencesKey("last_session_id") + val LAST_NEW_SESSION_CWD = stringPreferencesKey("last_new_session_cwd") + val CHAT_SCROLL_POSITIONS = stringPreferencesKey("chat_scroll_positions") + val SUBMIT_DURING_RUN = stringPreferencesKey("submit_during_run") + val WORD_COMPLETION = stringPreferencesKey("word_completion") + val SOUND_ENABLED = booleanPreferencesKey("sound_enabled") + val ACTIVE_GOALS = stringPreferencesKey("active_goals") + val COMPOSER_TODO_COLLAPSED = booleanPreferencesKey("composer_todo_collapsed") + val COMPOSER_SUBAGENTS_COLLAPSED = booleanPreferencesKey("composer_subagents_collapsed") + val OPEN_URL_AUTOMATICALLY = booleanPreferencesKey("open_url_automatically") + val ADVISOR_SESSIONS = stringPreferencesKey("advisor_sessions") + val COMPOSER_MODELS = stringPreferencesKey("composer_models") + val FONT_SIZE = stringPreferencesKey("omp-font-size") + val UI_SCALE = stringPreferencesKey("omp-ui-scale") + val TOUCH_TARGETS = stringPreferencesKey("omp-touch-targets") + val DISMISSED_APP_UPDATE = stringPreferencesKey("omp-web:dismissed-app-update") + val DISMISSED_OMP_UPDATE = stringPreferencesKey("omp-web:dismissed-omp-update") + val COMPLETED_APP_UPDATE = stringPreferencesKey("omp-web:completed-app-update") + val DISMISSED_ANDROID_UPDATE = stringPreferencesKey("ompweb:dismissed-android-update") + val ANDROID_UPDATE_CACHE = stringPreferencesKey("ompweb:android-update-cache") + val ANDROID_UPDATE_FEED = stringPreferencesKey("ompweb:android-update-feed") + val NOTIFY_PREFS = stringPreferencesKey("notify_prefs") + val NOTIFY_DEVICE_ID = stringPreferencesKey("notify_device_id") + val NOTIFY_CLIENT_ID = stringPreferencesKey("notify_client_id") + val NOTIFY_PERMISSION_PROMPTED = booleanPreferencesKey("notify_permission_prompted") + + val json = Json { ignoreUnknownKeys = true } + + fun parseAndroidUpdateCache(raw: String?): AndroidUpdateCache? { + if (raw.isNullOrBlank()) return null + return try { + json.decodeFromString(raw) + } catch (_: Exception) { + // A truncated or old cache is simply a cache miss. + null + } + } + + fun parseNotifyPrefs(raw: String?): NotifyPrefs { + if (raw.isNullOrBlank()) return NotifyPrefs.DEFAULT + return try { + NotifyPrefs.parse(json.parseToJsonElement(raw)) + } catch (_: Exception) { + NotifyPrefs.DEFAULT + } + } + + fun parseScrollPositions(raw: String?): Map { + if (raw.isNullOrBlank()) return emptyMap() + return try { + val root = json.parseToJsonElement(raw) as? kotlinx.serialization.json.JsonObject ?: return emptyMap() + root.mapNotNull { (key, value) -> + val index = (value as? JsonPrimitive)?.contentOrNull?.toIntOrNull() ?: return@mapNotNull null + if (index <= 0) null else key to index + }.toMap() + } catch (_: Exception) { + emptyMap() + } + } + + fun encodeScrollPositions(values: Map): String = + kotlinx.serialization.json.JsonObject( + values.mapValues { JsonPrimitive(it.value) }, + ).toString() + + fun parseGoals(raw: String?): Map { + if (raw.isNullOrBlank()) return emptyMap() + return try { + val root = json.parseToJsonElement(raw) as? kotlinx.serialization.json.JsonObject + ?: return emptyMap() + root.mapNotNull { (sessionId, value) -> + val goal = parseActiveGoal((value as? JsonPrimitive)?.contentOrNull) ?: return@mapNotNull null + sessionId to goal + }.toMap() + } catch (_: Exception) { + emptyMap() + } + } + + fun encodeGoals(goals: Map): String = + kotlinx.serialization.json.JsonObject( + goals.mapValues { (_, goal) -> JsonPrimitive(encodeActiveGoal(goal)) }, + ).toString() + + fun parseOrigin(raw: String?): ServerAddress? { + if (raw.isNullOrBlank()) return null + return (ServerAddress.parse(raw) as? ServerAddressResult.Valid)?.address + } + + fun parseStringSet(raw: String?): Set? { + if (raw == null) return null + return try { + (json.parseToJsonElement(raw) as? JsonArray) + ?.mapNotNull { (it as? JsonPrimitive)?.contentOrNull } + ?.toCollection(linkedSetOf()) + ?: emptySet() + } catch (_: Exception) { + emptySet() + } + } + + fun encodeStringSet(values: Set): String = + buildJsonArray { values.sorted().forEach { add(JsonPrimitive(it)) } }.toString() + + /** `{origin: [sessionId, …]}`; malformed entries are dropped. */ + fun parsePinnedSessions(raw: String?): Map> { + if (raw.isNullOrBlank()) return emptyMap() + return try { + val root = json.parseToJsonElement(raw) as? JsonObject ?: return emptyMap() + root.mapNotNull { (origin, value) -> + if (origin.isBlank()) return@mapNotNull null + val ids = (value as? JsonArray) + ?.mapNotNull { (it as? JsonPrimitive)?.contentOrNull } + ?.filter { it.isNotBlank() } + ?.toSet() + ?: return@mapNotNull null + if (ids.isEmpty()) null else origin to ids + }.toMap() + } catch (_: Exception) { + emptyMap() + } + } + + fun encodePinnedSessions(values: Map>): String = + JsonObject( + values.toSortedMap().mapValues { (_, ids) -> + buildJsonArray { ids.sorted().forEach { add(JsonPrimitive(it)) } } + }, + ).toString() + } +} + +/** + * Suspends until [snapshot] carries its first DataStore emission. Kept apart + * from [SettingsStore] so the wait itself is testable: a broadcaster or a + * restarted service that decides from the empty default would judge the origin + * and the enabled flag wrongly. + */ +suspend fun awaitLoadedSnapshot(snapshot: StateFlow): SettingsSnapshot = + snapshot.first { it.loaded } diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/data/WorkspacePreferences.kt b/android/app/src/main/java/io/github/kahme247/ompweb/data/WorkspacePreferences.kt new file mode 100644 index 000000000..eb8a7020b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/data/WorkspacePreferences.kt @@ -0,0 +1,46 @@ +package io.github.kahme247.ompweb.data + +import io.github.kahme247.ompweb.domain.ToolPreset +import kotlinx.coroutines.flow.StateFlow + +/** + * The preferences the workspace screens need, separated from the Android + * DataStore implementation so view models stay JVM-testable. + */ +interface WorkspacePreferences { + val snapshot: StateFlow + + suspend fun setExpandedProjects(projects: Set) + + suspend fun setUnreadSessionIds(ids: Set) + + /** Pins for one server origin; an empty set removes that origin's entry. */ + suspend fun setPinnedSessions(origin: String, ids: Set) + + suspend fun setToolPreset(preset: ToolPreset) + + /** + * The new-session pane is the last view: its workspace is stored and the + * session marker is cleared in one atomic DataStore edit, so a process + * death can never leave both markers behind. + */ + suspend fun setLastNewSessionView(cwd: String) + + /** + * A session is the last view: the session marker is stored and the + * new-session marker is cleared in one atomic DataStore edit. The shell + * writes this when it *selects* the session, before the detail request + * runs, so a dead link can always clear its own marker afterwards. + */ + suspend fun setLastSessionView(sessionId: String) + + /** Drops the new-session marker only; the session marker is untouched. */ + suspend fun clearLastNewSessionView() + + /** + * Drops the session marker only. The caller checks that it still names the + * dead session: a link for one session must never forget the view the user + * actually left. + */ + suspend fun clearLastSession() +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/AgentEnv.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/AgentEnv.kt new file mode 100644 index 000000000..90d46938e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/AgentEnv.kt @@ -0,0 +1,118 @@ +package io.github.kahme247.ompweb.domain + +/** + * Policy and text format for the environment variables handed to the `omp` + * child process — the port of `lib/omp/agent-env-policy.ts`, so the Settings + * editor rejects exactly what the server would reject instead of letting an + * invalid line travel to `PUT /api/web-settings` first. + * + * Users only add variables, never rewrite the plumbing omp-web depends on: + * process plumbing (`PATH`, `NODE_OPTIONS`, …), omp's own state roots + * (`HOME`, `PI_CODING_AGENT_DIR`, …) and the keys the server strips from every + * child spawn (`PORT`, `NODE_ENV`, `NEXT_*`) are denied. Matching is + * case-insensitive because Windows environment names are. + */ +data class AgentEnvParseResult( + val values: Map, + /** One English problem per offending line, prefixed with the line number. */ + val errors: List, +) { + val valid: Boolean get() = errors.isEmpty() +} + +object AgentEnv { + + /** Shell-style variable name: no dots, dashes or leading digits. */ + val NAME_PATTERN: Regex = Regex("^[A-Za-z_][A-Za-z0-9_]*$") + + /** + * Exact names that may never be overridden, compared upper-cased. Grouped + * by the failure they prevent (see the file comment). + */ + val DENIED_NAMES: Set = setOf( + // Process plumbing: a rewritten PATH breaks omp's binary/tool lookup, + // NODE_OPTIONS injects code into every child node process. + "PATH", + "PATHEXT", + "COMSPEC", + "SYSTEMROOT", + "WINDIR", + "NODE_OPTIONS", + // Where omp keeps its state; redirecting these splits the child from + // the ~/.omp omp-web enumerates. + "HOME", + "USERPROFILE", + "HOMEDRIVE", + "HOMEPATH", + "XDG_DATA_HOME", + "XDG_CONFIG_HOME", + "PI_CODING_AGENT_DIR", + "PI_CONFIG_DIR", + "OMP_PROFILE", + "PI_PROFILE", + // Stripped from every spawn by lib/project-command-env.ts, so a user + // override would be discarded without a word. + "PORT", + "NODE_ENV", + ) + + /** Prefixes that may never be overridden (compared upper-cased). */ + val DENIED_PREFIXES: List = listOf( + "OMP_WEB_", // omp-web's own plumbing (auth, ports, binary overrides) + "NEXT_", // stripped by sanitizeProjectCommandEnvironment + "_", // shell-internal namespace + ) + + /** True when [name] is reserved by omp-web and may not be overridden. */ + fun isDeniedName(name: String): Boolean { + val comparable = name.uppercase() + return comparable in DENIED_NAMES || DENIED_PREFIXES.any { comparable.startsWith(it) } + } + + /** + * Parses `KEY=VALUE` text. Blank lines and `#` comments are ignored, an + * `export ` prefix is tolerated and values are literal after the first + * `=` (trimmed, no quote/escape processing). A later assignment wins. + * Rejected lines are reported and left out of [AgentEnvParseResult.values]. + */ + fun parse(raw: String?): AgentEnvParseResult { + val values = LinkedHashMap() + val errors = mutableListOf() + val lines = (raw ?: "").split(Regex("\r?\n")) + lines.forEachIndexed { index, rawLine -> + val line = index + 1 + val trimmed = rawLine.trim() + if (trimmed.isEmpty() || trimmed.startsWith("#")) return@forEachIndexed + // `export KEY=VALUE` is what a NixOS-style generated .env looks like. + val body = trimmed.replace(Regex("^export\\s+"), "") + val separator = body.indexOf('=') + if (separator == -1) { + errors += "Line $line: expected KEY=VALUE" + return@forEachIndexed + } + val name = body.substring(0, separator).trim() + if (name.isEmpty()) { + errors += "Line $line: missing variable name" + return@forEachIndexed + } + if (!NAME_PATTERN.matches(name)) { + errors += "Line $line: \"$name\" is not a valid variable name" + return@forEachIndexed + } + if (isDeniedName(name)) { + errors += "Line $line: \"$name\" is reserved by omp-web and cannot be set" + return@forEachIndexed + } + values[name] = body.substring(separator + 1).trim() + } + return AgentEnvParseResult(values, errors) + } + + /** Validates an already-structured record with the same rules (server payloads). */ + fun sanitize(values: Map): AgentEnvParseResult = + parse(values.entries.joinToString("\n") { (name, value) -> "$name=$value" }) + + /** Canonical `KEY=VALUE` text, sorted by name so saves stay stable and diffable. */ + fun format(values: Map): String = + values.entries.sortedBy { it.key }.joinToString("\n") { (name, value) -> "$name=$value" } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/AndroidUpdate.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/AndroidUpdate.kt new file mode 100644 index 000000000..af4357fb8 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/AndroidUpdate.kt @@ -0,0 +1,422 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.ServerAddress +import java.net.URI +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +/** + * One published GitHub release the app can update to. + * + * The app is distributed as an APK on GitHub Releases (tags `android-v*`), so + * its update check reads the public releases list of the repository + * instead of asking the server. Releases for other parts of the project carry + * other tag prefixes and are filtered out by [androidVersionFromTag]. + */ +@Serializable +data class AndroidRelease( + /** Version without the tag prefix, e.g. `1.2.3` for `android-v1.2.3`. */ + val version: String, + /** The release page, opened in a Custom Tab. */ + val htmlUrl: String, + /** A pre-release; never offered as an update. */ + val prerelease: Boolean = false, + /** A draft; never offered as an update. */ + val draft: Boolean = false, + /** The release's downloadable files, named `ompweb-.apk` and `.sha256`. */ + val assets: List = emptyList(), +) + +/** One file attached to a release. */ +@Serializable +data class AndroidReleaseAsset( + val name: String, + val downloadUrl: String, + val size: Long = 0, +) + +/** + * The newest update one source offers, independent of where it came from. + * + * GitHub releases carry no `versionCode`, so [versionCode] is only set by a + * custom feed; the APK's own version code is checked before installing + * either way. [pageUrl] exists only for GitHub releases (the banner may offer + * "Release page" there); a custom feed has no page to open. + * + * This model is also what the six-hour cache stores: it holds no secrets and + * no request state, only what was announced. + */ +@Serializable +data class AndroidUpdateCandidate( + val versionName: String, + val versionCode: Long? = null, + val pageUrl: String? = null, + val apkUrl: String? = null, + val apkSize: Long? = null, + /** The feed's SHA-256, or null when [sha256Url] has to be fetched (GitHub asset). */ + val sha256: String? = null, + /** The release's `ompweb-.apk.sha256` asset. */ + val sha256Url: String? = null, + val notes: String? = null, +) + +/** The persisted result of one successful check (`ompweb-android-update`). */ +@Serializable +data class AndroidUpdateCache( + val checkedAtMillis: Long, + /** + * Where the answer came from: [ANDROID_UPDATE_GITHUB_SOURCE] or the custom + * feed URL. A cache from another source is never shown. + */ + val sourceKey: String = ANDROID_UPDATE_GITHUB_SOURCE, + /** The announced update; null means "checked, up to date". */ + val candidate: AndroidUpdateCandidate? = null, + /** + * The installed `versionCode` the check was made for. An app update + * invalidates the answer: the new build may itself be the announced one, + * and the source must be asked again at least once after the install. + * Older caches (written before this field existed) deserialize as 0 and + * therefore also force one check after the upgrade. + */ + val installedVersionCode: Long = 0, +) + +/** The banner/card state: an update is available when [notice] is non-null. */ +data class AndroidUpdateNotice( + val currentVersion: String, + val availableVersion: String, + /** GitHub release page, when the source has one. */ + val releaseUrl: String? = null, + /** Whether this build can download and install the APK itself. */ + val installable: Boolean = false, +) + +/** The tag prefix used for Android releases in this repository. */ +const val ANDROID_RELEASE_TAG_PREFIX: String = "android-v" + +/** The [AndroidUpdateCache.sourceKey] of GitHub Releases. */ +const val ANDROID_UPDATE_GITHUB_SOURCE: String = "github" + +/** How long a successful check is reused before the source is asked again. */ +const val ANDROID_UPDATE_CACHE_MILLIS: Long = 6L * 60L * 60L * 1000L + +/** + * How old the last successful check may be when the app comes back to the + * foreground. The process lives for days, so the six-hour cache would leave a + * published update unseen for most of that time; past this window the source + * is asked again on every later return. + */ +const val ANDROID_UPDATE_FOREGROUND_CACHE_MILLIS: Long = 30L * 60L * 1000L + +/** A downloaded update larger than this is refused before and during the download. */ +const val MAX_ANDROID_UPDATE_BYTES: Long = 200L * 1024L * 1024L + +/** A custom feed may not exceed this; it is a handful of fields. */ +const val MAX_ANDROID_UPDATE_FEED_BYTES: Long = 64L * 1024L + +/** A `.sha256` file is one line; anything larger is not a checksum. */ +const val MAX_ANDROID_UPDATE_CHECKSUM_BYTES: Long = 4L * 1024L + +/** The only release pages the update notice may open. */ +private const val ANDROID_RELEASE_PAGE_PATH = "/kahme247/ompweb/releases/" + +/** The only download URLs the GitHub source may hand to the downloader. */ +private const val ANDROID_RELEASE_ASSET_PATH = "/kahme247/ompweb/releases/download/" + +private val SHA256_HEX = Regex("[0-9a-f]{64}") + +/** Whether [raw] is a 64-character hexadecimal SHA-256 digest. */ +fun isAndroidUpdateSha256(raw: String?): Boolean = + raw != null && SHA256_HEX.matches(raw.trim().lowercase()) + +/** Lower-cases a valid digest; null for anything else. */ +fun normalizeAndroidUpdateSha256(raw: String?): String? = + raw?.trim()?.lowercase()?.takeIf { SHA256_HEX.matches(it) } + +/** + * Whether [raw] is an `https` page under this repository's GitHub releases. + * The API answer and the persisted cache are both untrusted input: checking + * only the scheme would let a manipulated entry send the banner's "Release + * page" button to a phishing host. + */ +fun isAndroidReleasePage(raw: String): Boolean { + val uri = parseHttpsUri(raw) ?: return false + return uri.host.equals("github.com", ignoreCase = true) && + (uri.path.orEmpty()).startsWith(ANDROID_RELEASE_PAGE_PATH) +} + +/** + * Whether [raw] is a download file under this repository's GitHub releases. + * A release asset redirects to a storage host; the request URL itself must be + * the repository's, so a manipulated releases answer cannot point the + * downloader at an arbitrary server. + */ +fun isAndroidReleaseAsset(raw: String): Boolean { + val uri = parseHttpsUri(raw) ?: return false + return uri.host.equals("github.com", ignoreCase = true) && + (uri.path.orEmpty()).startsWith(ANDROID_RELEASE_ASSET_PATH) +} + +private fun parseHttpsUri(raw: String): URI? { + if (!raw.startsWith("https://", ignoreCase = true)) return null + val uri = runCatching { URI(raw) }.getOrNull() ?: return null + if (!uri.scheme.equals("https", ignoreCase = true)) return null + if (uri.rawUserInfo != null) return null + return uri +} + +/** + * The version part of an `android-v*` tag, or null for every other tag (web + * and omp releases share the same list). + */ +fun androidVersionFromTag(tag: String): String? { + if (!tag.startsWith(ANDROID_RELEASE_TAG_PREFIX)) return null + return tag.removePrefix(ANDROID_RELEASE_TAG_PREFIX).trim().takeIf { it.isNotEmpty() } +} + +/** + * Strips the decoration `versionName` may carry: a leading `v`, build + * metadata after `+`, and the debug build's `-debug` suffix. A real + * pre-release suffix (`-beta.1`) is kept for [parseAndroidVersion]. + */ +fun normalizeInstalledVersion(raw: String): String = + raw.trim() + .removePrefix("v") + .removePrefix("V") + .substringBefore('+') + .removeSuffix("-debug") + .trim() + +private data class ParsedAndroidVersion(val core: List, val preRelease: String?) + +private fun parseAndroidVersion(raw: String): ParsedAndroidVersion? { + val version = normalizeInstalledVersion(raw) + if (version.isEmpty()) return null + val preRelease = version.substringAfter('-', missingDelimiterValue = "").takeIf { it.isNotEmpty() } + val corePart = version.substringBefore('-') + val segments = corePart.split('.') + if (segments.isEmpty() || segments.any { it.isEmpty() || !it.all(Char::isDigit) }) return null + return ParsedAndroidVersion(segments.map { it.toIntOrNull() ?: return null }, preRelease) +} + +/** Whether [raw] is a numeric version the comparison understands (`1.2.3`, `1.2.3-beta.1`). */ +fun isParsableAndroidVersion(raw: String): Boolean = parseAndroidVersion(raw) != null + +/** + * Compares two version strings numerically (`1.0.10` > `1.0.9`, `1.1` == + * `1.1.0`); a release sorts above the pre-release of the same core. An + * unparsable side compares equal, so it can never fake an update. + */ +fun compareAndroidVersions(left: String, right: String): Int { + val a = parseAndroidVersion(left) ?: return 0 + val b = parseAndroidVersion(right) ?: return 0 + val count = maxOf(a.core.size, b.core.size) + for (index in 0 until count) { + val difference = a.core.getOrElse(index) { 0 }.compareTo(b.core.getOrElse(index) { 0 }) + if (difference != 0) return difference + } + return when { + a.preRelease == null && b.preRelease == null -> 0 + a.preRelease == null -> 1 + b.preRelease == null -> -1 + else -> a.preRelease.compareTo(b.preRelease, ignoreCase = true) + } +} + +/** Whether [nowMillis] is still inside the 6-hour window of a successful check. */ +fun isAndroidUpdateCacheFresh(cache: AndroidUpdateCache, nowMillis: Long): Boolean { + val age = nowMillis - cache.checkedAtMillis + return age in 0 until ANDROID_UPDATE_CACHE_MILLIS +} + +/** + * The newest published, non-prerelease release that is newer than + * [currentVersion]; null when the installed build is current. Drafts and + * prereleases are excluded, and a release without a page under this + * repository's GitHub releases is ignored because the banner may only open + * that page. + */ +fun newestAndroidRelease(releases: List, currentVersion: String): AndroidRelease? { + val installed = normalizeInstalledVersion(currentVersion) + return releases + .asSequence() + .filter { !it.draft && !it.prerelease } + .filter { isAndroidReleasePage(it.htmlUrl) } + .filter { compareAndroidVersions(it.version, installed) > 0 } + .maxWithOrNull { left, right -> compareAndroidVersions(left.version, right.version) } +} + +/** + * Whether [candidate] is an update over the installed build. A source that + * announces a `versionCode` (custom feed) is judged by it, because Android + * decides installability by that number; GitHub releases only carry a name. + */ +fun isAndroidUpdateCandidateNewer( + candidate: AndroidUpdateCandidate, + currentVersion: String, + currentVersionCode: Long, +): Boolean { + val announcedCode = candidate.versionCode + if (announcedCode != null) return announcedCode > currentVersionCode + return compareAndroidVersions(candidate.versionName, currentVersion) > 0 +} + +/** Whether this candidate comes with everything needed to install it. */ +val AndroidUpdateCandidate.canInstall: Boolean + get() = apkUrl?.let(::isAndroidUpdateUrlAllowed) == true && + apkSize != null && + apkSize in 1..MAX_ANDROID_UPDATE_BYTES && + (sha256 == null || isAndroidUpdateSha256(sha256)) && + (sha256 != null || sha256Url?.let(::isAndroidReleaseAsset) == true) + +/** + * The banner state for [candidate] unless that version was dismissed. + * Dismissing is per version: a newer release shows again, the dismissed one + * never does. + */ +fun androidUpdateNotice( + candidate: AndroidUpdateCandidate?, + currentVersion: String, + currentVersionCode: Long, + dismissedVersion: String?, +): AndroidUpdateNotice? { + val update = candidate ?: return null + if (!isAndroidUpdateCandidateNewer(update, currentVersion, currentVersionCode)) return null + if (dismissedVersion != null && compareAndroidVersions(dismissedVersion, update.versionName) >= 0) { + return null + } + return AndroidUpdateNotice( + currentVersion = normalizeInstalledVersion(currentVersion), + availableVersion = update.versionName, + releaseUrl = update.pageUrl?.takeIf(::isAndroidReleasePage), + installable = update.canInstall, + ) +} + +/** A custom feed address the user entered: https anywhere, plain http only in a private network. */ +sealed interface AndroidUpdateFeedUrlResult { + /** The normalized absolute URL to fetch. */ + data class Valid(val url: String) : AndroidUpdateFeedUrlResult + + /** [message] is shown under the input field. */ + data class Invalid(val message: String) : AndroidUpdateFeedUrlResult +} + +/** + * Validates the feed address against the same rules as the server address + * (`ServerAddress`): `https://` anywhere, plain `http://` only for loopback, + * RFC 1918, CGNAT, link-local, `.local` and Tailscale names. The network + * layer enforces the resolved peer address again ([HttpPolicyInterceptor]). + */ +fun parseAndroidUpdateFeedUrl(raw: String): AndroidUpdateFeedUrlResult { + val input = raw.trim() + if (input.isEmpty()) return AndroidUpdateFeedUrlResult.Invalid("Enter the address of the update feed.") + if (input.length > 2048) return AndroidUpdateFeedUrlResult.Invalid("The address is too long.") + if (input.any { it.isWhitespace() || it.code < 0x20 || it.code == 0x7f }) { + return AndroidUpdateFeedUrlResult.Invalid("The address must not contain spaces.") + } + val uri = runCatching { URI(input) }.getOrNull() + ?: return AndroidUpdateFeedUrlResult.Invalid("Enter a complete address, for example https://example.com/ompweb/latest.json.") + val scheme = uri.scheme?.lowercase() + if (scheme != "http" && scheme != "https") { + return AndroidUpdateFeedUrlResult.Invalid("Only http:// and https:// addresses are supported.") + } + if (uri.rawUserInfo != null) { + return AndroidUpdateFeedUrlResult.Invalid("Do not include a username or password in the address.") + } + val host = uri.host?.lowercase()?.removeSuffix(".").orEmpty() + if (host.isEmpty()) { + return AndroidUpdateFeedUrlResult.Invalid("Enter a complete address, for example https://example.com/ompweb/latest.json.") + } + if (scheme == "http" && !ServerAddress.allowsPlainHttp(host)) { + return AndroidUpdateFeedUrlResult.Invalid(ANDROID_UPDATE_PLAIN_HTTP_MESSAGE) + } + return AndroidUpdateFeedUrlResult.Valid(uri.toString()) +} + +/** The plain-HTTP rule, worded like the settings screen's server address message. */ +const val ANDROID_UPDATE_PLAIN_HTTP_MESSAGE: String = ServerAddress.PLAIN_HTTP_MESSAGE + +/** + * Whether the updater may fetch [raw]: `https` anywhere, plain `http` only for + * a private host. Applied to the feed address, the feed's `apkUrl` and the + * checksum URL, so a feed cannot redirect the updater to a public plain-HTTP + * server. + */ +fun isAndroidUpdateUrlAllowed(raw: String): Boolean { + val uri = runCatching { URI(raw.trim()) }.getOrNull() ?: return false + if (uri.rawUserInfo != null) return false + return when (uri.scheme?.lowercase()) { + "https" -> true + "http" -> uri.host?.lowercase()?.let(ServerAddress::allowsPlainHttp) == true + else -> false + } +} + +/** The parsed and validated body of a custom feed (`/latest.json`). */ +@Serializable +data class AndroidUpdateFeed( + val versionName: String, + val versionCode: Long, + /** Absolute after [parseAndroidUpdateFeed]; the feed may send a relative path. */ + val apkUrl: String, + val sha256: String, + val size: Long, + val notes: String? = null, +) + +sealed interface AndroidUpdateFeedResult { + data class Parsed(val feed: AndroidUpdateFeed) : AndroidUpdateFeedResult + + /** [reason] is for diagnostics and tests; the UI reports a failed check. */ + data class Invalid(val reason: String) : AndroidUpdateFeedResult +} + +private val feedJson = Json { ignoreUnknownKeys = true } + +/** + * Parses `{"versionName","versionCode","apkUrl","sha256","size","notes"?}`. + * + * `apkUrl` may be relative to [feedUrl]. Every URL in the answer has to pass + * [isAndroidUpdateUrlAllowed]; the feed is untrusted input, so a single bad + * field rejects the whole answer instead of installing something partial. + */ +fun parseAndroidUpdateFeed(body: String, feedUrl: String): AndroidUpdateFeedResult { + val decoded = runCatching { feedJson.decodeFromString(body) }.getOrNull() + ?: return AndroidUpdateFeedResult.Invalid("The feed is not valid JSON with the required fields.") + val versionName = normalizeInstalledVersion(decoded.versionName) + if (!isParsableAndroidVersion(versionName)) { + return AndroidUpdateFeedResult.Invalid("The feed has no valid versionName.") + } + if (decoded.versionCode <= 0) { + return AndroidUpdateFeedResult.Invalid("The feed has no valid versionCode.") + } + if (versionName.length > 64) { + return AndroidUpdateFeedResult.Invalid("The feed's versionName is too long.") + } + val sha256 = normalizeAndroidUpdateSha256(decoded.sha256) + ?: return AndroidUpdateFeedResult.Invalid("The feed has no valid SHA-256.") + if (decoded.size !in 1..MAX_ANDROID_UPDATE_BYTES) { + return AndroidUpdateFeedResult.Invalid("The feed's APK size is missing or too large.") + } + val apkUrl = resolveAndroidUpdateUrl(feedUrl, decoded.apkUrl) + ?: return AndroidUpdateFeedResult.Invalid("The feed's apkUrl is not an allowed address.") + val notes = decoded.notes?.trim()?.takeIf { it.isNotEmpty() }?.take(2000) + return AndroidUpdateFeedResult.Parsed( + decoded.copy( + versionName = versionName, + apkUrl = apkUrl, + sha256 = sha256, + notes = notes, + ), + ) +} + +/** Resolves [raw] against [base] and returns it only when the updater may fetch it. */ +fun resolveAndroidUpdateUrl(base: String, raw: String): String? { + val trimmed = raw.trim() + if (trimmed.isEmpty()) return null + val resolved = runCatching { URI(base).resolve(trimmed) }.getOrNull() ?: return null + val absolute = resolved.toString() + return absolute.takeIf(::isAndroidUpdateUrlAllowed) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/AndroidUpdateInstall.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/AndroidUpdateInstall.kt new file mode 100644 index 000000000..6c28bf36e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/AndroidUpdateInstall.kt @@ -0,0 +1,126 @@ +package io.github.kahme247.ompweb.domain + +import java.io.File +import java.io.InputStream +import java.security.MessageDigest + +/** + * What PackageManager reports about one APK — the installed app or a + * downloaded file. Both sides are read with `GET_SIGNING_CERTIFICATES`, so the + * comparison uses the same certificate source Android uses to decide whether + * an update may replace an app. + */ +data class AndroidApkIdentity( + val packageName: String, + val versionCode: Long, + val versionName: String?, + /** SHA-256 of every current signer certificate, lower-case hex. */ + val signerSha256: Set, +) + +/** The outcome of the local checks that run before PackageInstaller opens a session. */ +sealed interface AndroidInstallDecision { + data object Install : AndroidInstallDecision + + /** [message] is shown to the user; nothing was installed. */ + data class Refuse(val message: String) : AndroidInstallDecision +} + +/** + * Decides whether a downloaded update may replace the installed app. + * + * The checks are the ones Android would silently fail on — or, worse, accept + * from another package — so they run here with a clear message instead: + * checksum, package name, announced version code, a strictly higher version + * code and the signing certificate of the installed app. Only a positive + * result of every check leads to an install. + */ +fun decideAndroidUpdateInstall( + appPackageName: String, + installed: AndroidApkIdentity?, + downloaded: AndroidApkIdentity?, + announcedVersionCode: Long?, + expectedSha256: String, + actualSha256: String, +): AndroidInstallDecision { + if (downloaded == null) { + return AndroidInstallDecision.Refuse( + "The downloaded file is not an installable app package.", + ) + } + val expected = normalizeAndroidUpdateSha256(expectedSha256) + ?: return AndroidInstallDecision.Refuse("The update has no valid SHA-256 to verify.") + val actual = normalizeAndroidUpdateSha256(actualSha256) + if (expected != actual) { + return AndroidInstallDecision.Refuse( + "The downloaded file is damaged or was tampered with (SHA-256 mismatch). It was not installed.", + ) + } + if (installed == null) { + return AndroidInstallDecision.Refuse( + "The installed app's version information could not be read.", + ) + } + if (installed.packageName != appPackageName) { + return AndroidInstallDecision.Refuse( + "The installed app does not match this package (${installed.packageName}).", + ) + } + if (downloaded.packageName != appPackageName) { + return AndroidInstallDecision.Refuse( + "The downloaded app is for another package (${downloaded.packageName}). It was not installed.", + ) + } + if (announcedVersionCode != null && downloaded.versionCode != announcedVersionCode) { + return AndroidInstallDecision.Refuse( + "The downloaded app's version code (${downloaded.versionCode}) does not match the announced one " + + "($announcedVersionCode). It was not installed.", + ) + } + if (downloaded.versionCode <= installed.versionCode) { + return AndroidInstallDecision.Refuse( + "The downloaded app (version code ${downloaded.versionCode}) is not newer than the installed " + + "version (${installed.versionCode}). It was not installed.", + ) + } + if (installed.signerSha256.isEmpty() || downloaded.signerSha256.isEmpty()) { + return AndroidInstallDecision.Refuse( + "The signing certificate of the app could not be read. The update was not installed.", + ) + } + if (installed.signerSha256 != downloaded.signerSha256) { + return AndroidInstallDecision.Refuse( + "The downloaded app is signed with a different key than the installed app. It was not installed.", + ) + } + return AndroidInstallDecision.Install +} + +/** The first SHA-256 digest in a `.sha256` asset (GNU `sha256sum` format). */ +fun parseAndroidUpdateSha256Asset(text: String): String? { + val match = Regex("(?i)\\b[0-9a-f]{64}\\b").find(text) ?: return null + return normalizeAndroidUpdateSha256(match.value) +} + +/** SHA-256 of [input] as lower-case hex. The stream is not closed. */ +fun sha256Hex(input: InputStream): String { + val digest = MessageDigest.getInstance("SHA-256") + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + while (true) { + val read = input.read(buffer) + if (read < 0) break + digest.update(buffer, 0, read) + } + return digest.digest().joinToString("") { "%02x".format(it) } +} + +/** SHA-256 of [file]; null when the file cannot be read. */ +fun sha256Hex(file: File): String? = + runCatching { file.inputStream().use(::sha256Hex) }.getOrNull() + +/** + * Whether [candidate]'s APK size is a plausible download. The announced size + * is the cap for the transfer; [MAX_ANDROID_UPDATE_BYTES] is the absolute one. + */ +fun isAndroidUpdateSizeAllowed(size: Long?): Boolean = + size != null && size in 1..MAX_ANDROID_UPDATE_BYTES diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/Attachments.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Attachments.kt new file mode 100644 index 000000000..3ff40e08c --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Attachments.kt @@ -0,0 +1,291 @@ +package io.github.kahme247.ompweb.domain + +import java.io.ByteArrayOutputStream +import java.io.InputStream + +/** + * Attachment budgets and composition, ported from `lib/chat-attachments.ts` and + * `lib/image-attachments.ts`. + * + * The mobile composer and the RPC payload builder must agree on the same + * numbers as the server routes: 8 MiB JSON command body, 5 MiB decoded images, + * 4 MiB attached text across at most 10 files. + */ + +/** Complete JSON body accepted by `POST /api/agent/{id}` and `/api/agent/new`. */ +const val MAX_AGENT_COMMAND_REQUEST_BYTES: Int = 8 * 1024 * 1024 + +/** Decoded bytes of all images in one message. */ +const val MAX_TOTAL_ATTACHED_IMAGE_BYTES: Int = 5 * 1024 * 1024 + +/** Decoded bytes of a single image; a lone image may fill the whole budget. */ +const val MAX_ATTACHED_IMAGE_BYTES: Int = MAX_TOTAL_ATTACHED_IMAGE_BYTES + +const val MAX_ATTACHED_IMAGES: Int = 10 + +/** Text-attachment budget, enforced on the aggregate rather than per file. */ +const val MAX_TOTAL_ATTACHED_TEXT_BYTES: Int = 4 * 1024 * 1024 + +/** A lone text file may fill the whole text budget. */ +const val MAX_ATTACHED_TEXT_BYTES: Int = MAX_TOTAL_ATTACHED_TEXT_BYTES + +const val MAX_ATTACHED_TEXT_FILES: Int = 10 + +/** `{"type":"image","data":"","mimeType":"",},` plus slack for the mime type. */ +private const val IMAGE_ENTRY_OVERHEAD_BYTES = 96 + +/** Command type, streaming behavior, and the surrounding JSON object. */ +private const val COMMAND_ENVELOPE_BYTES = 256 + +/** Attachment extensions that are always text; Markdown is decided by [isMarkdownFile]. */ +private val TEXT_FILE_EXTENSIONS = setOf("txt", "text") + +/** One base64 image ready for the RPC `images` array. */ +data class ComposerImage(val data: String, val mimeType: String) + +/** One text file inlined into the prompt as an `Attached file:` block. */ +data class AttachedTextFile( + val name: String, + val mimeType: String, + val content: String, + val size: Int, +) + +/** What the composer already holds (or has in flight) when a new batch lands. */ +data class TextAttachmentBudget(val usedBytes: Int, val usedSlots: Int) + +data class TextAttachmentSelection( + val accepted: List, + /** Candidates dropped for exceeding the per-file cap. */ + val tooLarge: Int, + /** Candidates dropped because the message's aggregate text budget was spent. */ + val overBudget: Int, +) + +fun fileExtension(name: String): String = + name.lowercase().replace('\\', '/').substringAfterLast('/').substringAfterLast('.', missingDelimiterValue = "") + +/** Human-readable limit for the composer banners ("512 KB" / "4 MB"). */ +fun formatAttachmentBytes(bytes: Int): String = if (bytes >= 1024 * 1024) { + "${Math.round(bytes / (1024.0 * 1024.0))} MB" +} else { + "${Math.round(bytes / 1024.0)} KB" +} + +/** + * Per-file, aggregate and slot rules for text attachments — the single place + * the composer and draft restore both enforce them. Candidates past the + * remaining slots are dropped silently; a candidate too large on its own is + * reported before a budget miss so the banner names the limit actually hit. + */ +fun selectTextAttachments( + candidates: List, + sizeOf: (T) -> Int, + budget: TextAttachmentBudget, +): TextAttachmentSelection { + val accepted = ArrayList(candidates.size) + val remainingSlots = (MAX_ATTACHED_TEXT_FILES - budget.usedSlots).coerceAtLeast(0) + var totalBytes = budget.usedBytes + var tooLarge = 0 + var overBudget = 0 + for (candidate in candidates) { + if (accepted.size >= remainingSlots) break + val size = sizeOf(candidate) + if (size < 0 || size > MAX_ATTACHED_TEXT_BYTES) { + tooLarge++ + continue + } + if (totalBytes + size > MAX_TOTAL_ATTACHED_TEXT_BYTES) { + overBudget++ + continue + } + totalBytes += size + accepted += candidate + } + return TextAttachmentSelection(accepted, tooLarge, overBudget) +} + +/** Banner text for a batch that produced no usable attachments. */ +fun describeTextAttachmentSkip(selection: TextAttachmentSelection<*>): String? { + if (selection.tooLarge > 0) { + return "${selection.tooLarge} file(s) skipped: files up to " + + "${formatAttachmentBytes(MAX_ATTACHED_TEXT_BYTES)} are supported." + } + if (selection.overBudget > 0) { + return "${selection.overBudget} file(s) skipped: attachments are limited to " + + "${formatAttachmentBytes(MAX_TOTAL_ATTACHED_TEXT_BYTES)} per message." + } + return null +} + +fun isTextAttachmentFile(name: String, mimeType: String): Boolean = + mimeType == "text/plain" || mimeType == "text/markdown" || + fileExtension(name) in TEXT_FILE_EXTENSIONS || isMarkdownFile(name) + +private fun languageForFile(name: String): String = + if (isMarkdownFile(name)) "markdown" else "text" + +private fun fenceForContent(content: String): String { + val longestRun = Regex("`+").findAll(content).maxOfOrNull { it.value.length } ?: 0 + return "`".repeat(maxOf(3, longestRun + 1)) +} + +/** + * Attachment names are provider-supplied metadata (a share can name itself + * anything): keep them on one line and free of fence delimiters so a crafted + * name cannot open or close the attachment block. + */ +fun sanitizeAttachmentName(name: String, maxLength: Int = 120): String { + val cleaned = buildString { + for (char in name) { + when { + char == '`' -> append('\'') + char == '\r' || char == '\n' || char.isISOControl() -> append(' ') + else -> append(char) + } + } + }.replace(Regex("\\s{2,}"), " ").trim() + return cleaned.take(maxLength).ifEmpty { "attachment" } +} + +/** Add text-file contents to the prompt while keeping the attachment boundary clear. */ +fun composeMessageWithTextAttachments(message: String, files: List): String { + if (files.isEmpty()) return message + val blocks = files.map { file -> + val name = sanitizeAttachmentName(file.name) + val fence = fenceForContent(file.content) + "Attached file: $name\n$fence${languageForFile(name)}\n${file.content}\n$fence" + } + return (listOf(message.trim()) + blocks).filter { it.isNotEmpty() }.joinToString("\n\n") +} + +/** + * Merge a share batch into a draft's images within the outgoing budgets, + * dropping byte-identical duplicates. Existing images win so a share never + * displaces what the user already attached. + */ +fun mergeImageAttachments( + existing: List, + incoming: List, +): List { + val merged = existing.toMutableList() + val seen = existing.mapTo(HashSet()) { it.data } + var totalBytes = existing.sumOf { base64DecodedByteLength(it.data) ?: 0 } + for (image in incoming) { + if (!seen.add(image.data)) continue + if (!image.mimeType.startsWith("image/")) continue + val bytes = base64DecodedByteLength(image.data) ?: continue + if (merged.size >= MAX_ATTACHED_IMAGES || bytes > MAX_ATTACHED_IMAGE_BYTES) continue + if (totalBytes + bytes > MAX_TOTAL_ATTACHED_IMAGE_BYTES) continue + merged += image + totalBytes += bytes + } + return merged +} + +/** + * Read [stream] fully, but only while it stays within [cap] bytes. Returns + * `null` when the source is larger: a silently truncated attachment would + * reach the model as if it were complete. + */ +fun readCappedOrNull(stream: InputStream, cap: Int): ByteArray? { + val out = ByteArrayOutputStream(minOf(cap, 64 * 1024)) + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var total = 0 + while (true) { + val read = stream.read(buffer) + if (read < 0) break + total += read + if (total > cap) return null + out.write(buffer, 0, read) + } + return out.toByteArray() +} + +private fun isBase64DataChar(code: Int): Boolean = + (code in 0x41..0x5a) || (code in 0x61..0x7a) || (code in 0x30..0x39) || + code == 0x2b || code == 0x2f + +/** Decoded byte length of a well-formed base64 payload, or null. */ +fun base64DecodedByteLength(data: String): Int? { + if (data.isEmpty() || data.length % 4 != 0) return null + val padding = when { + data.endsWith("==") -> 2 + data.endsWith("=") -> 1 + else -> 0 + } + val dataEnd = data.length - padding + for (index in 0 until dataEnd) { + if (!isBase64DataChar(data[index].code)) return null + } + for (index in dataEnd until data.length) { + if (data[index] != '=') return null + } + return (data.length / 4) * 3 - padding +} + +private fun imageByteLengthWithinLimits(image: ComposerImage): Int? { + if (!image.mimeType.startsWith("image/")) return null + val bytes = base64DecodedByteLength(image.data) ?: return null + return bytes.takeIf { it <= MAX_ATTACHED_IMAGE_BYTES } +} + +/** Count, per-image and aggregate rules for one outgoing `images` array. */ +fun validateImageBudget(images: List): String? { + if (images.size > MAX_ATTACHED_IMAGES) { + return "A message can include at most $MAX_ATTACHED_IMAGES images" + } + var totalBytes = 0 + for (image in images) { + val bytes = imageByteLengthWithinLimits(image) + ?: return "Each image must be valid base64 image data of " + + "${MAX_ATTACHED_IMAGE_BYTES / (1024 * 1024)}MB or smaller" + totalBytes += bytes + } + if (totalBytes > MAX_TOTAL_ATTACHED_IMAGE_BYTES) { + return "Attached images must total ${MAX_TOTAL_ATTACHED_IMAGE_BYTES / (1024 * 1024)}MB or less" + } + return null +} + +/** UTF-8 size of [value] once JSON-escaped, matching `JSON.stringify` bytes. */ +fun jsonStringByteLength(value: String): Int { + var bytes = 2 // the surrounding quotes + var index = 0 + while (index < value.length) { + val codePoint = value.codePointAt(index) + when (codePoint) { + 0x22, 0x5c -> bytes += 2 + 0x08, 0x0c, 0x0a, 0x0d, 0x09 -> bytes += 2 + in 0x00..0x1f -> bytes += 6 + in 0x20..0x7e -> bytes += 1 + in 0x80..0x7ff -> bytes += 2 + in 0x800..0xffff -> bytes += 3 + else -> bytes += 4 + } + index += Character.charCount(codePoint) + } + return bytes +} + +/** + * Size of the JSON command body an outgoing prompt produces. Base64 data is + * ASCII and needs no escaping, so it is measured by length. + */ +fun agentRequestByteLength(message: String, images: List): Int { + var bytes = COMMAND_ENVELOPE_BYTES + jsonStringByteLength(message) + for (image in images) { + bytes += IMAGE_ENTRY_OVERHEAD_BYTES + image.data.length + jsonStringByteLength(image.mimeType) + } + return bytes +} + +/** Preflight for the composer: reject a prompt the agent route would refuse. */ +fun validateOutgoingPrompt(message: String, images: List): String? { + validateImageBudget(images)?.let { return it } + if (agentRequestByteLength(message, images) > MAX_AGENT_COMMAND_REQUEST_BYTES) { + return "This message is too large to send: keep it under " + + "${MAX_AGENT_COMMAND_REQUEST_BYTES / (1024 * 1024)}MB including attachments." + } + return null +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/Btw.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Btw.kt new file mode 100644 index 000000000..60eb105ff --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Btw.kt @@ -0,0 +1,221 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.doubleOrNull + +/** + * Side questions (omp's `/btw`): record shapes and the pure state transitions + * applied to `btw_record`/`btw_delta` frames, `btw` responses and + * `get_btw_history` snapshots (web `lib/btw.ts`). + * + * These can arrive in any order relative to each other (SSE vs. HTTP), so a + * snapshot never rolls back what is already shown. + */ +enum class BtwStatus { RUNNING, COMPLETE, CANCELLED, ERROR, INTERRUPTED } + +/** One question/answer exchange; follow-ups of a topic are [BtwTurn]s too. */ +data class BtwTurn( + val question: String, + val answer: String, + val status: BtwStatus, + val createdAt: Long, + val updatedAt: Long, + val error: String? = null, +) + +/** A topic: the first turn's fields inline plus every follow-up after it. */ +data class BtwRecord( + val id: String, + val leafId: String?, + val question: String, + val answer: String, + val status: BtwStatus, + val createdAt: Long, + val updatedAt: Long, + val error: String? = null, + val followUps: List = emptyList(), +) { + val rootTurn: BtwTurn + get() = BtwTurn(question, answer, status, createdAt, updatedAt, error) +} + +fun btwTurns(record: BtwRecord): List = listOf(record.rootTurn) + record.followUps + +fun latestBtwTurn(record: BtwRecord): BtwTurn = record.followUps.lastOrNull() ?: record.rootTurn + +/** + * What Copy copies: the newest answer with text. A follow-up that was + * cancelled or failed before writing anything leaves the earlier answer + * visible, so it stays copyable. + */ +fun latestBtwAnswer(record: BtwRecord): String = + btwTurns(record).lastOrNull { it.answer.trim().isNotEmpty() }?.answer.orEmpty() + +/** Epoch ms a `Date` can hold (omp's schema range); outside it nothing renders. */ +private const val MAX_EPOCH_MS: Double = 8.64e15 + +private fun JsonPrimitive.isPlainString(): Boolean = isString + +private fun JsonObject.stringField(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf(JsonPrimitive::isPlainString)?.content + +/** The frame's `type`, tolerant of a missing/non-string field. */ +internal fun JsonObject.btwType(): String? = stringField("type") + +internal fun JsonObject.btwString(key: String): String? = stringField(key) + +/** A JSON number inside omp's epoch range; a quoted number or anything else is not. */ +private fun JsonElement?.epochMs(): Long? { + val primitive = this as? JsonPrimitive ?: return null + if (primitive.isString) return null + val value = primitive.doubleOrNull ?: return null + if (value < 0 || value > MAX_EPOCH_MS) return null + return value.toLong() +} + +internal fun parseBtwStatus(value: String?): BtwStatus? = when (value) { + "running" -> BtwStatus.RUNNING + "complete" -> BtwStatus.COMPLETE + "cancelled" -> BtwStatus.CANCELLED + "error" -> BtwStatus.ERROR + "interrupted" -> BtwStatus.INTERRUPTED + else -> null +} + +private fun parseTurn(element: JsonElement?): BtwTurn? { + val obj = element as? JsonObject ?: return null + val question = obj.stringField("question") ?: return null + val answer = obj.stringField("answer") ?: return null + val status = parseBtwStatus(obj.stringField("status")) ?: return null + val createdAt = obj["createdAt"].epochMs() ?: return null + val updatedAt = obj["updatedAt"].epochMs() ?: return null + // Absent is fine; present must be a string (`null` is not, as on the web). + val error = if (obj.containsKey("error")) { + (obj["error"] as? JsonPrimitive)?.takeIf(JsonPrimitive::isPlainString)?.content ?: return null + } else { + null + } + return BtwTurn(question, answer, status, createdAt, updatedAt, error) +} + +/** A well-formed record, or null for anything omp could not have sent. */ +fun parseBtwRecord(element: JsonElement?): BtwRecord? { + val obj = element as? JsonObject ?: return null + val id = obj.stringField("id") ?: return null + val followUps = if (obj.containsKey("followUps")) { + val array = obj["followUps"] as? JsonArray ?: return null + array.map { parseTurn(it) ?: return null } + } else { + emptyList() + } + val turn = parseTurn(obj) ?: return null + return BtwRecord( + id = id, + leafId = obj.stringField("leafId"), + question = turn.question, + answer = turn.answer, + status = turn.status, + createdAt = turn.createdAt, + updatedAt = turn.updatedAt, + error = turn.error, + followUps = followUps, + ) +} + +/** + * `incoming` is older than `current`: it misses a turn, reports a finished + * turn as running, or lacks text that deltas already appended. + */ +private fun isStaleSnapshot(current: BtwRecord, incoming: BtwRecord): Boolean { + val have = current.followUps.size + val got = incoming.followUps.size + if (got != have) return got < have + val mine = latestBtwTurn(current) + val theirs = latestBtwTurn(incoming) + if (theirs.status != BtwStatus.RUNNING) return false + if (mine.status != BtwStatus.RUNNING) return true + return mine.answer.length > theirs.answer.length && mine.answer.startsWith(theirs.answer) +} + +private fun BtwRecord.updateLatestTurn(patch: (BtwTurn) -> BtwTurn): BtwRecord { + if (followUps.isEmpty()) { + val updated = patch(rootTurn) + return copy( + question = updated.question, + answer = updated.answer, + status = updated.status, + createdAt = updated.createdAt, + updatedAt = updated.updatedAt, + error = updated.error, + ) + } + return copy(followUps = followUps.dropLast(1) + patch(followUps.last())) +} + +/** omp's history order: newest topic first. */ +private val NEWEST_FIRST: Comparator = Comparator { a, b -> + val byDate = b.createdAt.compareTo(a.createdAt) + if (byDate != 0) byDate else a.id.compareTo(b.id) +} + +/** Insert (newest first) or replace a record unless the snapshot is stale. */ +fun upsertBtwRecord(records: List, incoming: BtwRecord): List { + val index = records.indexOfFirst { it.id == incoming.id } + if (index == -1) return listOf(incoming) + records + if (isStaleSnapshot(records[index], incoming)) return records + return records.toMutableList().also { it[index] = incoming } +} + +/** Apply one SSE frame; unrelated or malformed frames leave [records] as is. */ +fun applyBtwEvent(records: List, event: JsonObject): List { + when (event.btwType()) { + "btw_record" -> { + val record = parseBtwRecord(event["record"]) ?: return records + return upsertBtwRecord(records, record) + } + + "btw_delta" -> { + val recordId = event.btwString("recordId") ?: return records + val delta = event.btwString("delta")?.takeIf { it.isNotEmpty() } ?: return records + // Streamed text extends the latest turn of a running record only. + val index = records.indexOfFirst { it.id == recordId } + if (index == -1) return records + val latest = latestBtwTurn(records[index]) + if (latest.status != BtwStatus.RUNNING) return records + val next = records.toMutableList() + next[index] = records[index].updateLatestTurn { it.copy(answer = it.answer + delta) } + return next + } + + else -> return records + } +} + +/** + * Replace local state with a history snapshot, keeping records the snapshot + * predates (started after it was taken, or with newer streamed text). A record + * that was running before the request ([knownBefore]) but is missing from the + * snapshot cannot still be running: omp lost it (failed checkpoint, replaced + * child), so it becomes `interrupted` instead of spinning forever. + */ +fun mergeBtwHistory( + local: List, + snapshot: List, + knownBefore: Set, +): List { + val localById = local.associateBy { it.id } + val snapshotIds = snapshot.mapTo(HashSet()) { it.id } + val merged = snapshot.map { record -> + val mine = localById[record.id] + if (mine != null && isStaleSnapshot(mine, record)) mine else record + }.toMutableList() + for (record in local) { + if (record.id in snapshotIds) continue + val lost = record.id in knownBefore && latestBtwTurn(record).status == BtwStatus.RUNNING + merged += if (lost) record.updateLatestTurn { it.copy(status = BtwStatus.INTERRUPTED) } else record + } + return merged.sortedWith(NEWEST_FIRST) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ColdStartRestore.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ColdStartRestore.kt new file mode 100644 index 000000000..42ae25617 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ColdStartRestore.kt @@ -0,0 +1,90 @@ +package io.github.kahme247.ompweb.domain + +/** + * What the shell does once on a fresh process: reopen the last session (or the + * new-session pane) from the persisted marker. + * + * The restore runs from a Compose effect whose keys change while the session + * list loads. Deciding the step from a plain snapshot keeps the two rules that + * the live run broke in one testable place: + * + * - `pending` is only settled by a step that actually resolved the marker; an + * empty list is not proof that the saved session is gone, so the effect waits + * instead of burning its one attempt, and + * - a re-run (rotation, a key change) never opens a second pane: as soon as + * something is on screen the step is [Done]. + */ +sealed interface ColdStartRestoreStep { + /** + * The process-level cold-start marker is settled after this step: the + * restore either ran or found nothing left to restore. + */ + val consumes: Boolean + + /** The list is not loaded yet (or is empty): keep the marker and wait. */ + data object Wait : ColdStartRestoreStep { + override val consumes: Boolean = false + } + + /** Nothing to restore (already on screen, or no marker stored). */ + data object Done : ColdStartRestoreStep { + override val consumes: Boolean = true + } + + /** Open the saved session; the list contains it. */ + data class OpenSession(val id: String) : ColdStartRestoreStep { + override val consumes: Boolean = true + } + + /** Reopen the new-session pane in its saved workspace. */ + data class OpenNewSession(val cwd: String) : ColdStartRestoreStep { + override val consumes: Boolean = true + } + + /** + * The saved session is gone from a list that *is* loaded and non-empty: + * consume the marker so the next start opens the list, and forget it. + */ + data class ForgetSession(val id: String) : ColdStartRestoreStep { + override val consumes: Boolean = true + } +} + +/** + * The one cold-start decision (see [ColdStartRestoreStep]). + * + * @param pending the process-level "restore not decided yet" flag. + * @param listLoaded `GET /api/sessions` answered successfully (not merely + * "no request in flight"): only such a list can resolve the target. + * @param sessionIds the loaded session ids. + * @param selectedSessionId the session the shell already shows. + * @param newSessionCwd the new-session pane the shell already shows. + * @param lastSessionId the persisted session marker (`last_session_id`). + * @param lastNewSessionCwd the persisted new-session marker. + */ +fun coldStartRestoreStep( + pending: Boolean, + listLoaded: Boolean, + sessionIds: List, + selectedSessionId: String?, + newSessionCwd: String?, + lastSessionId: String?, + lastNewSessionCwd: String?, +): ColdStartRestoreStep { + if (!pending) return ColdStartRestoreStep.Done + // A re-run after the restore already navigated (rotation, recomposition): + // the marker is settled, but never open a second pane. + if (selectedSessionId != null || newSessionCwd != null) return ColdStartRestoreStep.Done + if (lastSessionId != null) { + if (sessionIds.contains(lastSessionId)) return ColdStartRestoreStep.OpenSession(lastSessionId) + // An empty list is what a cold start shows before (and without) its + // first answer — it must not decide that the saved session is gone. + return if (listLoaded && sessionIds.isNotEmpty()) { + ColdStartRestoreStep.ForgetSession(lastSessionId) + } else { + ColdStartRestoreStep.Wait + } + } + if (lastNewSessionCwd != null) return ColdStartRestoreStep.OpenNewSession(lastNewSessionCwd) + return ColdStartRestoreStep.Done +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ComposerPrefs.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ComposerPrefs.kt new file mode 100644 index 000000000..dd08b632c --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ComposerPrefs.kt @@ -0,0 +1,127 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.longOrNull + +/** + * Composer behaviors ported from `lib/composer-prefs.ts` plus the small pure + * helpers the composer needs (bash mode, caret insertion, prompt history and + * the per-session goal record of `lib/web-mode-state.ts`). + */ + +/** How a submit during a run is delivered (`omp-web:submit-during-run`). */ +enum class SubmitDuringRunBehavior(val wireName: String) { + STEER("steer"), + QUEUE("queue"); + + companion object { + /** Queuing stays resilient while a `!!` shell command owns the session. */ + val DEFAULT = QUEUE + + fun fromWire(value: String?): SubmitDuringRunBehavior = + entries.firstOrNull { it.wireName == value } ?: DEFAULT + } +} + +/** Completion sound (`omp-sound-enabled`), on unless the user turned it off. */ +const val DEFAULT_SOUND_ENABLED: Boolean = true + +/** + * What the filled composer button does while a run is live. + * + * `null` means Stop (nothing is waiting), otherwise the draft is submitted + * exactly like Enter, through `omp-web:submit-during-run`. The web can leave + * Steer to Enter because a desktop keyboard submits with it; an Android phone's + * return key only inserts a newline, so the button is the only submit a phone + * user has and must honour the same choice — otherwise "Steer the current run" + * would silently mean "always queue" on the device. + */ +fun streamingSubmitBehavior( + hasContent: Boolean, + behavior: SubmitDuringRunBehavior, +): SubmitDuringRunBehavior? = if (hasContent) behavior else null + +/** One `!`/`!!` shell invocation typed into the composer. */ +data class BashInvocation(val command: String, val excludeFromContext: Boolean) + +/** + * `!command` runs in omp with its output in the context; `!!command` runs it + * excluded from the context (web `bashExcluded`). Attachments never ride + * along, exactly like `ChatInput`. + */ +fun parseBashInvocation(text: String, hasAttachments: Boolean): BashInvocation? { + if (hasAttachments) return null + val trimmed = text.trim() + if (!trimmed.startsWith("!")) return null + val excludeFromContext = trimmed.startsWith("!!") + val command = (if (excludeFromContext) trimmed.drop(2) else trimmed.drop(1)).trim() + if (command.isEmpty()) return null + return BashInvocation(command, excludeFromContext) +} + +/** A text edit at a caret, with the caret position that follows it. */ +data class TextInsertion(val text: String, val caret: Int) + +fun insertAtCursor(text: String, caret: Int, insert: String): TextInsertion { + val position = caret.coerceIn(0, text.length) + val next = text.substring(0, position) + insert + text.substring(position) + return TextInsertion(next, position + insert.length) +} + +/** + * Up-arrow prompt history: newest first, de-duplicated, at most [limit] + * entries, returned oldest → newest (web `ChatWindow` `inputHistory`). + */ +fun inputHistory(userTexts: List, limit: Int = 50): List { + if (userTexts.isEmpty()) return emptyList() + val seen = HashSet() + val history = ArrayList(minOf(userTexts.size, limit)) + for (index in userTexts.indices.reversed()) { + val text = userTexts[index].trim() + if (text.isEmpty() || !seen.add(text)) continue + history += text + if (history.size >= limit) break + } + return history.asReversed() +} + +/** The active `/goal` of one session (`sessionStorage omp-web:goal:`). */ +data class ActiveGoal(val objective: String, val startedAt: Long) + +fun createActiveGoal(objective: String, startedAt: Long): ActiveGoal = + ActiveGoal(objective.trim(), startedAt) + +private val goalJson = Json { ignoreUnknownKeys = true } + +/** Parse a stored goal safely: user data and old versions never break chat. */ +fun parseActiveGoal(raw: String?): ActiveGoal? { + if (raw.isNullOrBlank()) return null + return try { + val root = goalJson.parseToJsonElement(raw) as? JsonObject ?: return null + val objective = (root["objective"] as? JsonPrimitive)?.contentOrNull ?: return null + val startedAt = (root["startedAt"] as? JsonPrimitive)?.longOrNull ?: return null + if (objective.isBlank() || startedAt < 0) return null + ActiveGoal(objective, startedAt) + } catch (_: Exception) { + null + } +} + +fun encodeActiveGoal(goal: ActiveGoal): String = buildString { + append('{') + append("\"objective\":") + append(JsonPrimitive(goal.objective)) + append(",\"startedAt\":") + append(goal.startedAt) + append('}') +} + +fun formatGoalElapsed(elapsedMs: Long): String { + val minutes = (elapsedMs.coerceAtLeast(0) / 60_000) + val hours = minutes / 60 + val rest = minutes % 60 + return if (hours > 0) "${hours}h ${rest}m" else "${rest}m" +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ComposerRouting.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ComposerRouting.kt new file mode 100644 index 000000000..1eb526e22 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ComposerRouting.kt @@ -0,0 +1,58 @@ +package io.github.kahme247.ompweb.domain + +/** + * Where a composer submission goes. The chat and new-session composers share + * these rules (web `handleSend` / `sendQueued`) so a `!cmd`, a web slash + * command, a client built-in and a plain prompt behave identically on both + * surfaces. + */ +sealed interface ComposerRoute { + /** `!cmd` / `!!cmd`, only valid without attachments. */ + data class Bash(val invocation: BashInvocation) : ComposerRoute + + /** A web slash command expanded into a real instruction. */ + data class WebPrompt( + val name: String, + val args: String, + val prompt: String, + ) : ComposerRoute + + /** A web command whose required argument is missing; the text stays. */ + data class Usage(val command: String, val argumentHint: String) : ComposerRoute + + /** A client-owned action command (`/compact`, `/reload`, …). */ + data class Builtin(val name: String, val args: String) : ComposerRoute + + /** Everything else: send [prompt] (text attachments already inlined). */ + data class Prompt(val prompt: String) : ComposerRoute +} + +/** + * Classify [raw] before routing. [prompt] is the resolved prompt (attachments + * inlined); [bash] is the parsed shell invocation when the composer accepted + * one. Attachments disable both shell mode and slash routing, exactly like the + * web composer. + */ +fun routeComposerSubmission( + raw: String, + prompt: String, + hasAttachments: Boolean, + hasImages: Boolean, + bash: BashInvocation?, +): ComposerRoute { + if (bash != null) return ComposerRoute.Bash(bash) + if (hasAttachments || hasImages || !raw.startsWith("/")) return ComposerRoute.Prompt(prompt) + val match = Regex("^/(\\S+)(?:\\s+([\\s\\S]*))?$").find(raw) ?: return ComposerRoute.Prompt(prompt) + val name = match.groupValues[1] + val args = match.groupValues[2].trim() + if (getWebSlashCommand(name) != null) { + return when (val expansion = expandWebSlashCommand(raw)) { + is WebSlashExpansion.Expand -> ComposerRoute.WebPrompt(name, args, expansion.prompt) + is WebSlashExpansion.UsageError -> + ComposerRoute.Usage(expansion.command, expansion.argumentHint) + WebSlashExpansion.NotWeb -> ComposerRoute.Prompt(prompt) + } + } + if (isClientBuiltinCommand(name)) return ComposerRoute.Builtin(name, args) + return ComposerRoute.Prompt(prompt) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ContextUsage.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ContextUsage.kt new file mode 100644 index 000000000..8f8fe281b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ContextUsage.kt @@ -0,0 +1,231 @@ +package io.github.kahme247.ompweb.domain + +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Locale +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull + +/** + * Context/usage arithmetic and formatting for the composer ring and its detail + * sheet, ported from `components/ChatInput.tsx` (ring, ring title, usage-limit + * label, compact result) and `components/ComposerPanels.tsx` + * (`ContextDetailPanel`), plus `lib/format.ts`. + */ + +/** omp's context totals (`contextUsage`): a single total, no per-category split. */ +data class ContextUsageInfo( + val tokens: Int?, + val contextWindow: Int, + val percent: Double?, +) + +/** A catalog row's capacity, shown next to the window in the detail sheet. */ +data class ModelCapacity(val contextWindow: Int?, val maxTokens: Int?) + +/** Ring tone thresholds: warning past 70 %, error past 90 %. */ +enum class ContextTone { NORMAL, WARNING, ERROR } + +fun contextTone(percent: Double?): ContextTone = when { + percent == null -> ContextTone.NORMAL + percent > 90 -> ContextTone.ERROR + percent > 70 -> ContextTone.WARNING + else -> ContextTone.NORMAL +} + +/** `formatPercent`: one stable decimal, `.` as the separator like the web. */ +fun formatPercent(percent: Double): String = String.format(Locale.US, "%.1f%%", percent) + +/** `formatCompactNumber`: `1.2M`, `34k`, `999`. */ +fun formatTokenCount(tokens: Long): String = when { + tokens >= 1_000_000 -> String.format(Locale.US, "%.1fM", tokens / 1_000_000.0) + tokens >= 1_000 -> "${Math.round(tokens / 1_000.0)}k" + else -> tokens.toString() +} + +fun formatTokenCount(tokens: Int): String = formatTokenCount(tokens.toLong()) + +/** The same compaction for a raw count (tool headers, context rows). */ +fun formatCompactNumber(value: Double): String = + if (value.isFinite()) formatTokenCount(Math.round(value)) else "0" + +/** Token count as the chips show it (`2.2k`, `999k`); null when not meaningful. */ +fun formatTokens(tokens: Long?): String? { + if (tokens == null || tokens <= 0) return null + if (tokens >= 1000) { + // 0 decimals from 10k up, one below; JS toFixed rounds half away from zero. + val fixed = if (tokens >= 10_000) { + Math.round(tokens / 1000.0).toString() + } else { + formatDecimal(tokens / 1000.0) + } + return fixed + "k" + } + return tokens.toString() +} + +fun formatTokens(tokens: Int?): String? = formatTokens(tokens?.toLong()) + +/** `42s` / `7m`; null below a second or when absent. */ +fun formatDuration(ms: Long?): String? { + if (ms == null || ms <= 0 || ms < 1_000) return null + if (ms < 60_000) return "${ms / 1000}s" + return "${Math.round(ms / 60_000.0)}m" +} + +private val THINKING_SUFFIX = Regex(":(off|minimal|low|medium|high|xhigh|max)$") + +/** Last path segment of a `provider/model:thinking` string. */ +fun shortModel(model: String?): String? { + if (model.isNullOrEmpty()) return null + val id = model.substringAfterLast('/') + return id.replace(THINKING_SUFFIX, "").ifEmpty { null } +} + +/** `provider/model` of a `provider/model:thinking` string. */ +fun fullModel(model: String?): String? { + if (model.isNullOrEmpty()) return null + return model.replace(THINKING_SUFFIX, "").ifEmpty { null } +} + +/** Thinking level suffix of a `provider/model:thinking` string. */ +fun modelEffort(model: String?): String? { + if (model == null) return null + return THINKING_SUFFIX.find(model)?.groupValues?.getOrNull(1) +} + +/** `formatCost`: `$0.0123` under a cent, `$1.20` above it; null when there is nothing to show. */ +fun formatCost(cost: Double?): String? { + if (cost == null || !cost.isFinite() || cost <= 0.0) return null + return String.format(Locale.US, if (cost < 0.01) "$%.4f" else "$%.2f", cost) +} + +/** Cache hit rate: `cacheRead / (input + cacheRead) * 100`, null when the denominator is 0. */ +fun cacheHitRate(input: Int, cacheRead: Int): Double? { + val denominator = input.toLong() + cacheRead + return if (denominator > 0) cacheRead.toDouble() / denominator * 100.0 else null +} + +/** omp's allowance percentage as the badge prints it (`42`, `42.5`). */ +fun formatAllowancePercent(percent: Double): String = + if (percent % 1.0 == 0.0) percent.toInt().toString() else String.format(Locale.US, "%.1f", percent) + +/** + * omp's structured provider usage-limit state (`WebSessionState.usageLimit`): + * `low_priority` while the provider serves below normal priority, `wrap_up` + * once the limit is reached. + */ +data class UsageLimit( + val stage: Stage, + val resetsAtSec: Long? = null, + val allowanceLeftPercent: Double? = null, + val extraUsage: Boolean = false, +) { + enum class Stage { LOW_PRIORITY, WRAP_UP } +} + +/** The label variants of `formatUsageLimitLabel`, with the absolute reset instant instead of text. */ +sealed interface UsageLimitLabel { + data class LowPriority(val resetAtMillis: Long?, val allowancePercent: Double?) : UsageLimitLabel + + data class WrapUp(val resetAtMillis: Long?, val extraUsage: Boolean) : UsageLimitLabel +} + +/** + * A reset time we can actually put on screen, or null when omp gave us none. + * omp may omit the field or send a non-number, and a reset already in the past + * would render a plausible but wrong clock — both are worse than saying no time + * at all, because the user would plan around them. + */ +fun usableSlowModeResetTime(resetsAtSec: Long?, nowMillis: Long): Long? { + if (resetsAtSec == null) return null + val resetAtMillis = resetsAtSec * 1000 + return if (resetAtMillis <= nowMillis) null else resetAtMillis +} + +private const val SLOW_MODE_SAME_DAY_MILLIS: Long = 20 * 3_600_000L + +/** + * `formatSlowModeResetClock`: today's clock time, or weekday + time when the + * reset is more than 20 hours away. + */ +fun formatSlowModeResetClock( + resetsAtMillis: Long, + nowMillis: Long, + zone: ZoneId = ZoneId.systemDefault(), + locale: Locale = Locale.getDefault(), +): String { + val pattern = if (resetsAtMillis - nowMillis > SLOW_MODE_SAME_DAY_MILLIS) "EEE HH:mm" else "HH:mm" + val formatter = DateTimeFormatter.ofPattern(pattern, locale).withZone(zone) + return formatter.format(Instant.ofEpochMilli(resetsAtMillis)) +} + +fun usageLimitLabel(limit: UsageLimit, nowMillis: Long): UsageLimitLabel { + val resetAtMillis = usableSlowModeResetTime(limit.resetsAtSec, nowMillis) + return when (limit.stage) { + UsageLimit.Stage.LOW_PRIORITY -> + UsageLimitLabel.LowPriority(resetAtMillis, limit.allowanceLeftPercent) + + UsageLimit.Stage.WRAP_UP -> + UsageLimitLabel.WrapUp(resetAtMillis, limit.extraUsage) + } +} + +/** One compacted turn (`CompactResultInfo`), from a `compact` answer or `auto_compaction_end`. */ +data class CompactResult( + val reason: String, + val tokensBefore: Int, + val estimatedTokensAfter: Int, +) { + /** Tokens the compaction saved (never negative when the estimate grows). */ + val savedTokens: Int get() = maxOf(0, tokensBefore - estimatedTokensAfter) +} + +/** + * `readCompactResult`: the server estimates `estimatedTokensAfter` from the + * summary when omp's `CompactionResult` omits it; nothing usable without + * `tokensBefore`. + */ +fun readCompactResult(element: JsonElement?, reason: String): CompactResult? { + val root = element as? JsonObject ?: return null + val before = (root["tokensBefore"] as? JsonPrimitive)?.intOrNull ?: return null + val after = (root["estimatedTokensAfter"] as? JsonPrimitive)?.intOrNull ?: 0 + return CompactResult(reason = reason, tokensBefore = before, estimatedTokensAfter = after) +} + +/** `SessionStatsInfo` token totals. */ +data class SessionTokenTotals( + val input: Int = 0, + val output: Int = 0, + val cacheRead: Int = 0, + val cacheWrite: Int = 0, + val total: Int = 0, +) + +/** `SessionStatsInfo` as the context sheet renders it (`get_session_stats`). */ +data class SessionStats( + val sessionId: String = "", + val sessionFile: String? = null, + val sessionName: String? = null, + val userMessages: Int = 0, + val assistantMessages: Int = 0, + val toolCalls: Int = 0, + val toolResults: Int = 0, + val totalMessages: Int = 0, + val tokens: SessionTokenTotals = SessionTokenTotals(), + val cost: Double = 0.0, + val contextUsage: ContextUsageInfo? = null, +) + +/** Parse `contextUsage` (`{tokens, contextWindow, percent}`); null when omp reports none. */ +fun parseContextUsage(element: JsonElement?): ContextUsageInfo? { + val root = element as? JsonObject ?: return null + val window = (root["contextWindow"] as? JsonPrimitive)?.intOrNull ?: return null + val tokens = (root["tokens"] as? JsonPrimitive)?.intOrNull + val percent = (root["percent"] as? JsonPrimitive)?.doubleOrNull + return ContextUsageInfo(tokens = tokens, contextWindow = window, percent = percent) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/Decimals.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Decimals.kt new file mode 100644 index 000000000..392da61ec --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Decimals.kt @@ -0,0 +1,16 @@ +package io.github.kahme247.ompweb.domain + +/** + * [value] rounded half away from zero to [decimals] places, without a trailing + * `.0` — the shape of the web's `toFixed(n).replace(/\.0$/, "")` in the file + * size and token chips. One implementation for both, so their rounding cannot + * drift apart. + */ +fun formatDecimal(value: Double, decimals: Int = 1): String { + if (decimals <= 0) return Math.round(value).toString() + var factor = 1.0 + repeat(decimals) { factor *= 10.0 } + val rounded = Math.round(value * factor) / factor + val text = rounded.toString() + return if (text.endsWith(".0")) text.dropLast(2) else text +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/Dictation.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Dictation.kt new file mode 100644 index 000000000..c8fc87853 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Dictation.kt @@ -0,0 +1,671 @@ +package io.github.kahme247.ompweb.domain + +import java.io.File +import java.util.Locale + +/** Where a dictation capture is transcribed: this phone or the configured server. */ +enum class DictationEngine(val wire: String) { + SERVER("server"), + ON_DEVICE("on_device"), +} + +/** The user's "Dictation engine" choice (`auto` prefers the server). */ +enum class DictationEnginePreference(val wire: String) { + /** Server STT when the app is connected to one, otherwise this phone. */ + AUTO("auto"), + ON_DEVICE("on_device"), + SERVER("server"), + ; + + companion object { + fun fromWire(value: String?): DictationEnginePreference = + entries.firstOrNull { it.wire == value } ?: AUTO + } +} + +/** + * Resolves the preference against the installation. "Configured server" means + * the app has a server origin; [serverSttUnconfigured] is the server's own 501 + * answer to an earlier upload ("STT not configured"), which makes Auto use the + * phone from then on instead of failing the same way every time. The STT + * provider itself is server-side configuration this client cannot see. + * + * [onDeviceAvailable] is the phone's installed on-device speech model (not a + * cloud recognizer). Auto uses the phone only while a model is installed; with + * a server it stays on the server path, and with neither it fails with the + * no-model error. An explicit [DictationEnginePreference.ON_DEVICE] is kept and + * fails the same way instead of silently uploading audio. + */ +fun resolveDictationEngine( + preference: DictationEnginePreference, + serverConfigured: Boolean, + serverSttUnconfigured: Boolean = false, + onDeviceAvailable: Boolean = true, +): DictationEngine = when (preference) { + DictationEnginePreference.SERVER -> DictationEngine.SERVER + DictationEnginePreference.ON_DEVICE -> DictationEngine.ON_DEVICE + DictationEnginePreference.AUTO -> when { + serverConfigured && !serverSttUnconfigured -> DictationEngine.SERVER + onDeviceAvailable -> DictationEngine.ON_DEVICE + // No local model: the server is the only engine left (it reports its + // own failure); without a server the on-device start reports the + // missing model instead of reaching for a cloud recognizer. + serverConfigured -> DictationEngine.SERVER + else -> DictationEngine.ON_DEVICE + } +} + +/** Languages offered in Settings → Dictation language (BCP-47 tags). */ +val DICTATION_LANGUAGE_TAGS: List = listOf( + "en-US", "en-GB", "de-DE", "fr-FR", "es-ES", "it-IT", "pt-BR", "nl-NL", + "pl-PL", "ru-RU", "tr-TR", "uk-UA", "cs-CZ", "sv-SE", "da-DK", "nb-NO", + "fi-FI", "el-GR", "hu-HU", "ro-RO", "hi-IN", "id-ID", "ja-JP", "ko-KR", + "zh-CN", "zh-TW", "th-TH", "vi-VN", +) + +private val LANGUAGE_TAG = Regex("^[a-z]{2,3}(-[A-Za-z]{4})?(-([A-Z]{2}|[0-9]{3}))?$") + +/** + * The recognition language: the picked tag when it is a well-formed BCP-47 + * language tag, otherwise the device locale. A blank country stays + * language-only (`de`), never `de-`. + */ +fun dictationLanguageTag(preferred: String?, device: Locale): String { + val candidate = preferred?.trim()?.takeIf { it.isNotEmpty() && LANGUAGE_TAG.matches(it) } + if (candidate != null) return candidate + val language = device.language.ifBlank { "en" } + return if (device.country.isBlank()) language else "$language-${device.country.uppercase(Locale.ROOT)}" +} + +/** Display name of a language tag in the user's locale ("German (Germany)"). */ +fun dictationLanguageLabel(tag: String, display: Locale = Locale.getDefault()): String { + val name = Locale.forLanguageTag(tag).getDisplayName(display) + return name.ifBlank { tag } +} + +/** The offered tags with the device language first when the list lacks it. */ +fun offeredDictationLanguages( + device: Locale, + offered: List = DICTATION_LANGUAGE_TAGS, +): List { + val deviceTag = dictationLanguageTag(null, device) + return if (offered.any { it.equals(deviceTag, ignoreCase = true) }) offered else listOf(deviceTag) + offered +} + +/** + * The live on-device transcript as edits on the composer field. + * + * The first partial anchors at the caret, later partials replace the visible + * live text instead of appending to it, and the final transcript replaces the + * partial. The field stays editable while listening: when the user changed it + * under the live text, the buffer keeps what is visible and continues at the + * new caret. Pure and anchored on text/caret reads, so the composer owns the + * actual field. + */ +class LiveDictationBuffer { + + private var base: String? = null + private var anchor = 0 + private var live = "" + /** The text this buffer produced last, to notice a user edit. */ + private var applied: String? = null + + val active: Boolean get() = base != null + + /** The live text as last accepted (already trimmed). */ + val text: String get() = live + + /** A new partial from the recognizer; null when the field is unchanged. */ + fun onPartial(current: String, caret: Int, partial: String): TextInsertion? { + val next = partial.trim() + if (base == null) { + base = current + anchor = caret.coerceIn(0, current.length) + live = "" + } else if (current != applied) { + // The user edited the field while listening: the visible text is + // theirs, so it becomes the new base and the caret the new anchor. + base = current + anchor = caret.coerceIn(0, current.length) + live = "" + } + if (next == live) return null + live = next + val insertion = render(base.orEmpty(), anchor, live) ?: return null + applied = insertion.text + return insertion + } + + /** The final transcript; null when it is blank and nothing was shown. */ + fun onFinal(current: String, caret: Int, final: String): TextInsertion? { + val start = base + val text = final.trim() + val insertion = if (start == null) { + if (text.isEmpty()) null else render(current, caret.coerceIn(0, current.length), text) + } else { + if (current != applied) { + base = current + anchor = caret.coerceIn(0, current.length) + live = "" + } + val shown = if (text.isEmpty()) live else text + if (shown.isEmpty()) null else render(base.orEmpty(), anchor, shown) + } + reset() + return insertion + } + + /** Removes the live text again; null when the user already changed it. */ + fun onCancel(current: String): TextInsertion? { + val start = base + val restored = if (start != null && current == applied) TextInsertion(start, anchor) else null + reset() + return restored + } + + fun reset() { + base = null + anchor = 0 + live = "" + applied = null + } + + private fun render(base: String, anchor: Int, text: String): TextInsertion? { + val at = anchor.coerceIn(0, base.length) + val separator = if (text.isNotEmpty() && at > 0 && !base[at - 1].isWhitespace()) " " else "" + val rendered = base.substring(0, at) + separator + text + base.substring(at) + if (rendered == base && text.isEmpty()) return null + return TextInsertion(rendered, at + separator.length + text.length) + } +} + +/** + * What the composer does with a transcript once the server job finishes. + * Mirrors `SttAfter` in `lib/stt.ts`: it travels with the recording, is kept + * on the server job, and comes back with the owner's claim — so the choice + * survives the composer being rebuilt (session switch, process death). + */ +enum class SttAfter(val wire: String?) { + Send("send"), + Steer("steer"), + FollowUp("followup"), + + /** Record and insert the text into the draft; the field sends it. */ + Insert(null), +} + +/** Job lifecycle as `lib/stt-jobs.ts` reports it. */ +enum class SttJobStatus(val wire: String) { + Pending("pending"), + Done("done"), + Error("error"), + + /** Another composer claimed or discarded the job; stand down silently. */ + Gone("gone"), +} + +/** What a poll of `GET /api/stt/{jobId}` sees (never the transcript). */ +data class SttJob(val id: String, val status: SttJobStatus, val error: String?) + +/** What `DELETE /api/stt/{jobId}?claim=…` answers; [text] only for the claimer. */ +data class SttClaim( + val id: String, + val status: SttJobStatus, + val error: String?, + val text: String?, + val after: SttAfter?, +) + +/** The recording this app keeps so a lost server job can be re-uploaded. */ +data class LocalRecording(val file: File, val mimeType: String, val after: SttAfter) + +/** + * Persistence the dictation flow needs, separated from Android so the + * ownership/claim logic stays JVM-testable. + */ +interface DictationStore { + + /** + * This installation's job-owner token (web: `omp-web:stt-owner-token` in + * sessionStorage). It owns the jobs this app uploads and retries, gets the + * first claim and alone gets the send/queue choice back. A claim token is + * per view-model instance and never equals this one. + */ + fun ownerToken(): String + + /** A fresh file in the app cache for one recording. */ + fun scratchFile(name: String): File + + /** Keeps [file] as the local copy for [scope] (re-upload after a server restart). */ + fun keepRecording(scope: String, file: File, mimeType: String, after: SttAfter) + + /** The kept recording of [scope], if it still exists. */ + fun savedRecording(scope: String): LocalRecording? + + fun clearRecording() +} + +/** Everything the composer needs to render the dictation surface. */ +sealed interface DictationState { + data object Idle : DictationState + + data class Recording( + val elapsedMs: Long, + val level: Float, + val paused: Boolean, + ) : DictationState + + /** + * On-device recognition is listening: the composer field stays visible and + * receives the transcript at its caret as it is spoken; the strip below it + * offers stop/cancel. Stops on silence (the recognizer ends the utterance) + * or tap. + */ + data class Live(val elapsedMs: Long) : DictationState + + data class Uploading(val elapsedMs: Long) : DictationState + + data class Transcribing(val elapsedMs: Long) : DictationState + + data class Failed(val message: String, val canRetry: Boolean) : DictationState + + /** The job was claimed or discarded elsewhere; the composer stands down. */ + data object Gone : DictationState +} + +/** + * One finished transcript. [isOwner] is true when this composer uploaded (or + * retried) the job: only then may the composer act on [after]; another + * device's transcript is inserted as text only. + */ +data class DictationResult(val text: String, val after: SttAfter, val isOwner: Boolean) + +/** What a delivered transcript does with the composer (web `onTranscript`). */ +sealed interface DictationOutcome { + val text: String + + /** Put the transcript into the field; the user sends it. */ + data class Insert(override val text: String) : DictationOutcome + + data class Send(override val text: String) : DictationOutcome + + data class Steer(override val text: String) : DictationOutcome + + data class FollowUp(override val text: String) : DictationOutcome +} + +/** + * Append a transcript to the draft with one separating space, unless the + * draft is empty or already ends in whitespace (web `onTranscript`). + */ +fun mergeTranscript(draft: String, transcript: String): String = + if (draft.isNotEmpty() && !draft.endsWith(" ")) "$draft $transcript" else draft + transcript + +/** + * Classifies a finished transcript like the web composer: another composer's + * job and an explicit `Insert` only fill the field. Without a session (a new + * chat / the new-session pane) `Send`, `Steer` and `FollowUp` all become a + * normal send — there is no run to steer and nothing to queue behind. + */ +fun dictationOutcome( + result: DictationResult, + draft: String, + hasSession: Boolean, +): DictationOutcome { + if (!result.isOwner || result.after == SttAfter.Insert) return DictationOutcome.Insert(result.text) + val text = mergeTranscript(draft, result.text) + if (!hasSession) return DictationOutcome.Send(text) + return when (result.after) { + SttAfter.Send -> DictationOutcome.Send(text) + SttAfter.Steer -> DictationOutcome.Steer(text) + SttAfter.FollowUp -> DictationOutcome.FollowUp(text) + SttAfter.Insert -> DictationOutcome.Insert(result.text) + } +} + +/** Playback of a recording (local or the server's kept copy). */ +sealed interface DictationPlayback { + data object Idle : DictationPlayback + + data object Preparing : DictationPlayback + + data class Playing(val positionMs: Long, val durationMs: Long, val paused: Boolean = false) : DictationPlayback +} + +enum class DictationPhase { Idle, Recording, Uploading, Transcribing, Failed, Gone } + +/** One decision of the poll/claim loop (see [DictationMachine]). */ +sealed interface DictationStep { + /** Keep waiting (poll again, or claim again). */ + data object Wait : DictationStep + + /** The job is done: send the one claim that delivers the text. */ + data object Claim : DictationStep + + data class Delivered(val text: String, val after: SttAfter, val isOwner: Boolean) : DictationStep + + data class Failed(val message: String) : DictationStep + + /** Claimed or discarded elsewhere: stop following without an error. */ + data object StandDown : DictationStep +} + +/** + * The dictation ownership/claim/retry/timeout rules of `hooks/useDictation.ts`, + * as a pure state machine. + * + * It owns no I/O: the view model feeds recorder events, HTTP answers and the + * wall clock in, and acts on the [DictationStep] it gets back. The rules it + * keeps are the ones the server enforces (`lib/stt-jobs.ts`): a claim on an + * unfinished job changes nothing, a finished job answers the first claim token + * (repeatable with the same token), only the owner gets the `after` choice, + * "gone" means another composer won, a 404 is only this browser's failure when + * it recorded the job, and the follow loop gives up after six minutes. + */ +class DictationMachine(private val clock: () -> Long) { + + var phase: DictationPhase = DictationPhase.Idle + private set + + var after: SttAfter = SttAfter.Insert + private set + + var jobId: String? = null + private set + + /** True when this composer uploaded or retried [jobId] (first claim, `after`). */ + var mine: Boolean = true + private set + + var message: String? = null + private set + + var canRetry: Boolean = false + private set + + var hasLocalAudio: Boolean = false + private set + + var paused: Boolean = false + private set + + /** Length of the finished recording; shown while uploading/transcribing. */ + var finalElapsedMs: Long = 0L + private set + + private var startedAt = 0L + private var pausedAt: Long? = null + private var pausedTotal = 0L + private var deadline = 0L + private var claiming = false + + /** True while the last claim answered "done" without text (owner grace). */ + val awaitingClaim: Boolean get() = claiming + + fun reset() { + phase = DictationPhase.Idle + after = SttAfter.Insert + jobId = null + mine = true + message = null + canRetry = false + hasLocalAudio = false + paused = false + finalElapsedMs = 0L + startedAt = 0L + pausedAt = null + pausedTotal = 0L + deadline = 0L + claiming = false + } + + fun setLocalAudio(present: Boolean) { + hasLocalAudio = present + canRetry = present || jobId != null + } + + /** A new recording replaces whatever the deck showed. */ + fun start(after: SttAfter): Boolean { + if (phase == DictationPhase.Recording || phase == DictationPhase.Uploading || phase == DictationPhase.Transcribing) { + return false + } + reset() + this.after = after + phase = DictationPhase.Recording + startedAt = clock() + return true + } + + fun pause(): Boolean { + if (phase != DictationPhase.Recording || paused) return false + paused = true + pausedAt = clock() + return true + } + + fun resume(): Boolean { + if (phase != DictationPhase.Recording || !paused) return false + pausedTotal += clock() - (pausedAt ?: clock()) + pausedAt = null + paused = false + return true + } + + /** Recording time without the paused stretches, capped at the 5-minute limit. */ + fun elapsedMs(): Long { + if (phase != DictationPhase.Recording) return finalElapsedMs + val end = if (paused) (pausedAt ?: clock()) else clock() + return (end - startedAt - pausedTotal).coerceIn(0L, MAX_RECORDING_MS) + } + + /** + * The recorder answered or was stopped: the recording now uploads. + * [after] lets the composer's Send/Queue button decide the intent at stop + * time (web `stopAndFinishDictation`); null keeps the intent chosen at + * [start]. + */ + fun finishRecording(after: SttAfter? = null) { + if (phase != DictationPhase.Recording) return + if (after != null) this.after = after + finalElapsedMs = elapsedMs() + paused = false + pausedAt = null + phase = DictationPhase.Uploading + } + + fun uploadAccepted(id: String) { + phase = DictationPhase.Transcribing + jobId = id + mine = true + message = null + canRetry = false + claiming = false + deadline = clock() + STT_TIMEOUT_MS + } + + /** A recording that produced no uploadable audio (nothing was said, mic denied). */ + fun fail(message: String) { + phase = DictationPhase.Failed + this.message = message + claiming = false + canRetry = hasLocalAudio || jobId != null + } + + fun beginUploadRetry(): Boolean { + if (!hasLocalAudio) return false + phase = DictationPhase.Uploading + message = null + canRetry = false + claiming = false + deadline = 0L + return true + } + + /** Jobs of this scope a browser may pick up (nothing of its own is running). */ + fun canAdopt(): Boolean = phase == DictationPhase.Idle || phase == DictationPhase.Gone + + /** + * Adopts the newest live job of the scope. A failed job is shown with its + * error so this composer can play, retry or discard the server's copy. + */ + fun adopt(jobs: List): Boolean { + if (!canAdopt()) return false + val job = jobs.firstOrNull { it.status != SttJobStatus.Gone } ?: return false + jobId = job.id + mine = false + hasLocalAudio = false + finalElapsedMs = 0L + after = SttAfter.Insert + claiming = false + if (job.status == SttJobStatus.Error) { + phase = DictationPhase.Failed + message = job.error ?: TRANSCRIPTION_FAILED + canRetry = true + } else { + phase = DictationPhase.Transcribing + message = null + canRetry = false + deadline = clock() + STT_TIMEOUT_MS + } + return true + } + + /** A failed job the server still holds audio for: retry it server-side. */ + fun serverRetryJobId(): String? = jobId + + fun retryAccepted(job: SttJob): DictationStep? = when (job.status) { + SttJobStatus.Gone -> standDown() + SttJobStatus.Error -> failStep(job.error ?: TRANSCRIPTION_FAILED) + else -> { + phase = DictationPhase.Transcribing + jobId = job.id + // `retrySttJob` makes the retrying browser the job's owner, so a + // lost job is this composer's failure, not another's. + mine = true + message = null + canRetry = false + claiming = false + deadline = clock() + STT_TIMEOUT_MS + null + } + } + + /** + * The server no longer has the job (restart, expiry, pruned tombstone): + * only the local copy can be retried. + */ + fun retryJobMissing(): DictationStep { + jobId = null + canRetry = hasLocalAudio + return if (beginUploadRetry()) DictationStep.Wait else failStep(JOB_NOT_FOUND) + } + + /** A kept recording whose upload never finished (process death): offer a re-upload. */ + fun restoreRecording(after: SttAfter, message: String) { + reset() + this.after = after + hasLocalAudio = true + fail(message) + } + + fun retryRejected(message: String): DictationStep = failStep(message) + + /** `DELETE` without a claim: discard everywhere, including the upstream request. */ + fun discard() { + reset() + } + + /** Cancelled while recording. */ + fun cancelCapture() { + reset() + } + + /** `GET /api/stt/{jobId}` answer: `null` is a 404. */ + fun observe(job: SttJob?): DictationStep { + if (phase != DictationPhase.Transcribing) return DictationStep.Wait + if (expired()) return failStep(TIMED_OUT) + return when (job?.status) { + null -> { + jobId = null + if (mine) failStep(JOB_NOT_FOUND) else standDown() + } + SttJobStatus.Pending -> DictationStep.Wait + SttJobStatus.Error -> failStep(job.error ?: TRANSCRIPTION_FAILED) + SttJobStatus.Gone -> standDown() + SttJobStatus.Done -> if (claiming) DictationStep.Wait else { + claiming = true + DictationStep.Claim + } + } + } + + /** `DELETE ?claim=…` answer: `null` is a 404. */ + fun takeClaim(claim: SttClaim?): DictationStep { + if (phase != DictationPhase.Transcribing) return DictationStep.Wait + if (expired()) return failStep(TIMED_OUT) + if (claim == null) { + jobId = null + return if (mine) failStep(JOB_NOT_FOUND) else standDown() + } + return when (claim.status) { + SttJobStatus.Gone -> standDown() + SttJobStatus.Error -> failStep(claim.error ?: TRANSCRIPTION_FAILED) + SttJobStatus.Pending -> { + // A claim on an unfinished job is a no-op; poll again. + claiming = false + DictationStep.Wait + } + SttJobStatus.Done -> { + val text = claim.text?.trim() + if (text.isNullOrEmpty()) { + // "done" without text: the recording browser keeps the first + // claim for 15s; repeat the claim with the same token. + DictationStep.Wait + } else { + val owner = mine || claim.after != null + phase = DictationPhase.Idle + jobId = null + hasLocalAudio = false + message = null + canRetry = false + claiming = false + deadline = 0L + DictationStep.Delivered(text, claim.after ?: SttAfter.Insert, owner) + } + } + } + } + + private fun expired(): Boolean = deadline > 0 && clock() > deadline + + private fun failStep(message: String): DictationStep { + fail(message) + return DictationStep.Failed(message) + } + + private fun standDown(): DictationStep { + phase = DictationPhase.Gone + jobId = null + message = null + canRetry = false + hasLocalAudio = false + claiming = false + deadline = 0L + return DictationStep.StandDown + } + + companion object { + /** `MAX_RECORDING_MS` in `hooks/useDictation.ts`. */ + const val MAX_RECORDING_MS = 300_000L + + /** The server gives the STT endpoint ~5 min; the deck waits a bit longer. */ + const val STT_TIMEOUT_MS = 6 * 60_000L + const val STT_POLL_MS = 500L + const val STT_ADOPT_MS = 4_000L + + const val TRANSCRIPTION_FAILED = "Transcription failed" + const val TIMED_OUT = "Transcription timed out" + const val JOB_NOT_FOUND = "Transcription job not found" + const val NO_SPEECH = "No speech detected" + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ExtensionUi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ExtensionUi.kt new file mode 100644 index 000000000..e012c1bb0 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ExtensionUi.kt @@ -0,0 +1,665 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.put + +/** + * The blocking dialog part of omp's extension UI protocol + * (`extension_ui_request`, web `ExtensionDialog.tsx`). + */ +sealed interface ExtensionDialogState { + val id: String + + /** Absolute expiry in epoch milliseconds; null for a request without timeout. */ + val expiresAt: Long? + + data class Select( + override val id: String, + val title: String, + val options: List, + override val expiresAt: Long? = null, + ) : ExtensionDialogState + + data class Confirm( + override val id: String, + val title: String, + val message: String, + override val expiresAt: Long? = null, + ) : ExtensionDialogState + + data class Input( + override val id: String, + val title: String, + val placeholder: String?, + override val expiresAt: Long? = null, + ) : ExtensionDialogState + + data class Editor( + override val id: String, + val title: String, + val prefill: String, + /** omp sets this for plain-text answers ("Other"), rendered as prose. */ + val promptStyle: Boolean, + override val expiresAt: Long? = null, + ) : ExtensionDialogState + + data class Ask( + override val id: String, + val questions: List, + override val expiresAt: Long? = null, + ) : ExtensionDialogState +} + +/** One `ask` question (`RpcAskDialogQuestion`). */ +data class AskQuestion( + val id: String, + val question: String, + val header: String?, + val options: List, + val multi: Boolean, + /** Index into [options]; null when nothing is recommended. */ + val recommended: Int?, +) + +data class AskOption(val label: String, val description: String?, val preview: String?) + +/** Exactly one variant is answered, mirroring `ExtensionUiResponse`. */ +sealed interface ExtensionResponse { + val id: String + + data class Value(override val id: String, val value: String) : ExtensionResponse + + data class Confirmed(override val id: String, val confirmed: Boolean) : ExtensionResponse + + data class Cancelled(override val id: String) : ExtensionResponse + + data class Answers(override val id: String, val answers: List) : ExtensionResponse +} + +/** Per-question answer sent back in question order (`RpcAskDialogAnswer`). */ +data class AskAnswer(val id: String, val selectedOptions: List, val customInput: String?) + +data class ExtensionStatusItem(val key: String, val text: String) + +enum class ExtensionWidgetPlacement { AboveEditor, BelowEditor } + +data class ExtensionWidgetItem( + val key: String, + val lines: List, + val placement: ExtensionWidgetPlacement, +) + +/** The custom/terminal panel (`method: "custom"`), fed by `extension_ui_input`. */ +data class ExtensionTerminalState(val id: String, val lines: List) + +enum class ExtensionNoticeLevel { Info, Warning, Error } + +/** What one `extension_ui_request` frame asks of the UI. */ +sealed interface ExtensionUiFrame { + data class Dialog(val request: ExtensionDialogState) : ExtensionUiFrame + + /** `method: "cancel"` — closes only the dialog with [targetId]. */ + data class Cancel(val targetId: String) : ExtensionUiFrame + + data class Notify(val message: String, val level: ExtensionNoticeLevel) : ExtensionUiFrame + + data class OpenUrl(val url: String, val instructions: String?) : ExtensionUiFrame + + /** [text] null clears the extension's status entry. */ + data class Status(val key: String, val text: String?) : ExtensionUiFrame + + /** [lines] null removes the widget. */ + data class Widget( + val key: String, + val lines: List?, + val placement: ExtensionWidgetPlacement, + ) : ExtensionUiFrame + + data class EditorText(val text: String) : ExtensionUiFrame + + data class Terminal(val id: String, val lines: List) : ExtensionUiFrame + + data class TerminalClosed(val id: String) : ExtensionUiFrame + + /** A method this client deliberately ignores (`setTitle`: the browser is the only client with a title). */ + data object Ignored : ExtensionUiFrame +} + +/** One `host_uri_request` frame: the agent reading or writing a host resource. */ +data class HostUriRequest( + val id: String, + val write: Boolean, + val url: String, + /** Present for writes. */ + val content: String?, +) + +/** A `pi-web://clipboard` read waiting for the user's consent. */ +data class ClipboardPrompt(val sessionId: String) + +/** An `open_url` request the user has to confirm before the app hands it to the system. */ +data class ExtensionOpenUrlPrompt(val url: String, val instructions: String?) + +/** An extension notice with the level omp attached (`notify`), shown as a coloured bar. */ +data class ExtensionNotice(val message: String, val level: ExtensionNoticeLevel = ExtensionNoticeLevel.Info) + +/** + * Parse one `extension_ui_request` frame. Returns null for anything else so a + * caller can test the frame type first. Unknown methods degrade to + * [ExtensionUiFrame.Ignored], never to an error. + */ +fun parseExtensionUiFrame(frame: JsonObject, now: Long): ExtensionUiFrame? { + if (frame.str("type") != "extension_ui_request") return null + val method = frame.str("method") ?: return ExtensionUiFrame.Ignored + val id = frame.str("id").orEmpty() + val expiresAt = frame.expiresAt(now) + return when (method) { + "select" -> { + if (id.isEmpty()) return ExtensionUiFrame.Ignored + ExtensionUiFrame.Dialog( + ExtensionDialogState.Select( + id = id, + title = frame.str("title").orEmpty(), + options = frame.strings("options"), + expiresAt = expiresAt, + ), + ) + } + + "confirm" -> { + if (id.isEmpty()) return ExtensionUiFrame.Ignored + ExtensionUiFrame.Dialog( + ExtensionDialogState.Confirm( + id = id, + title = frame.str("title").orEmpty(), + message = frame.str("message").orEmpty(), + expiresAt = expiresAt, + ), + ) + } + + "input" -> { + if (id.isEmpty()) return ExtensionUiFrame.Ignored + ExtensionUiFrame.Dialog( + ExtensionDialogState.Input( + id = id, + title = frame.str("title").orEmpty(), + placeholder = frame.str("placeholder"), + expiresAt = expiresAt, + ), + ) + } + + "editor" -> { + if (id.isEmpty()) return ExtensionUiFrame.Ignored + ExtensionUiFrame.Dialog( + ExtensionDialogState.Editor( + id = id, + title = frame.str("title").orEmpty(), + prefill = frame.str("prefill").orEmpty(), + promptStyle = frame.bool("promptStyle") == true, + expiresAt = expiresAt, + ), + ) + } + + "ask" -> { + if (id.isEmpty()) return ExtensionUiFrame.Ignored + ExtensionUiFrame.Dialog( + ExtensionDialogState.Ask( + id = id, + questions = (frame.sub("questions") as? JsonArray) + ?.mapNotNull { (it as? JsonObject)?.let(::parseAskQuestion) } + .orEmpty(), + expiresAt = expiresAt, + ), + ) + } + + "cancel" -> { + val target = frame.str("targetId") + if (target.isNullOrEmpty()) ExtensionUiFrame.Ignored else ExtensionUiFrame.Cancel(target) + } + + "notify" -> ExtensionUiFrame.Notify( + message = frame.str("message").orEmpty(), + level = when (frame.str("notifyType")) { + "warning" -> ExtensionNoticeLevel.Warning + "error" -> ExtensionNoticeLevel.Error + else -> ExtensionNoticeLevel.Info + }, + ) + + "open_url" -> ExtensionUiFrame.OpenUrl( + // The wrapper allows the extension to hand a different launch URL. + url = frame.str("launchUrl") ?: frame.str("url").orEmpty(), + instructions = frame.str("instructions"), + ) + + "setStatus" -> { + val key = frame.str("statusKey") + if (key.isNullOrEmpty()) ExtensionUiFrame.Ignored else ExtensionUiFrame.Status(key, frame.str("statusText")) + } + + "setWidget" -> { + val key = frame.str("widgetKey") + if (key.isNullOrEmpty()) { + ExtensionUiFrame.Ignored + } else { + ExtensionUiFrame.Widget( + key = key, + lines = frame.sub("widgetLines")?.let { parseStringArray(it) }, + placement = if (frame.str("widgetPlacement") == "belowEditor") { + ExtensionWidgetPlacement.BelowEditor + } else { + ExtensionWidgetPlacement.AboveEditor + }, + ) + } + } + + "set_editor_text" -> ExtensionUiFrame.EditorText(frame.str("text").orEmpty()) + + "custom" -> { + if (id.isEmpty()) { + ExtensionUiFrame.Ignored + } else if (frame.bool("closed") == true) { + ExtensionUiFrame.TerminalClosed(id) + } else { + ExtensionUiFrame.Terminal(id, frame.strings("lines")) + } + } + + else -> ExtensionUiFrame.Ignored + } +} + +/** Parse one `host_uri_request` frame, or null for another frame type. */ +fun parseHostUriRequest(frame: JsonObject): HostUriRequest? { + if (frame.str("type") != "host_uri_request") return null + val id = frame.str("id") ?: return null + if (id.isEmpty()) return null + return HostUriRequest( + id = id, + write = frame.str("operation") == "write", + url = frame.str("url").orEmpty(), + content = frame.str("content"), + ) +} + +private fun parseAskQuestion(raw: JsonObject): AskQuestion? { + val id = raw.str("id") ?: return null + val options = (raw.sub("options") as? JsonArray) + ?.mapNotNull { (it as? JsonObject)?.let(::parseAskOption) } + .orEmpty() + return AskQuestion( + id = id, + question = raw.str("question").orEmpty(), + header = raw.str("header"), + options = options, + multi = raw.bool("multi") == true, + recommended = raw.num("recommended")?.toInt()?.takeIf { it in options.indices }, + ) +} + +private fun parseAskOption(raw: JsonObject): AskOption? { + val label = raw.str("label") ?: return null + return AskOption( + label = label, + description = raw.str("description"), + preview = raw.str("preview"), + ) +} + +private fun parseStringArray(element: JsonElement): List? = + (element as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.contentOrNull } + +private fun JsonObject.expiresAt(now: Long): Long? { + val absolute = num("expiresAt")?.toLong() + if (absolute != null && absolute > 0) return absolute + val timeout = num("timeout")?.toLong() + return if (timeout != null && timeout > 0) now + timeout else null +} + +private fun JsonObject.strings(key: String): List = sub(key)?.let(::parseStringArray).orEmpty() + +private fun JsonObject.sub(key: String): JsonElement? = this[key] + +private fun JsonObject.str(key: String): String? = (this[key] as? JsonPrimitive)?.contentOrNull + +private fun JsonObject.bool(key: String): Boolean? = (this[key] as? JsonPrimitive)?.booleanOrNull + +private fun JsonObject.num(key: String): Number? = (this[key] as? JsonPrimitive)?.let { primitive -> + primitive.longOrNull ?: primitive.intOrNull +} + +/** + * Dialog state of one session, including the replay bookkeeping the protocol + * requires: a reconnecting stream replays open requests with a fresh sequence, + * so the same id must never stack a second dialog and a request this tab + * already answered must never come back. + */ +class ExtensionUiState(private val answeredLimit: Int = ANSWERED_HISTORY_LIMIT) { + var dialog: ExtensionDialogState? = null + private set + + var statusItems: List = emptyList() + private set + + var widgets: List = emptyList() + private set + + var terminal: ExtensionTerminalState? = null + private set + + private val answered = ArrayDeque() + + /** A session switch drops every panel; answered ids stay (a late replay must not reopen). */ + fun bindSession() { + dialog = null + statusItems = emptyList() + widgets = emptyList() + terminal = null + } + + /** Forget every panel and every answered id: the signed-in user is being dropped. */ + fun reset() { + bindSession() + answered.clear() + } + + /** Show [request] unless it was answered already; a replay replaces the current state. */ + fun request(request: ExtensionDialogState): Boolean { + if (isAnswered(request.id)) return false + dialog = request + return true + } + + /** `method: "cancel"` clears only the matching dialog. */ + fun cancel(targetId: String): Boolean { + if (dialog?.id != targetId) return false + dialog = null + return true + } + + /** Forget [id] after this client answered it, so its replay is ignored. */ + fun markAnswered(id: String) { + answered.remove(id) + answered.addLast(id) + while (answered.size > answeredLimit) answered.removeFirst() + } + + private fun isAnswered(id: String): Boolean = id in answered + + /** + * Take [request] out of the UI because this client answers it now: the id + * counts as answered (so its reconnect replay cannot reopen the panel) and + * the panel is removed in the same step, before the RPC is sent. False when + * the request is not the shown dialog or was answered already. + */ + fun claimAnswer(request: ExtensionDialogState): Boolean { + if (isAnswered(request.id)) return false + if (dialog?.id != request.id) return false + markAnswered(request.id) + dialog = null + return true + } + + /** + * Put a claimed request back after its RPC failed, so the user can answer + * again. A request past its own [ExtensionDialogState.expiresAt] stays out: + * the server settled it already, a panel for it could only be answered into + * the void. The id of an unexpired request is never left burnt; a newer + * dialog keeps the screen. + */ + fun restore(request: ExtensionDialogState, now: Long): Boolean { + val at = request.expiresAt + if (at != null && at <= now) return false + answered.remove(request.id) + if (dialog != null) return false + dialog = request + return true + } + + /** Drop an expired dialog; true when this call removed it. */ + fun expire(now: Long): Boolean { + val current = dialog ?: return false + val at = current.expiresAt ?: return false + if (at > now) return false + markAnswered(current.id) + dialog = null + return true + } + + fun setStatus(key: String, text: String?) { + val rest = statusItems.filterNot { it.key == key } + statusItems = if (text.isNullOrEmpty()) rest else rest + ExtensionStatusItem(key, text) + } + + fun setWidget(key: String, lines: List?, placement: ExtensionWidgetPlacement) { + val rest = widgets.filterNot { it.key == key } + widgets = if (lines == null) { + rest + } else { + rest + ExtensionWidgetItem(key, lines, placement) + } + } + + fun setTerminal(id: String, lines: List) { + terminal = ExtensionTerminalState(id, lines) + } + + fun closeTerminal(id: String): Boolean { + if (terminal?.id != id) return false + terminal = null + return true + } + + private companion object { + /** Enough ids to cover a reconnect replay plus the last few answers. */ + const val ANSWERED_HISTORY_LIMIT = 64 + } +} + +// --------------------------------------------------------------------------- +// `ask` forms: the drafts live in the UI, the rules live here. +// --------------------------------------------------------------------------- + +data class AskDraft( + val selected: List = emptyList(), + /** The Other text stays for the question even while another option is chosen. */ + val other: String = "", + val otherChosen: Boolean = false, +) + +data class AskForm(val drafts: List) { + + fun draftAt(index: Int): AskDraft = drafts.getOrNull(index) ?: AskDraft() + + /** Multi-select may stay empty; single-select needs a choice or a typed Other. */ + fun canSubmit(questions: List): Boolean = questions.withIndex().all { (index, question) -> + val draft = draftAt(index) + question.multi || draft.selected.isNotEmpty() || (draft.otherChosen && draft.other.trim().isNotEmpty()) + } + + /** Option ids are labels, exactly as the web dialog sends them. */ + fun answers(questions: List): List = questions.mapIndexed { index, question -> + val draft = draftAt(index) + val custom = if (draft.otherChosen) draft.other.trim() else "" + AskAnswer( + id = question.id, + selectedOptions = question.options.map { it.label }.filter { it in draft.selected }, + customInput = custom.ifEmpty { null }, + ) + } + + fun toggleOption(questions: List, index: Int, label: String): AskForm { + val question = questions.getOrNull(index) ?: return this + return update(index) { current -> + if (question.multi) { + val selected = if (label in current.selected) { + current.selected - label + } else { + current.selected + label + } + current.copy(selected = selected) + } else { + current.copy(selected = listOf(label), otherChosen = false) + } + } + } + + fun chooseOther(questions: List, index: Int, chosen: Boolean): AskForm = + update(index) { current -> + val multi = questions.getOrNull(index)?.multi == true + current.copy( + selected = if (chosen && !multi) emptyList() else current.selected, + otherChosen = chosen, + ) + } + + /** + * Tapping back into kept Other text means answering with it again (web + * `chooseKeptOther`); it never invents a choice for empty text. + */ + fun focusOther(questions: List, index: Int): AskForm { + val current = draftAt(index) + if (current.otherChosen || current.other.trim().isEmpty()) return this + return chooseOther(questions, index, true) + } + + fun setOther(questions: List, index: Int, text: String): AskForm = + update(index) { current -> + if (text.trim().isEmpty()) { + current.copy(other = text, otherChosen = false) + } else { + val multi = questions.getOrNull(index)?.multi == true + current.copy( + other = text, + otherChosen = true, + selected = if (multi) current.selected else emptyList(), + ) + } + } + + private fun update(index: Int, transform: (AskDraft) -> AskDraft): AskForm { + if (index !in drafts.indices) return this + return AskForm(drafts.mapIndexed { at, draft -> if (at == index) transform(draft) else draft }) + } +} + +/** Fresh drafts for [questions]; single-select starts on its recommended option. */ +fun initialAskForm(questions: List): AskForm = AskForm( + questions.map { question -> + val recommended = question.recommended + ?.takeIf { !question.multi } + ?.let { question.options.getOrNull(it) } + AskDraft(selected = recommended?.let { listOf(it.label) }.orEmpty()) + }, +) + +/** `pi-web://clipboard` is the only host resource the app implements. */ +fun hostUriResource(url: String): String = url.replace(Regex("^pi-web://", RegexOption.IGNORE_CASE), "") + +// --------------------------------------------------------------------------- +// Terminal input (web `lib/terminal-input.ts`) +// --------------------------------------------------------------------------- + +/** + * The `extension_ui_input` data for one hardware key press in the custom + * terminal panel; null means the key produces no terminal input. [key] uses + * the DOM names (`Enter`, `ArrowUp`, `Backspace`, …) so the port stays + * comparable with the web table. + */ +fun terminalKeyData(key: String, ctrl: Boolean, alt: Boolean, shift: Boolean): String? { + // Ctrl+V stays a paste for the text field, exactly like the web terminal. + if (ctrl && !alt && key.equals("v", ignoreCase = true)) return null + if (ctrl && !alt) { + legacyCtrlSequence(key)?.let { return it } + } + if (alt && !ctrl) { + if (key == "Backspace") return "\u001b\u007f" + TERMINAL_ALT_ARROWS[key]?.let { return it } + if (key.length == 1) return "\u001b$key" + } + if (key == "Enter") return if (shift) "\n" else "\r" + if (key == "Tab") return if (shift) "\u001b[Z" else "\t" + return TERMINAL_SPECIAL_KEYS[key] +} + +private val TERMINAL_SPECIAL_KEYS = mapOf( + "ArrowUp" to "\u001b[A", + "ArrowDown" to "\u001b[B", + "ArrowRight" to "\u001b[C", + "ArrowLeft" to "\u001b[D", + "Home" to "\u001b[H", + "End" to "\u001b[F", + "Insert" to "\u001b[2~", + "Delete" to "\u001b[3~", + "PageUp" to "\u001b[5~", + "PageDown" to "\u001b[6~", + "Escape" to "\u001b", + "Backspace" to "\u007f", +) + +private val TERMINAL_ALT_ARROWS = mapOf( + "ArrowLeft" to "\u001bb", + "ArrowRight" to "\u001bf", + "ArrowUp" to "\u001bp", + "ArrowDown" to "\u001bn", +) + +private fun legacyCtrlSequence(key: String): String? { + if (key.length != 1) return null + val code = key.uppercase()[0].code + if (code in 64..95) return (code and 0x1f).toChar().toString() + return if (key == "?") "\u007f" else null +} + +// --------------------------------------------------------------------------- +// Response builders +// --------------------------------------------------------------------------- + +/** `extension_ui_response`: exactly one answer variant (web `respondToExtensionUi`). */ +fun extensionResponseCommand(response: ExtensionResponse): JsonObject = buildJsonObject { + put("type", "extension_ui_response") + put("id", response.id) + when (response) { + is ExtensionResponse.Value -> put("value", response.value) + is ExtensionResponse.Confirmed -> put("confirmed", response.confirmed) + is ExtensionResponse.Cancelled -> put("cancelled", true) + is ExtensionResponse.Answers -> put( + "answers", + buildJsonArray { + response.answers.forEach { answer -> + add( + buildJsonObject { + put("id", answer.id) + put( + "selectedOptions", + buildJsonArray { answer.selectedOptions.forEach { add(JsonPrimitive(it)) } }, + ) + answer.customInput?.let { put("customInput", it) } + }, + ) + } + }, + ) + } +} + +/** `extension_ui_input` for the custom/terminal panel. */ +fun extensionInputCommand(id: String, data: JsonElement): JsonObject = buildJsonObject { + put("type", "extension_ui_input") + put("id", id) + put("data", data) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/FileTree.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/FileTree.kt new file mode 100644 index 000000000..d6a5f25a3 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/FileTree.kt @@ -0,0 +1,256 @@ +package io.github.kahme247.ompweb.domain + +/** + * The explorer's tree, its git decorations and the path/mention helpers the + * file panel shares with the composer. + * + * Port of the client halves of `components/FileExplorer.tsx` and + * `lib/file-paths.ts`/`lib/file-fuzzy.ts`: lazy directory listings keyed by + * absolute path, the flattened visible rows, the directory "contains changes" + * dots, the ranked file-index rows, and the `@path` text the `@` button + * inserts. + */ + +/** One entry of `GET /api/files/{dir}?type=list` (dirs first, alphabetical). */ +data class FileEntry(val name: String, val isDir: Boolean, val size: Long = 0) + +/** One visible tree row; `empty` marks an expanded directory without entries. */ +data class FileTreeRow( + val name: String, + val path: String, + val isDir: Boolean, + val size: Long, + val depth: Int, + val expanded: Boolean, + val loading: Boolean, + val empty: Boolean = false, +) + +/** One ranked `/api/file-index` result, flattened for the search list. */ +data class FileSearchRow(val path: String, val name: String, val directory: String) + +private val WINDOWS_DRIVE = Regex("""^[a-zA-Z]:[\\/]""") +private val WINDOWS_DRIVE_SLASH = Regex("""^[a-zA-Z]:/""") + +/** Slashes are always `/` internally; Windows and UNC paths keep their prefix. */ +fun normalizeFilePath(filePath: String): String = + if (WINDOWS_DRIVE.containsMatchIn(filePath) || filePath.startsWith("\\\\")) { + filePath.replace('\\', '/') + } else { + filePath + } + +/** True for drive, UNC, POSIX-absolute and `~/` paths (the file API expands the last). */ +fun isAbsoluteFilePath(filePath: String): Boolean { + val path = normalizeFilePath(filePath) + return path.startsWith("/") || WINDOWS_DRIVE_SLASH.containsMatchIn(path) || path.startsWith("~/") +} + +/** + * Paths are compared the way the filesystem does: NTFS is case-insensitive, so + * a session's cwd and the server's realpath'd file may differ in case. + */ +fun comparableFilePath(filePath: String): String { + val normalized = normalizeFilePath(filePath).trimEnd('/') + return if (WINDOWS_DRIVE_SLASH.containsMatchIn(normalized) || normalized.startsWith("//")) { + normalized.lowercase() + } else { + normalized + } +} + +fun fileName(filePath: String): String = + normalizeFilePath(filePath).trimEnd('/').substringAfterLast('/') + +/** `getFileDirectory` (web): the parent, the drive/UNC root staying intact. */ +fun fileDirectory(filePath: String): String { + val normalized = normalizeFilePath(filePath).trimEnd('/') + val cut = normalized.lastIndexOf('/') + if (cut < 0) return "" + if (cut == 0) return "/" + val parent = normalized.substring(0, cut) + // `C:` alone is not a directory; `C:/` is. + return if (WINDOWS_DRIVE.matches(parent + "/")) "$parent/" else parent +} + +/** + * `joinFilePath` (web): exactly one separator, whatever the parent ended with. + * The root keeps its slash (`joinFilePath("/", "a") == "/a"`), and a bare + * `C:` becomes the drive root, so every path built here stays absolute. + */ +fun joinFilePath(parent: String, child: String): String { + val base = normalizeFilePath(parent).trimEnd('/') + val name = normalizeFilePath(child).trimStart('/') + return "$base/$name" +} + +/** `getRelativeFilePath` (web): the path below [cwd], the absolute path otherwise. */ +fun relativeFilePath(filePath: String, cwd: String?): String { + if (cwd.isNullOrBlank()) return filePath + val normalizedFile = normalizeFilePath(filePath) + val normalizedCwd = normalizeFilePath(cwd).trimEnd('/') + val fileKey = comparableFilePath(normalizedFile) + val cwdKey = comparableFilePath(normalizedCwd) + if (fileKey == cwdKey) return "" + if (!fileKey.startsWith("$cwdKey/")) return filePath + // Slice by the *original* casing: only the comparison is case-insensitive. + val normalizedFileTrimmed = normalizedFile.trimEnd('/') + return normalizedFileTrimmed.substring(normalizedCwd.length + 1) +} + +/** True when [filePath] is [directory] itself or lies below it. */ +fun isInsideDirectory(filePath: String, directory: String): Boolean { + val file = comparableFilePath(filePath) + val dir = comparableFilePath(directory) + return file == dir || file.startsWith("$dir/") +} + +/** + * Merges one directory listing into the tree. The new listing replaces the + * old one, so a refresh drops files that were renamed or deleted; every other + * directory keeps its entries (a collapse must not throw away the cache). + */ +fun mergeDirectoryListing( + childrenByPath: Map>, + directory: String, + entries: List, +): Map> { + val sorted = entries.sortedWith( + compareByDescending { it.isDir }.thenBy { it.name.lowercase() }, + ) + return childrenByPath + (comparableFilePath(directory) to sorted) +} + +/** Args: the tree's root, its loaded listings keyed by comparable path, and the expanded set. */ +fun flattenFileTree( + root: String, + childrenByPath: Map>, + expanded: Set, + loading: Set = emptySet(), +): List { + val rows = mutableListOf() + val expandedKeys = expanded.mapTo(HashSet()) { comparableFilePath(it) } + val loadingKeys = loading.mapTo(HashSet()) { comparableFilePath(it) } + + fun walk(directory: String, depth: Int) { + val key = comparableFilePath(directory) + val entries = childrenByPath[key] ?: return + for (entry in entries) { + val path = joinFilePath(directory, entry.name) + val pathKey = comparableFilePath(path) + val open = entry.isDir && pathKey in expandedKeys + rows += FileTreeRow( + name = entry.name, + path = path, + isDir = entry.isDir, + size = entry.size, + depth = depth, + expanded = open, + loading = entry.isDir && pathKey in loadingKeys, + ) + if (!open) continue + val children = childrenByPath[pathKey] + if (children == null) continue + if (children.isEmpty()) { + rows += FileTreeRow( + name = "", + path = path, + isDir = true, + size = 0, + depth = depth + 1, + expanded = false, + loading = false, + empty = true, + ) + } else { + walk(path, depth + 1) + } + } + } + + walk(root, 0) + return rows +} + +/** Every ancestor directory of [target] below [cwd], outermost first (for reveal). */ +fun revealAncestors(cwd: String, target: String): List { + if (!isInsideDirectory(target, cwd)) return emptyList() + val relative = relativeFilePath(target, cwd).trimEnd('/') + if (relative.isEmpty()) return emptyList() + val segments = relative.split('/').filter { it.isNotEmpty() } + if (segments.size <= 1) return emptyList() + val ancestors = mutableListOf() + var path = cwd + // The target itself is not expanded: it is a row to scroll to, and + // expanding a file is meaningless. + for (segment in segments.dropLast(1)) { + path = joinFilePath(path, segment) + ancestors += path + } + return ancestors +} + +/** + * The directory dots of the explorer: every ancestor of a changed file up to + * and including [cwd]. Directories outside the workspace are ignored, so a + * session opened in a subdirectory does not decorate its parents. + */ +fun changedDirectoryPaths(cwd: String, changedPaths: List): Set { + val directories = mutableSetOf() + val root = comparableFilePath(cwd) + for (changed in changedPaths) { + var directory = fileDirectory(normalizeFilePath(changed)) + // A changed file directly in cwd still marks cwd. + while (isInsideDirectory(directory, cwd)) { + val key = comparableFilePath(directory) + directories += key + if (key == root) break + val parent = fileDirectory(directory) + if (parent == directory || parent.isEmpty()) break + directory = parent + } + } + return directories +} + +/** Ranked index rows stay in server order; duplicates and empty paths are dropped. */ +fun fileSearchRows(paths: List): List { + val seen = HashSet() + return paths.mapNotNull { raw -> + val path = normalizeFilePath(raw) + if (path.isEmpty() || !seen.add(path)) return@mapNotNull null + val cut = path.lastIndexOf('/') + FileSearchRow( + path = path, + name = if (cut < 0) path else path.substring(cut + 1), + directory = if (cut < 0) "" else path.substring(0, cut), + ) + } +} + +/** `buildAtMentionText` (web): a directory gets a trailing slash, spaces are quoted. */ +fun atMentionText(relativePath: String, isDir: Boolean): String { + val path = if (isDir) "$relativePath/" else relativePath + return if (path.contains(' ')) "@\"$path\" " else "@$path " +} + +/** `buildFileAtMentionsText` (web): one mention per uploaded file, in server order. */ +fun atMentionsText(relativePaths: List): String = + relativePaths.joinToString("") { atMentionText(it, isDir = false) } + +/** `buildFileLineMentionText` (web): a closed file mention for one line or an inclusive range. */ +fun fileLineMentionText(relativePath: String, startLine: Int, endLine: Int): String { + val first = maxOf(1, minOf(startLine, endLine)) + val last = maxOf(1, maxOf(startLine, endLine)) + val mention = if (relativePath.contains(' ')) "@\"$relativePath\"" else "@$relativePath" + val suffix = if (first == last) ":$first" else ":$first-$last" + return "$mention$suffix " +} + +/** + * The mention the viewer inserts for a file: `@path` normally, `@path:line` + * when the tab was opened at a line (web `onMentionLines`), so the agent gets + * the same closed reference the Markdown line links produce. + */ +fun fileMentionText(relativePath: String, line: Int?): String = + if (line != null) fileLineMentionText(relativePath, line, line) else atMentionText(relativePath, isDir = false) diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/FileViewerModes.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/FileViewerModes.kt new file mode 100644 index 000000000..a4593cf8a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/FileViewerModes.kt @@ -0,0 +1,131 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.ui.files.FileOpenMode + +/** + * How the file viewer renders a path, and which modes it offers. + * + * Port of the client halves of `lib/file-types.ts` and + * `components/FileViewer.tsx`: the extension table decides image/audio/PDF/ + * docx, everything else is text until the server answers with a binary body. + * PDF and docx are documents the app renders itself; audio is not a preview + * type of the phone panel (the inventory's file-viewer row lists markdown, + * docx, image and binary only), so it is offered as a download like any other + * binary. + */ +enum class ViewerKind { Image, Pdf, Docx, Text } + +/** The two response languages the viewer can render instead of raw source. */ +enum class TextPreviewKind { Markdown, Html } + +private val IMAGE_EXTENSIONS = setOf("png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif") + +private val PDF_EXTENSIONS = setOf("pdf") +private val DOCX_EXTENSIONS = setOf("docx") + +/** The one definition of "Markdown" for the viewer and the composer. */ +private val MARKDOWN_EXTENSIONS = setOf("md", "markdown", "mdx") + +/** `getFileExt` (web) is [fileExtension] in `domain/Attachments.kt`. */ + +fun isImageFile(filePath: String): Boolean = fileExtension(filePath) in IMAGE_EXTENSIONS + +fun isPdfFile(filePath: String): Boolean = fileExtension(filePath) in PDF_EXTENSIONS + +fun isDocxFile(filePath: String): Boolean = fileExtension(filePath) in DOCX_EXTENSIONS + +/** The extension-first classification; an unknown extension is treated as text. */ +fun viewerKind(filePath: String): ViewerKind = when { + isImageFile(filePath) -> ViewerKind.Image + isPdfFile(filePath) -> ViewerKind.Pdf + isDocxFile(filePath) -> ViewerKind.Docx + else -> ViewerKind.Text +} + +/** Preview is offered exactly for the two languages the viewer can render. */ +fun textPreviewKind(language: String?): TextPreviewKind? = when (language?.lowercase()) { + "markdown", "md" -> TextPreviewKind.Markdown + "html", "htm" -> TextPreviewKind.Html + else -> null +} + +/** Markdown opens on its rendered preview like the web; everything else on source. */ +fun defaultViewerMode(filePath: String, language: String? = null): FileOpenMode { + if (viewerKind(filePath) != ViewerKind.Text) return FileOpenMode.Source + return when { + textPreviewKind(language) != null -> FileOpenMode.Preview + isMarkdownFile(filePath) -> FileOpenMode.Preview + else -> FileOpenMode.Source + } +} + +fun isMarkdownFile(filePath: String): Boolean = + fileExtension(filePath) in MARKDOWN_EXTENSIONS + +/** + * The mode buttons the toolbar shows, in order. Source is always available for + * text; Preview only for markdown/HTML; Diff only when git reported a textual + * patch. A non-text file has no mode switch at all. + */ +fun availableViewerModes(kind: ViewerKind, language: String?, hasDiff: Boolean): List { + if (kind != ViewerKind.Text) return emptyList() + return buildList { + add(FileOpenMode.Source) + if (textPreviewKind(language) != null) add(FileOpenMode.Preview) + if (hasDiff) add(FileOpenMode.Diff) + } +} + +/** Falls back to Source when the requested mode is not offered (web's guard effect). */ +fun effectiveViewerMode(requested: FileOpenMode, available: List): FileOpenMode = + if (requested in available || available.isEmpty()) requested else FileOpenMode.Source + +// --- Upload conflicts ------------------------------------------------------- + +/** The strategies the web sends as `?conflict=`; `error` is the pre-flight-free first try. */ +enum class UploadConflictStrategy(val wire: String) { + Error("error"), + Overwrite("overwrite"), + Skip("skip"), +} + +/** The pre-flight answer: names already present, and the subset replace cannot touch. */ +data class UploadConflictPlan( + val conflicts: List, + val nonReplaceable: List, +) { + /** Files "Replace all" can actually overwrite. */ + val replaceable: List get() = conflicts.filterNot { it in nonReplaceable.toSet() } + + /** At least one file can be overwritten, so the Replace button is meaningful. */ + val canReplace: Boolean get() = replaceable.isNotEmpty() +} + +/** + * The conflict dialog's data, or null when the pre-flight found nothing to ask + * about. `nonReplaceable` entries are still listed: the web names them above + * the buttons instead of letting "Replace all" fail per file. + */ +fun uploadConflictPlan(conflicts: List, nonReplaceable: List): UploadConflictPlan? { + if (conflicts.isEmpty()) return null + val known = conflicts.toSet() + return UploadConflictPlan( + conflicts = conflicts.distinct(), + nonReplaceable = nonReplaceable.filter { it in known }.distinct(), + ) +} + +/** + * Upload caps shared with the server (`lib/upload-limits.ts`): 25 MiB per + * file, 100 MiB per selection. Every file travels in its own request, so one + * request stays below the server's 26 MiB pre-auth proxy cap. + */ +const val MAX_UPLOAD_BYTES_PER_FILE: Long = 25L * 1024 * 1024 +const val MAX_UPLOAD_BYTES_TOTAL: Long = 100L * 1024 * 1024 + +/** `formatSize` in the web explorer: bytes under 1 KiB, one decimal for KiB/MiB. */ +fun formatFileSize(bytes: Long): String = when { + bytes < 1024 -> "$bytes B" + bytes < 1024 * 1024 -> formatDecimal(bytes / 1024.0) + " KB" + else -> formatDecimal(bytes / (1024.0 * 1024.0)) + " MB" +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/GitClone.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/GitClone.kt new file mode 100644 index 000000000..740c9a6d8 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/GitClone.kt @@ -0,0 +1,40 @@ +package io.github.kahme247.ompweb.domain + +private val HTTPS_URL = Regex("""^https://[^\s/@]+(?:@[^\s/]+)?/\S+$""") +private val SSH_URL = Regex("""^ssh://(?:[^\s/@]+@)?[^\s/]+/\S+$""") +// scp-like `user@host:path` / `alias:path`. A 2+ char host and no backslashes +// keep Windows paths (`C:/repo`, `C:\repo`) out; `::` transports never match. +private val SCP_URL = Regex("""^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9.-]{2,}:(?!:|//)[^\s\\]+$""") +private val DIRECTORY_NAME = Regex("""^[A-Za-z0-9._-]+$""") + +/** + * Accepts https, ssh:// and scp-like ssh URLs only (no file://, ext::, or + * option-looking input); returns the directory name git would clone into, or + * null when the URL is unsupported or yields no safe name. + * + * Port of `cloneDirectoryName` in `lib/git-clone.ts`. + */ +fun cloneDirectoryName(url: String): String? { + val trimmed = url.trim() + if (!HTTPS_URL.matches(trimmed) && !SSH_URL.matches(trimmed) && !SCP_URL.matches(trimmed)) return null + val path = trimmed.replace(Regex("[?#].*$"), "").replace(Regex("/+$"), "") + val name = path.substring(maxOf(path.lastIndexOf('/'), path.lastIndexOf(':')) + 1) + .replace(Regex("""\.git$"""), "") + return name.takeIf { DIRECTORY_NAME.matches(it) && it != "." && it != ".." } +} + +const val MAX_PROGRESS_LINES = 200 + +/** + * Appends git output to the progress log, applying carriage returns (git + * rewrites its progress line with `\r`) so the log keeps only each line's + * latest state. A trailing `\r` is kept so the next chunk overwrites it. + */ +fun appendProgress(log: String, chunk: String): String { + val lines = (log + chunk).split("\n").map { line -> + val segments = line.split("\r").filter { it.isNotEmpty() } + val latest = segments.lastOrNull() ?: "" + if (line.endsWith("\r") && latest.isNotEmpty()) "$latest\r" else latest + } + return lines.takeLast(MAX_PROGRESS_LINES).joinToString("\n") +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/GitStatus.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/GitStatus.kt new file mode 100644 index 000000000..5961628a9 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/GitStatus.kt @@ -0,0 +1,159 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive + +/** + * Git changes as `GET /api/git/status` and `GET /api/git/diff` deliver them + * (port of `lib/git-types.ts` and the client halves of + * `components/GitChangesPanel.tsx` / `components/FileExplorer.tsx`). + * + * The server runs git; the app never does. This file therefore owns exactly + * what the client decides: how an untrusted response becomes status rows, + * which rows the filter keeps, which rows `.gitattributes` moved into the + * collapsed group, and which row is previewed first. + */ + +/** `GitFileStatusKind` in `lib/git-types.ts`; [code] is the one-letter badge. */ +enum class GitFileStatusKind(val wireName: String, val code: String) { + MODIFIED("modified", "M"), + ADDED("added", "A"), + DELETED("deleted", "D"), + RENAMED("renamed", "R"), + UNTRACKED("untracked", "U"), + CONFLICT("conflict", "C"); + + companion object { + fun fromWire(value: String?): GitFileStatusKind? = + entries.firstOrNull { it.wireName == value } + + /** Fallback for a server that only sent the porcelain code. */ + fun fromCode(value: String?): GitFileStatusKind? = + entries.firstOrNull { it.code == value } + } +} + +/** Why `.gitattributes` moved a changed file out of the main review list. */ +enum class GitCollapseReason(val wireName: String) { + GENERATED("generated"), + VENDORED("vendored"), + DOCUMENTATION("documentation"), + NO_DIFF("no-diff"); + + companion object { + fun fromWire(value: String?): GitCollapseReason? = + entries.firstOrNull { it.wireName == value } + } +} + +/** One changed file, as `GitFileStatus` in `lib/git-types.ts`. */ +data class GitFileStatus( + /** Absolute path on the server, exactly as the routes report it. */ + val filePath: String, + val status: GitFileStatusKind, + /** Badge character; the server's `code`, validated to the six known ones. */ + val code: String, + val collapseReason: GitCollapseReason? = null, +) + +/** `GitStatusResponse`: the whole answer of `GET /api/git/status?cwd=`. */ +data class GitStatusResponse( + val isGitRepository: Boolean, + val repositoryRoot: String?, + val files: List, +) + +/** + * `GitFileDiffResponse`: `{supported, status?, patch?}`. `supported: false` + * covers every case where git has no textual diff (deleted, binary, + * `.gitattributes -diff`, unchanged, too large) — the viewer then shows the + * "no diff" state instead of an empty pane. + */ +data class GitFileDiff( + val supported: Boolean, + val patch: String? = null, +) + +/** The two groups the changes list renders (web `[primaryFiles, attributeFiles]`). */ +data class GitChangeGroups( + val primary: List = emptyList(), + val collapsed: List = emptyList(), +) + +/** + * Parses `GET /api/git/status` leniently: `null` means the body is not the + * documented shape (the caller reports [`io.github.kahme247.ompweb.net.ApiError.Http`]), + * while a single row without a recognizable status is dropped — there is no + * label, colour or diff mode to give it. + */ +fun parseGitStatusResponse(body: String): GitStatusResponse? { + val root = runCatching { Json.parseToJsonElement(body) }.getOrNull() as? JsonObject ?: return null + val filesElement = root["files"] as? JsonArray ?: return null + val files = filesElement.mapNotNull(::parseGitFileStatus) + return GitStatusResponse( + isGitRepository = root["isGitRepository"]?.jsonPrimitive?.booleanOrNull ?: false, + repositoryRoot = root["repositoryRoot"]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() }, + files = files, + ) +} + +private fun parseGitFileStatus(element: JsonElement): GitFileStatus? { + val entry = element as? JsonObject ?: return null + val filePath = entry["filePath"]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() } ?: return null + val rawCode = entry["code"]?.jsonPrimitive?.contentOrNull + val kind = GitFileStatusKind.fromWire(entry["status"]?.jsonPrimitive?.contentOrNull) + ?: GitFileStatusKind.fromCode(rawCode) + ?: return null + val code = rawCode?.takeIf { candidate -> GitFileStatusKind.fromCode(candidate) != null } ?: kind.code + return GitFileStatus( + filePath = filePath, + status = kind, + code = code, + collapseReason = GitCollapseReason.fromWire(entry["collapseReason"]?.jsonPrimitive?.contentOrNull), + ) +} + +/** Parses `GET /api/git/diff`; `null` when the body is not the documented shape. */ +fun parseGitFileDiff(body: String): GitFileDiff? { + val root = runCatching { Json.parseToJsonElement(body) }.getOrNull() as? JsonObject ?: return null + val supported = root["supported"]?.jsonPrimitive?.booleanOrNull ?: return null + return GitFileDiff( + supported = supported, + patch = root["patch"]?.jsonPrimitive?.contentOrNull, + ) +} + +// --- Client-side list rules (web `GitChangesPanel`) ------------------------- + +/** + * The row the pane previews: the previous selection while it still exists, + * otherwise the first reviewable change (not one `.gitattributes` collapsed), + * exactly like the web. + */ +fun pickGitSelection(previous: String?, files: List): String? { + if (previous != null && files.any { it.filePath == previous }) return previous + return (files.firstOrNull { it.collapseReason == null } ?: files.firstOrNull())?.filePath +} + +/** + * Splits the changed files the way the web does: the filter matches the path + * shown in the row (relative to [cwd], `/`-separated, case-insensitive), and + * `.gitattributes` rows go to the collapsed group. + */ +fun gitChangeGroups(files: List, filter: String, cwd: String?): GitChangeGroups { + val needle = filter.trim().lowercase() + val matching = if (needle.isEmpty()) { + files + } else { + files.filter { relativeFilePath(it.filePath, cwd).lowercase().contains(needle) } + } + return GitChangeGroups( + primary = matching.filter { it.collapseReason == null }, + collapsed = matching.filter { it.collapseReason != null }, + ) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/HostTools.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/HostTools.kt new file mode 100644 index 000000000..59248e53a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/HostTools.kt @@ -0,0 +1,269 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +/** The consent-bearing host tools the app registers with omp (`set_host_tools`). */ +enum class HostToolKind { OpenUrl, OpenFile } + +/** One `host_tool_call` that needs the user: a URL or file from an agent. */ +data class HostToolPrompt( + val id: String, + val sessionId: String, + val toolName: String, + val kind: HostToolKind, + /** The URL, or the full file path (the dialog shows it as-is). */ + val target: String, + /** True when the call comes from a session the user is not viewing. */ + val crossSession: Boolean, +) + +/** What the UI does with a host tool call before asking the user. */ +enum class HostToolDecision { + /** Open it right away (same session, auto-open setting on / file viewer). */ + Execute, + + /** Queue the consent prompt. */ + Ask, + + /** Answer with an error result; nothing is opened. */ + Error, +} + +/** Web `requestOpenUrl`/`handleCrossSessionHostTool`: only the viewed session may auto-open. */ +fun isCrossSession(callSessionId: String, viewedSessionId: String?): Boolean = + viewedSessionId == null || viewedSessionId != callSessionId + +/** + * `open_url`: unsafe or missing URLs are rejected; a link from another session + * always asks, and the auto-open preference applies to the viewed session only. + */ +fun openUrlDecision( + url: String?, + callSessionId: String, + viewedSessionId: String?, + autoOpen: Boolean, +): HostToolDecision = when { + !isSafeOpenUrl(url) -> HostToolDecision.Error + isCrossSession(callSessionId, viewedSessionId) -> HostToolDecision.Ask + autoOpen -> HostToolDecision.Execute + else -> HostToolDecision.Ask +} + +/** + * `open_file`: no path is an error, another session's file always asks, + * a file of the viewed session opens in the app's file viewer. + */ +fun openFileDecision( + path: String?, + callSessionId: String, + viewedSessionId: String?, +): HostToolDecision = when { + path.isNullOrBlank() -> HostToolDecision.Error + isCrossSession(callSessionId, viewedSessionId) -> HostToolDecision.Ask + else -> HostToolDecision.Execute +} + +/** Result text the agent sees for an opened target. */ +fun openedResult(target: String): String = "Opened $target" + +/** Result text while the consent prompt is shown (web `requestOpenUrl`). */ +fun askedResult(target: String, crossSession: Boolean): String = if (crossSession) { + "Asked the user to confirm opening $target; they are viewing another ompweb session." +} else { + "Asked the user to confirm opening $target." +} + +const val UNSAFE_URL_RESULT: String = "Unsafe or invalid URL not opened" +const val NO_PATH_RESULT: String = "No path provided" +const val NOTIFICATION_SHOWN_RESULT: String = "Notification shown" +const val CLIPBOARD_EMPTY_RESULT: String = "(clipboard is empty)" +const val CLIPBOARD_UNAVAILABLE_READ: String = "Clipboard read is not available" +const val CLIPBOARD_UNAVAILABLE_WRITE: String = "Clipboard write is not available" +const val CLIPBOARD_WRITE_FAILED: String = "Clipboard write failed" +const val CLIPBOARD_READ_DENIED: String = + "Clipboard read was denied. Return to the ompweb app and try again." + +fun unknownHostToolResult(toolName: String): String = "Host tool \"$toolName\" is not available in ompweb" + +fun unknownHostUriResult(resource: String): String = "Unknown pi-web resource: $resource" + +/** + * The `set_host_tools` payload (web `HOST_TOOL_DEFINITIONS`). Registration is + * per wrapper and not persisted, so it is re-sent on every reconnect. + */ +val HOST_TOOL_DEFINITIONS: JsonArray = buildJsonArray { + add( + buildJsonObject { + put("name", "open_url") + put("description", "Open a URL in the user's browser.") + put( + "parameters", + buildJsonObject { + put("type", "object") + put( + "properties", + buildJsonObject { + put("url", buildJsonObject { put("type", "string") }) + }, + ) + put("required", buildJsonArray { add(JsonPrimitive("url")) }) + }, + ) + }, + ) + add( + buildJsonObject { + put("name", "notify") + put("description", "Show a browser notification to the user.") + put( + "parameters", + buildJsonObject { + put("type", "object") + put( + "properties", + buildJsonObject { + put("title", buildJsonObject { put("type", "string") }) + put( + "message", + buildJsonObject { + put("type", "string") + put("description", "Optional notification body.") + }, + ) + }, + ) + put("required", buildJsonArray { add(JsonPrimitive("title")) }) + }, + ) + }, + ) + add( + buildJsonObject { + put("name", "open_file") + put("description", "Open a file in the workspace file viewer.") + put( + "parameters", + buildJsonObject { + put("type", "object") + put( + "properties", + buildJsonObject { + put( + "path", + buildJsonObject { + put("type", "string") + put("description", "Absolute or workspace-relative file path.") + }, + ) + }, + ) + put("required", buildJsonArray { add(JsonPrimitive("path")) }) + }, + ) + }, + ) +} + +/** + * The `set_host_uri_schemes` payload (web `HOST_URI_SCHEMES`): + * `pi-web://clipboard` writes the device clipboard and reads it only after the + * user consented in-app for that session. + */ +val HOST_URI_SCHEMES: JsonArray = buildJsonArray { + add( + buildJsonObject { + put("scheme", "pi-web") + put( + "description", + "App-integrated resources: pi-web://clipboard writes the user's clipboard and reads it after in-app consent.", + ) + put("writable", true) + }, + ) +} + +/** + * Shared guard for URLs the app hands to the system (agent/extension + * `open_url`, markdown links; web `isSafeOpenUrl`): only http, https and + * mailto, no protocol-relative, no other scheme. + */ +fun isSafeOpenUrl(raw: String?): Boolean { + if (raw == null) return false + val url = raw.trim() + if (url.isEmpty()) return false + if (url.startsWith("//")) return false + val match = Regex("^([a-zA-Z][a-zA-Z0-9+.-]*):").find(url) ?: return false + return match.groupValues[1].lowercase() in SAFE_URL_SCHEMES +} + +private val SAFE_URL_SCHEMES = setOf("http", "https", "mailto") + +fun setHostToolsCommand(): JsonObject = buildJsonObject { + put("type", "set_host_tools") + put("tools", HOST_TOOL_DEFINITIONS) +} + +fun setHostUriSchemesCommand(): JsonObject = buildJsonObject { + put("type", "set_host_uri_schemes") + put("schemes", HOST_URI_SCHEMES) +} + +/** `host_tool_result {id,isError,result:{content:[{type:"text",text}]}}`. */ +fun hostToolResultCommand(id: String, text: String, isError: Boolean): JsonObject = buildJsonObject { + put("type", "host_tool_result") + put("id", id) + put("isError", isError) + put( + "result", + buildJsonObject { + put( + "content", + buildJsonArray { + add( + buildJsonObject { + put("type", "text") + put("text", text) + }, + ) + }, + ) + }, + ) +} + +/** `host_uri_result` — a read answer carries content, a failure carries `isError` + `error`. */ +fun hostUriResultCommand( + id: String, + content: String? = null, + contentType: String? = null, + error: String? = null, +): JsonObject = buildJsonObject { + put("type", "host_uri_result") + put("id", id) + if (error != null) { + put("isError", true) + put("error", error) + } else { + if (content != null) { + put("content", content) + put("contentType", contentType ?: "text/plain") + } + } +} + +/** + * Reads and writes the device clipboard for `pi-web://clipboard`. The + * controller holds the seam so it stays JVM-testable; the Android + * implementation lives in `ui/hosttools/AndroidHostTools.kt`. + */ +interface ClipboardBridge { + /** Current text, or null when the clipboard is empty or unreadable. */ + fun readText(): String? + + fun writeText(text: String) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/LiveStatus.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/LiveStatus.kt new file mode 100644 index 000000000..10f27c55b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/LiveStatus.kt @@ -0,0 +1,93 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.ElsewhereSession + +/** + * "We have not heard from the agent yet" decisions. + * + * Cache-first startup paints the last known transcript/list from disk before + * the server answers. Until something about the *live* state has arrived, an + * empty status line reads as "the agent is idle / nothing is running", which is + * a claim the client cannot make yet. These two predicates decide when the UI + * says so instead (chat footer *Connecting…*, session list *Updating…*). + */ + +/** + * The chat footer shows *Connecting…* while the transcript is already on screen + * but the run state is unknown: cached history (`historyStale`) or any message + * rows. An empty chat has its own loading placeholder, so it stays quiet here. + */ +fun chatShowsConnecting(liveStateKnown: Boolean, hasTranscript: Boolean): Boolean = + !liveStateKnown && hasTranscript + +/** + * The session list marks itself as updating while it shows cached rows and the + * running badges are not known yet (no list answer and no `running` frame). + * [failed] ends that wait: the request answered with an error, so a spinner + * would claim an answer is still on its way. + */ +fun sessionsShowsUpdating(runningKnown: Boolean, hasRows: Boolean, failed: Boolean): Boolean = + !runningKnown && hasRows && !failed + +/** + * The list's revalidation failed while cached rows are on screen. Without rows + * the full-screen error takes over; with rows the list keeps its content and + * shows this compact error with a retry instead of the endless *Updating…*. + */ +fun sessionsShowsRefreshError(hasRows: Boolean, failed: Boolean): Boolean = + hasRows && failed + +/** + * Who owns a session another process is running (`SessionElsewhere.kind`). + * A newer server may report a kind this app does not know; [Unknown] keeps the + * banner generic instead of hiding the owner. + */ +enum class ElsewhereKind { OmpWeb, Terminal, Unknown } + +fun elsewhereKind(raw: String?): ElsewhereKind = when (raw) { + "omp-web" -> ElsewhereKind.OmpWeb + "terminal" -> ElsewhereKind.Terminal + else -> ElsewhereKind.Unknown +} + +/** + * How strongly the server proved the ownership (`SessionElsewhere.source`), + * strongest first. [Transcript] only says "written recently, never closed", + * which a crashed writer also leaves behind. + */ +enum class ElsewhereSource { InstanceList, Process, Transcript, Unknown } + +fun elsewhereSource(raw: String?): ElsewhereSource = when (raw) { + "instance-list" -> ElsewhereSource.InstanceList + "process" -> ElsewhereSource.Process + "transcript" -> ElsewhereSource.Transcript + else -> ElsewhereSource.Unknown +} + +/** + * What a send does while another omp process owns the session. + * + * omp allows one writer per session: a prompt from this app makes it move the + * chat to a copy with a new id. That must never happen silently, so: + * + * - [Allow]: nobody else owns the chat (or the user confirmed the copy) — send. + * - [Block]: a live instance-list entry or a live `omp --resume` process proves + * the owner. The send is refused with the banner; the explicit + * "send anyway (copy)" action is the only way through. + * - [Confirm]: only the transcript signal (recent write, no exit record), which + * is an indication, not proof. Ask once before sending instead of locking the + * composer. + */ +enum class ForeignSubmitDecision { Allow, Block, Confirm } + +/** + * [ForeignSubmitDecision] for a send into a session whose owner is + * [elsewhere]. [confirmed] is true once the user accepted "send anyway (copy)"; + * it is never stored, so the next send asks again. + */ +fun foreignSubmitDecision(elsewhere: ElsewhereSession?, confirmed: Boolean): ForeignSubmitDecision = when { + elsewhere == null -> ForeignSubmitDecision.Allow + confirmed -> ForeignSubmitDecision.Allow + elsewhereSource(elsewhere.source) == ElsewhereSource.Transcript -> ForeignSubmitDecision.Confirm + else -> ForeignSubmitDecision.Block +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/LocalPrefs.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/LocalPrefs.kt new file mode 100644 index 000000000..47e36eaf8 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/LocalPrefs.kt @@ -0,0 +1,53 @@ +package io.github.kahme247.ompweb.domain + +/** + * The four local display preferences of Settings → Interface & Behavior + * (`hooks/useFontSize.ts`, `hooks/useUiScale.ts`, `hooks/useTouchTargets.ts`). + * + * The web stores the wire values verbatim under `omp-font-size`, + * `omp-ui-scale` and `omp-touch-targets`; an unknown or missing value falls + * back to the documented default instead of throwing, so a hand-edited + * DataStore cannot break the UI. + */ +enum class FontSizePreference(val wire: String, val fontScale: Float) { + SM("sm", 0.875f), + MD("md", 1f), + LG("lg", 1.125f), + XL("xl", 1.25f); + + companion object { + val DEFAULT = MD + + fun fromWire(value: String?): FontSizePreference = + entries.firstOrNull { it.wire == value } ?: DEFAULT + } +} + +enum class UiScalePreference(val wire: String, val densityScale: Float) { + COMPACT("compact", 0.9f), + STANDARD("standard", 1f), + COMFORTABLE("comfortable", 1.1f), + LARGE("large", 1.2f); + + companion object { + val DEFAULT = STANDARD + + fun fromWire(value: String?): UiScalePreference = + entries.firstOrNull { it.wire == value } ?: DEFAULT + } +} + +enum class TouchTargetsPreference(val wire: String, val minimumSizeDp: Float) { + /** Material's own 48 dp minimum. */ + AUTO("auto", 48f), + COMPACT("compact", 40f), + ACCESSIBLE("accessible", 56f); + + companion object { + val DEFAULT = AUTO + + fun fromWire(value: String?): TouchTargetsPreference = + entries.firstOrNull { it.wire == value } ?: DEFAULT + } +} + diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ModelOptions.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ModelOptions.kt new file mode 100644 index 000000000..03227588b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ModelOptions.kt @@ -0,0 +1,125 @@ +package io.github.kahme247.ompweb.domain + +import java.text.Collator +import java.util.Locale + +/** + * Model-picker options and ordering, ported from + * `components/ChatInput-model-options.ts` and the grouping helpers of + * `components/ChatInput-model-picker.tsx`. + */ + +/** One row of the picker (`ModelOption`). */ +data class ModelOption(val provider: String, val modelId: String, val name: String = "") { + /** `provider:modelId` — the key `omp-composer-models` stores. */ + val key: String get() = "$provider:$modelId" + + /** The name the picker paints, falling back to the id like the web. */ + val displayName: String get() = name.ifBlank { modelId } +} + +/** + * `Intl.Collator(locale, {numeric: true, sensitivity: "base"})`: case- and + * accent-insensitive, but digit runs compare by value (`gpt-5.2` before `gpt-5.10`). + */ +class ModelNameComparator(locale: Locale = Locale.getDefault()) : Comparator { + private val collator: Collator = Collator.getInstance(locale).apply { + strength = Collator.PRIMARY + } + + override fun compare(a: String, b: String): Int { + var i = 0 + var j = 0 + while (i < a.length && j < b.length) { + val digitRun = a[i].isDigit() && b[j].isDigit() + val startA = i + val startB = j + if (digitRun) { + while (i < a.length && a[i].isDigit()) i++ + while (j < b.length && b[j].isDigit()) j++ + val numberA = a.substring(startA, i).trimStart('0') + val numberB = b.substring(startB, j).trimStart('0') + if (numberA.length != numberB.length) return numberA.length - numberB.length + val numeric = numberA.compareTo(numberB) + if (numeric != 0) return numeric + } else { + while (i < a.length && !a[i].isDigit()) i++ + while (j < b.length && !b[j].isDigit()) j++ + val textual = collator.compare(a.substring(startA, i), b.substring(startB, j)) + if (textual != 0) return textual + } + } + return (a.length - i) - (b.length - j) + } +} + +/** `compareModelOptions`: display name, then provider, then id. */ +fun compareModelOptions(a: ModelOption, b: ModelOption, comparator: Comparator): Int { + val byName = comparator.compare(a.displayName, b.displayName) + if (byName != 0) return byName + val byProvider = comparator.compare(a.provider, b.provider) + if (byProvider != 0) return byProvider + return comparator.compare(a.modelId, b.modelId) +} + +/** The catalog rows the picker offers: allow-list filtered, then name-sorted. */ +fun buildModelOptions( + models: List, + visibleKeys: Set?, + comparator: Comparator = ModelNameComparator(), +): List = models + .filter { visibleKeys == null || visibleKeys.contains(it.key) } + .sortedWith { a, b -> compareModelOptions(a, b, comparator) } + +/** `filterModelOptions`: case-insensitive substring over name, id and provider. */ +fun filterModelOptions( + options: List, + query: String, + locale: Locale = Locale.getDefault(), +): List { + val normalizedQuery = query.trim().lowercase(locale) + if (normalizedQuery.isEmpty()) return options + return options.filter { option -> + option.name.lowercase(locale).contains(normalizedQuery) || + option.modelId.lowercase(locale).contains(normalizedQuery) || + option.provider.lowercase(locale).contains(normalizedQuery) + } +} + +/** One provider rail entry with that provider's models. */ +data class ProviderModelGroup(val provider: String, val options: List) + +/** `groupModelOptionsByProvider`: provider first-seen order from the sorted list. */ +fun groupModelOptionsByProvider(options: List): List { + val order = ArrayList() + val byProvider = LinkedHashMap>() + for (option in options) { + val bucket = byProvider.getOrPut(option.provider) { + order += option.provider + mutableListOf() + } + bucket += option + } + return order.map { provider -> ProviderModelGroup(provider, byProvider.getValue(provider)) } +} + +/** `orderProviderGroups`: the current model's provider first so the picker opens on what you use. */ +fun orderProviderGroups( + groups: List, + currentProvider: String?, +): List { + if (currentProvider == null) return groups + val index = groups.indexOfFirst { it.provider == currentProvider } + if (index <= 0) return groups + return buildList(groups.size) { + add(groups[index]) + groups.forEachIndexed { at, group -> if (at != index) add(group) } + } +} + +/** `providerInitials`: the first letter of up to two `-`/`_` separated parts. */ +fun providerInitials(id: String): String { + val parts = id.split('-', '_').filter { it.isNotEmpty() }.take(2) + val initials = parts.mapNotNull { it.firstOrNull() }.joinToString("").uppercase() + return initials.ifEmpty { "?" } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ModelsConfig.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ModelsConfig.kt new file mode 100644 index 000000000..89f7d61fb --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ModelsConfig.kt @@ -0,0 +1,688 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.put + +/** + * The `models.yml` editor model, ported from `lib/omp/models-config.ts` and + * `ModelsConfig*.tsx`, plus the System tab's registry/roles/fallbacks helpers. + * + * Pure: no Android or HTTP types, so every rule below is JVM-tested. The app + * never parses YAML — the server returns the parsed file as JSON and serializes + * it back, so "round-trip" here means the JSON tree survives the editor + * untouched (unknown keys are carried in `extras`, like the web's YAML merge). + */ + +/** `cost` of one model. The server requires all four fields when the block exists. */ +data class ModelCostConfig( + val input: Double? = null, + val output: Double? = null, + val cacheRead: Double? = null, + val cacheWrite: Double? = null, + val extras: Map = emptyMap(), +) { + fun toJson(): JsonObject = buildJsonObject { + val emitted = HashSet(4) + input?.let { put("input", JsonPrimitive(it)); emitted += "input" } + output?.let { put("output", JsonPrimitive(it)); emitted += "output" } + cacheRead?.let { put("cacheRead", JsonPrimitive(it)); emitted += "cacheRead" } + cacheWrite?.let { put("cacheWrite", JsonPrimitive(it)); emitted += "cacheWrite" } + extras.forEach { (key, value) -> if (key !in emitted) put(key, value) } + } + + companion object { + val COST_KEYS = listOf("input", "output", "cacheRead", "cacheWrite") + val KNOWN = COST_KEYS.toSet() + } +} + +/** omp's `thinking` block (mode/efforts/defaultLevel/effortMap). */ +data class ThinkingConfig( + val mode: String? = null, + val efforts: List? = null, + val defaultLevel: String? = null, + val effortMap: Map? = null, + val extras: Map = emptyMap(), +) { + fun toJson(): JsonObject = buildJsonObject { + val emitted = HashSet(4) + mode?.let { put("mode", it); emitted += "mode" } + efforts?.let { list -> + put("efforts", buildJsonArray { list.forEach { add(JsonPrimitive(it)) } }) + emitted += "efforts" + } + defaultLevel?.let { put("defaultLevel", it); emitted += "defaultLevel" } + effortMap?.let { map -> + put("effortMap", buildJsonObject { map.forEach { (level, effort) -> put(level, effort) } }) + emitted += "effortMap" + } + extras.forEach { (key, value) -> if (key !in emitted) put(key, value) } + } + + companion object { + val KNOWN = setOf("mode", "efforts", "defaultLevel", "effortMap") + } +} + +/** One `providers..models[]` row (the subset the editor edits). */ +data class ModelConfig( + val id: String = "", + val name: String? = null, + val api: String? = null, + val reasoning: Boolean? = null, + val thinking: ThinkingConfig? = null, + /** Raw `input` list; `text`/`image`. Null means "not specified". */ + val input: List? = null, + val contextWindow: Int? = null, + val maxTokens: Int? = null, + val cost: ModelCostConfig? = null, + /** Keys the editor does not expose (`baseUrl`, `headers`, `compat`, …), preserved verbatim. */ + val extras: Map = emptyMap(), +) { + val hasImageInput: Boolean get() = input?.contains("image") == true + + /** True while the row is only the empty "Add model" draft (`omitUntouchedModelDrafts`). */ + val isUntouchedDraft: Boolean + get() = id.isBlank() && name == null && api == null && reasoning == null && thinking == null && + input == null && contextWindow == null && maxTokens == null && cost == null && extras.isEmpty() + + fun toJson(): JsonObject = buildJsonObject { + val emitted = HashSet(10) + put("id", id) + emitted += "id" + name?.let { put("name", it); emitted += "name" } + api?.let { put("api", it); emitted += "api" } + reasoning?.let { put("reasoning", JsonPrimitive(it)); emitted += "reasoning" } + thinking?.let { put("thinking", it.toJson()); emitted += "thinking" } + input?.let { list -> + put("input", buildJsonArray { list.forEach { add(JsonPrimitive(it)) } }) + emitted += "input" + } + contextWindow?.let { put("contextWindow", JsonPrimitive(it)); emitted += "contextWindow" } + maxTokens?.let { put("maxTokens", JsonPrimitive(it)); emitted += "maxTokens" } + cost?.let { put("cost", it.toJson()); emitted += "cost" } + extras.forEach { (key, value) -> if (key !in emitted) put(key, value) } + } + + companion object { + val KNOWN = setOf( + "id", "name", "api", "reasoning", "thinking", "input", "contextWindow", "maxTokens", "cost", + ) + } +} + +/** One `providers.` entry. */ +data class ProviderConfig( + val baseUrl: String? = null, + val apiKey: String? = null, + val api: String? = null, + /** `apiKey`, `none` or `oauth`; null keeps omp's default (`apiKey`). */ + val auth: String? = null, + val headers: Map? = null, + val models: List = emptyList(), + /** Keys the editor does not expose (`compat`, `modelOverrides`, …), preserved verbatim. */ + val extras: Map = emptyMap(), +) { + fun toJson(): JsonObject = buildJsonObject { + val emitted = HashSet(6) + // A fresh provider draft carries empty strings; the web writes bare + // `{api: …}` and an empty key would be a real (broken) value to omp. + baseUrl?.takeIf { it.isNotBlank() }?.let { put("baseUrl", it); emitted += "baseUrl" } + apiKey?.takeIf { it.isNotBlank() }?.let { put("apiKey", it); emitted += "apiKey" } + api?.let { put("api", it); emitted += "api" } + auth?.let { put("auth", it); emitted += "auth" } + headers?.let { map -> + put("headers", buildJsonObject { map.forEach { (key, value) -> put(key, value) } }) + emitted += "headers" + } + if (models.isNotEmpty()) { + put("models", buildJsonArray { models.forEach { add(it.toJson()) } }) + emitted += "models" + } + extras.forEach { (key, value) -> if (key !in emitted) put(key, value) } + } + + companion object { + val KNOWN = setOf("baseUrl", "apiKey", "api", "auth", "headers", "models") + } +} + +/** The whole `models.yml` document: `providers` plus any other top-level key. */ +data class ModelsFileConfig( + val providers: Map = emptyMap(), + val extras: Map = emptyMap(), + /** + * JSON paths (`providers.`, `providers..models[2]`) whose value + * is not an object. The editor cannot express them, so saving is blocked + * instead of dropping the user's content. + */ + val unparsedEntries: List = emptyList(), +) { + fun toJson(): JsonObject = buildJsonObject { + val rawProviders = extras["providers"] + if (providers.isNotEmpty() || rawProviders == null) { + put("providers", buildJsonObject { providers.forEach { (name, provider) -> put(name, provider.toJson()) } }) + } + extras.forEach { (key, value) -> if (key != "providers" || providers.isEmpty()) put(key, value) } + } + + /** + * The body the editor PUTs: blank "Add model" rows are dropped + * (`omitUntouchedModelDrafts`) so leaving an empty row never writes it. + */ + fun forSave(): ModelsFileConfig = copy( + providers = LinkedHashMap(providers.size).apply { + providers.forEach { (name, provider) -> + put(name, provider.copy(models = provider.models.filterNot { it.isUntouchedDraft })) + } + }, + ) +} + +// ── Parsing ────────────────────────────────────────────────────────────────── + +private fun JsonObject.stringOrNull(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull + +private fun JsonObject.booleanOrNull(key: String): Boolean? = + (this[key] as? JsonPrimitive)?.booleanOrNull + +private fun JsonObject.intOrNull(key: String): Int? = + (this[key] as? JsonPrimitive)?.intOrNull + +private fun JsonObject.doubleOrNull(key: String): Double? = + (this[key] as? JsonPrimitive)?.doubleOrNull + +/** `null` when the value is missing, not an array, or holds a non-string element. */ +private fun JsonObject.stringList(key: String): List? { + val array = this[key] as? JsonArray ?: return null + return array.map { element -> + (element as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull ?: return null + } +} + +/** `null` when the value is missing, not an object, or holds a non-string value. */ +private fun JsonObject.stringMap(key: String): Map? { + val root = this[key] as? JsonObject ?: return null + val result = LinkedHashMap(root.size) + for ((name, value) in root) { + val text = (value as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull ?: return null + result[name] = text + } + return result +} + +/** + * Keys of [this] that are not [known]; explicit `null`s stay there so a save + * preserves them. + */ +private fun JsonObject.extrasExcept(known: Set): MutableMap { + val result = LinkedHashMap() + forEach { (key, value) -> + if (key !in known || value == JsonNull) result[key] = value + } + return result +} + +/** + * Keeps a known key verbatim when its value has the wrong JSON shape, so a + * hand-written oddity survives an editor save instead of being silently + * dropped. Valid values are typed and therefore not duplicated. + */ +private fun JsonObject.keepRaw( + extras: MutableMap, + key: String, + converted: Any?, +) { + if (converted != null) return + val raw = this[key] ?: return + extras[key] = raw +} + +private fun parseCost(element: JsonElement?): ModelCostConfig? { + val root = element as? JsonObject ?: return null + return ModelCostConfig( + input = root.doubleOrNull("input"), + output = root.doubleOrNull("output"), + cacheRead = root.doubleOrNull("cacheRead"), + cacheWrite = root.doubleOrNull("cacheWrite"), + extras = root.extrasExcept(ModelCostConfig.KNOWN), + ) +} + +private fun parseThinking(element: JsonElement?): ThinkingConfig? { + val root = element as? JsonObject ?: return null + val efforts = root.stringList("efforts") + val effortMap = root.stringMap("effortMap") + val extras = root.extrasExcept(ThinkingConfig.KNOWN) + root.keepRaw(extras, "efforts", efforts) + root.keepRaw(extras, "effortMap", effortMap) + return ThinkingConfig( + mode = root.stringOrNull("mode"), + efforts = efforts, + defaultLevel = root.stringOrNull("defaultLevel"), + effortMap = effortMap, + extras = extras, + ) +} + +private fun parseModel(element: JsonElement): ModelConfig? { + val root = element as? JsonObject ?: return null + val thinking = parseThinking(root["thinking"]) + val input = root.stringList("input") + val cost = parseCost(root["cost"]) + val extras = root.extrasExcept(ModelConfig.KNOWN) + root.keepRaw(extras, "thinking", thinking) + root.keepRaw(extras, "input", input) + root.keepRaw(extras, "cost", cost) + return ModelConfig( + id = root.stringOrNull("id").orEmpty(), + name = root.stringOrNull("name"), + api = root.stringOrNull("api"), + reasoning = root.booleanOrNull("reasoning"), + thinking = thinking, + input = input, + contextWindow = root.intOrNull("contextWindow"), + maxTokens = root.intOrNull("maxTokens"), + cost = cost, + extras = extras, + ) +} + +private fun parseProvider( + name: String, + element: JsonElement, + unparsed: MutableList, +): ProviderConfig? { + val root = element as? JsonObject ?: return null + val modelsRoot = root["models"] as? JsonArray + val models = modelsRoot?.mapIndexedNotNull { index, model -> + parseModel(model) ?: run { + unparsed += "providers.$name.models[$index]" + null + } + } + val headers = root.stringMap("headers") + val extras = root.extrasExcept(ProviderConfig.KNOWN) + // A non-list `models` value cannot be edited, so keep it verbatim instead + // of writing an empty list over the user's YAML. + if (models == null) root.keepRaw(extras, "models", null) + root.keepRaw(extras, "headers", headers) + return ProviderConfig( + baseUrl = root.stringOrNull("baseUrl"), + apiKey = root.stringOrNull("apiKey"), + api = root.stringOrNull("api"), + auth = root.stringOrNull("auth"), + headers = headers, + models = models.orEmpty(), + extras = extras, + ) +} + +/** + * Parses the JSON the server returns for models.yml. Non-object providers or + * models cannot be edited; they are reported in + * [ModelsFileConfig.unparsedEntries] so a save is blocked instead of silently + * deleting them. + */ +fun parseModelsFile(root: JsonObject): ModelsFileConfig { + val providersRoot = root["providers"] as? JsonObject + val providers = LinkedHashMap(providersRoot?.size ?: 0) + val unparsed = ArrayList() + providersRoot?.forEach { (name, value) -> + val provider = parseProvider(name, value, unparsed) + if (provider != null) providers[name] = provider else unparsed += "providers.$name" + } + val extras = root.extrasExcept(setOf("providers")) + if (providersRoot == null) root["providers"]?.let { extras["providers"] = it } + return ModelsFileConfig(providers = providers, extras = extras, unparsedEntries = unparsed) +} + +// ── Validation (mirrors validateModelsConfig) ──────────────────────────────── + +/** + * Provider `baseUrl` rule: blank (inherit) or an http(s) URL, scheme matched + * case-insensitively. This is the editor's own client-side guard, stricter than + * `lib/omp/models-config.ts`, which only requires a non-empty `baseUrl` when + * models are defined and accepts `not-a-url`. The inline hint and + * [validateModelsConfig] both use it so an invalid value never reaches the file. + */ +fun isValidProviderBaseUrl(value: String?): Boolean { + val text = value?.trim().orEmpty() + if (text.isEmpty()) return true + val lower = text.lowercase(java.util.Locale.ROOT) + return lower.startsWith("http://") || lower.startsWith("https://") +} + +/** + * Returns the first validation error, or `null` when the file may be saved. + * Messages are the server's own (`lib/omp/models-config.ts`) so the editor + * shows what `PUT /api/models-config` would have answered, with one exception: + * the `baseUrl` format error is Android's stricter client-side guard (see + * [isValidProviderBaseUrl]); the server does not reject `not-a-url` there. + */ +fun validateModelsConfig(file: ModelsFileConfig): String? { + for ((providerName, provider) in file.providers) { + if (!isValidProviderBaseUrl(provider.baseUrl)) { + return "Provider $providerName: \"baseUrl\" must be an http:// or https:// URL." + } + val models = provider.models + if (models.isNotEmpty()) { + if (provider.baseUrl.isNullOrBlank()) { + return "Provider $providerName: \"baseUrl\" is required when defining custom models." + } + if (provider.apiKey.isNullOrBlank() && (provider.auth ?: "apiKey") != "none") { + return "Provider $providerName: \"apiKey\" is required when defining custom models unless auth is \"none\"." + } + } + for (model in models) { + if (model.id.isBlank()) { + return "Provider $providerName: model missing \"id\"" + } + if (provider.api.isNullOrBlank() && model.api.isNullOrBlank()) { + return "Provider $providerName, model ${model.id}: no \"api\" specified. Set at provider or model level." + } + if (model.contextWindow != null && model.contextWindow <= 0) { + return "Provider $providerName, model ${model.id}: invalid contextWindow" + } + if (model.maxTokens != null && model.maxTokens <= 0) { + return "Provider $providerName, model ${model.id}: invalid maxTokens" + } + val cost = model.cost ?: continue + for (key in ModelCostConfig.COST_KEYS) { + val value = when (key) { + "input" -> cost.input + "output" -> cost.output + "cacheRead" -> cost.cacheRead + else -> cost.cacheWrite + } + if (value == null || !value.isFinite()) { + return "Provider $providerName, model ${model.id}: cost.$key is required " + + "(cost needs input, output, cacheRead, and cacheWrite)" + } + } + } + } + return null +} + +/** `new-provider`, `new-provider-1`, … never colliding with an existing key. */ +fun uniqueProviderName(providers: Map, base: String = "new-provider"): String { + if (!providers.containsKey(base)) return base + var index = 1 + while (providers.containsKey("$base-$index")) index++ + return "$base-$index" +} + +// ── Editing ────────────────────────────────────────────────────────────────── + +fun ModelsFileConfig.withProvider(name: String, provider: ProviderConfig): ModelsFileConfig { + val next = LinkedHashMap(providers) + next[name] = provider + return copy(providers = next) +} + +/** Renames in place; the provider keeps its position in the list. */ +fun ModelsFileConfig.renameProvider(oldName: String, newName: String): ModelsFileConfig { + if (oldName == newName || !providers.containsKey(oldName)) return this + val next = LinkedHashMap(providers.size) + providers.forEach { (name, provider) -> next[if (name == oldName) newName else name] = provider } + return copy(providers = next) +} + +fun ModelsFileConfig.withoutProvider(name: String): ModelsFileConfig = + copy(providers = LinkedHashMap(providers).apply { remove(name) }) + +/** Appends an empty model row and returns its index. */ +fun ModelsFileConfig.addModelDraft(providerName: String): Pair { + val provider = providers[providerName] ?: ProviderConfig() + val index = provider.models.size + return withProvider(providerName, provider.copy(models = provider.models + ModelConfig())) to index +} + +fun ModelsFileConfig.withModel(providerName: String, index: Int, model: ModelConfig): ModelsFileConfig { + val provider = providers[providerName] ?: return this + if (index !in provider.models.indices) return this + val models = provider.models.toMutableList().apply { this[index] = model } + return withProvider(providerName, provider.copy(models = models)) +} + +fun ModelsFileConfig.withoutModel(providerName: String, index: Int): ModelsFileConfig { + val provider = providers[providerName] ?: return this + if (index !in provider.models.indices) return this + val models = provider.models.toMutableList().apply { removeAt(index) } + return withProvider(providerName, provider.copy(models = models)) +} + +// ── Editor option lists (web `ModelsConfig-types.ts`) ─────────────────────── + +/** `API_OPTIONS`: the API protocols omp can speak. */ +val API_PROTOCOLS: List = listOf( + "openai-completions", + "openai-responses", + "openai-codex-responses", + "azure-openai-responses", + "anthropic-messages", + "bedrock-converse-stream", + "google-generative-ai", + "google-gemini-cli", + "google-vertex", +) + +/** One "quick endpoint preset" of the provider editor. */ +data class EndpointPreset(val label: String, val baseUrl: String, val auth: String) + +/** `ENDPOINT_PRESETS`; [auth] is `none`, `apiKey`, or `keep` (leave the provider's mode). */ +val ENDPOINT_PRESETS: List = listOf( + EndpointPreset("Ollama", "http://localhost:11434/v1", "none"), + EndpointPreset("LM Studio / vLLM", "http://localhost:1234/v1", "none"), + EndpointPreset("OpenRouter", "https://openrouter.ai/api/v1", "apiKey"), + EndpointPreset("Local Proxy (2455)", "http://127.0.0.1:2455/v1", "keep"), +) + +/** Drops an all-empty cost block so the editor never writes `cost: {}` (which omp rejects). */ +fun normalizedModelCost(cost: ModelCostConfig?): ModelCostConfig? { + if (cost == null) return null + val empty = cost.input == null && cost.output == null && cost.cacheRead == null && + cost.cacheWrite == null && cost.extras.isEmpty() + return if (empty) null else cost +} + +// ── Thinking levels (`model.thinking`) ─────────────────────────────────────── + +/** `provider/model` — the key `enabledModels` and role/fallback selectors use. */ +val ModelOption.registryKey: String get() = "$provider/$modelId" + +/** The levels `ThinkingEditor` offers (`THINKING_LEVELS`). */ +val MODEL_THINKING_LEVELS: List = listOf("minimal", "low", "medium", "high", "xhigh", "max") + +/** + * Includes/excludes [level] exactly like `ThinkingEditor.setLevel`: + * - excluded: the level leaves `efforts`, its `effortMap` entry is dropped; + * - included with a blank [customEffort]: `efforts` contains it, no map entry; + * - included with a custom effort: the map carries the override. + * + * Returns `null` (the whole block is removed) when no level remains or every + * level is on with no override, matching the web's reset-to-auto rule. + */ +fun withThinkingLevel( + thinking: ThinkingConfig?, + level: String, + included: Boolean, + customEffort: String?, +): ThinkingConfig? { + if (level !in MODEL_THINKING_LEVELS) return thinking + val current = thinking ?: ThinkingConfig() + val includedLevels = LinkedHashSet(current.efforts ?: MODEL_THINKING_LEVELS) + val map = LinkedHashMap(current.effortMap.orEmpty()) + if (included) { + includedLevels.add(level) + val effort = customEffort?.trim().orEmpty() + if (effort.isEmpty()) map.remove(level) else map[level] = effort + } else { + includedLevels.remove(level) + map.remove(level) + } + val ordered = MODEL_THINKING_LEVELS.filter { includedLevels.contains(it) } + if (ordered.isEmpty() || (ordered.size == MODEL_THINKING_LEVELS.size && map.isEmpty())) return null + return current.copy( + mode = current.mode ?: "effort", + efforts = ordered, + effortMap = map.ifEmpty { null }, + ) +} + +// ── Model roles (`/api/model-roles`) ───────────────────────────────────────── +/** omp's native role names, in the web's display order. */ +val NATIVE_MODEL_ROLES: List = listOf( + "default", "smol", "slow", "vision", "plan", "designer", "commit", "tiny", "task", "advisor", +) + +/** `provider/model` plus an optional `:effort` suffix. */ +data class ModelRoleSelector(val model: String, val effort: String?) { + val wire: String get() = composeModelRoleSelector(model, effort) +} + +private val ROLE_EFFORT = Regex(":([^,:]+)$") + +/** Splits `provider/model:effort` exactly like `ModelsConfig-panels.tsx`. */ +fun parseModelRoleSelector(raw: String): ModelRoleSelector { + val effort = ROLE_EFFORT.find(raw)?.groupValues?.get(1) + val model = if (effort == null) raw else raw.replaceFirst(ROLE_EFFORT, "") + return ModelRoleSelector(model = model, effort = effort) +} + +/** Rebuilds a role selector, clearing the role when it has no model. */ +fun composeModelRoleSelector(model: String, effort: String?): String = + if (model.isEmpty()) "" else model + (effort?.takeIf { it.isNotEmpty() }?.let { ":$it" } ?: "") + +/** + * The values the role and fallback pickers offer: `provider/model` + * ([registryKey]), never the composer's `provider:model` key. The web uses the + * slash form for both dropdowns (`ModelsConfig-panels.tsx`); a colon selector is + * unresolvable for omp even though the route would store it. + */ +fun roleModelKeys(options: List): List = options.map { it.registryKey } + +// ── Fallback chains (`retry.fallbackChains`) ───────────────────────────────── + +fun addFallbackModel(chain: List, selector: String): List { + val value = selector.trim() + if (value.isEmpty() || chain.contains(value)) return chain + return chain + value +} + +fun removeFallbackModel(chain: List, selector: String): List = + chain.filterNot { it == selector } + +/** Swaps the entry at [index] with its neighbour; out-of-range is a no-op. */ +fun moveFallbackModel(chain: List, index: Int, offset: Int): List { + val target = index + offset + if (index !in chain.indices || target !in chain.indices) return chain + return chain.toMutableList().apply { + val moved = this[index] + this[index] = this[target] + this[target] = moved + } +} + +// ── Native registry (`enabledModels`, `disabledProviders`, provider order) ─── + +/** Every provider the registry panel lists: live models, connected providers and already-disabled ids. */ +fun registryProviderIds( + modelProviders: List, + connectedProviders: List, + disabledProviders: List, +): List = (modelProviders + connectedProviders + disabledProviders) + .filter { it.isNotBlank() } + .distinct() + .sorted() + +/** The stored preference first, then every remaining provider (web `orderedProviders`). */ +fun orderedRegistryProviders(order: List, all: List): List { + val known = LinkedHashMap() + order.forEach { if (it.isNotBlank()) known[it] = Unit } + all.forEach { if (it.isNotBlank()) known[it] = Unit } + return known.keys.toList() +} + +/** "Restrict selected models" on: the allow-list starts with every live model. */ +fun toggleModelAllowList(enabledModels: List, allKeys: List, enabled: Boolean): List = + if (enabled) allKeys else emptyList() + +fun toggleEnabledModel(enabledModels: List, key: String, enabled: Boolean): List = + if (enabled) { + if (enabledModels.contains(key)) enabledModels else enabledModels + key + } else { + enabledModels.filterNot { it == key } + } + +fun toggleDisabledProvider(disabledProviders: List, provider: String, disabled: Boolean): List = + if (disabled) { + if (disabledProviders.contains(provider)) disabledProviders else disabledProviders + provider + } else { + disabledProviders.filterNot { it == provider } + } + +// ── Composer visibility (`omp-composer-models`) ────────────────────────────── + +/** `null` means "every model is offered"; the first toggle materializes the full set. */ +private fun materializedVisibility(visible: Set?, all: List): Set = + visible ?: all.map { it.key }.toSet() + +fun toggleComposerModelVisibility( + visible: Set?, + all: List, + key: String, + visibleNow: Boolean, +): Set { + val next = materializedVisibility(visible, all).toMutableSet() + if (visibleNow) next.add(key) else next.remove(key) + return next +} + +fun toggleComposerProviderVisibility( + visible: Set?, + all: List, + provider: String, + visibleNow: Boolean, +): Set { + val next = materializedVisibility(visible, all).toMutableSet() + all.filter { it.provider == provider }.forEach { model -> + if (visibleNow) next.add(model.key) else next.remove(model.key) + } + return next +} + +fun visibleComposerModelCount(visible: Set?, all: List): Int = + all.count { visible == null || visible.contains(it.key) } + +// ── Save guards ────────────────────────────────────────────────────────────── + +/** + * `models.yml` has no ETag. A save therefore re-reads the file and compares it + * with the JSON the editor loaded; a difference means someone changed it + * externally and a blind PUT would overwrite that change. A failed fresh read + * counts as a conflict (the editor never saves over an unknown state). + */ +fun modelsConfigConflict(loaded: JsonObject?, fresh: JsonObject?): Boolean = + fresh == null || loaded != fresh + +/** + * Registry/fallback writes rebuild the whole `/api/omp-settings` snapshot from + * the loaded document. Without that document (or after a failed load) a write + * would replace retry/registry keys with the defaults, so it stays blocked. + */ +fun nativeSettingsWritable(settings: JsonObject?, error: String?): Boolean = + settings != null && error == null + diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/NativeSettings.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/NativeSettings.kt new file mode 100644 index 000000000..2bd8a557b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/NativeSettings.kt @@ -0,0 +1,632 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.intOrNull + +/** + * Settings categories that hold native OMP rows (web `SettingsConfig.tsx`). + * [EXTENSIONS] is the web tab `mcp` (Extensions & Tools: native MCP toggles, + * MCP server editor, Skills and Plugins). The web tab ids live in Slice A's + * `SettingsTab`. + */ +enum class NativeCategory { GENERAL, SAFETY, MODELS, INTELLIGENCE, EXTENSIONS } + +/** omp's thinking levels (`NativeSettings.defaultThinkingLevel`). */ +val THINKING_LEVELS: List = listOf("auto", "minimal", "low", "medium", "high", "xhigh", "max") + +/** `textVerbosity` choices. */ +val TEXT_VERBOSITIES: List = listOf("low", "medium", "high") + +/** `personality` choices. */ +val PERSONALITIES: List = listOf("default", "friendly", "pragmatic", "none") + +/** `tools.approvalMode` choices. */ +val APPROVAL_MODES: List = listOf("always-ask", "write", "yolo") + +/** `tools.approval.bash` choices. */ +val BASH_APPROVAL_POLICIES: List = listOf("allow", "prompt", "deny") + +/** `tools.approval.extension` choices, in the web's select order. */ +val EXTENSION_APPROVAL_POLICIES: List = listOf("prompt", "allow") + +/** `memory.backend` choices. */ +val MEMORY_BACKENDS: List = listOf("off", "local", "mnemopi", "hindsight") + +/** `mnemopi.scoping` choices, in the web's select order. */ +val MEMORY_SCOPES: List = listOf("per-project", "per-project-tagged", "global") + +/** `retry.fallbackRevertPolicy` choices. */ +val FALLBACK_REVERT_POLICIES: List = listOf("cooldown-expiry", "never") + +/** `advisor.syncBacklog` choices. */ +val ADVISOR_BACKLOGS: List = listOf("off", "1", "3", "5") + +/** Retry attempts offered by the web select (the server accepts 0–20). */ +val RETRY_ATTEMPTS: List = listOf(0, 1, 2, 3, 4, 5) + +/** omp's compaction methods in its settings-menu order (`lib/compaction-methods.ts`). */ +val COMPACTION_METHODS: List = listOf("remote", "snapcompact", "handoff", "soft", "shake") + +/** omp's effective order while `compaction.methodOrder` is unset. */ +val DEFAULT_COMPACTION_METHOD_ORDER: List = listOf("remote", "snapcompact", "handoff", "shake", "soft") + +/** + * Defaults the web settings panel shows for keys omp leaves unset (it renders + * `?? default` next to every control). These are display defaults; a key that + * is absent stays absent in the document and omp's own default applies. + */ +object NativeSettingsDefaults { + const val THINKING_LEVEL = "high" + const val AUTO_THINKING_SOURCE = "classifier" + const val TEXT_VERBOSITY = "medium" + const val PERSONALITY = "default" + const val HIDE_THINKING_BLOCK = false + const val EXTERNAL_THINKING = false + const val APPROVAL_MODE = "yolo" + const val BASH_APPROVAL = "prompt" + const val EXTENSION_APPROVAL = "prompt" + const val SHOW_STARTUP_DIAGNOSTICS = true + const val COMPACTION_ENABLED = true + const val COMPACTION_AUTO_CONTINUE = true + const val COMPACTION_MID_TURN = true + const val MEMORY_BACKEND = "mnemopi" + const val AUTOLEARN_ENABLED = true + const val AUTOLEARN_AUTO_CONTINUE = true + const val MEMORY_SCOPE = "per-project" + const val AUTO_RECALL = true + const val AUTO_RETAIN = true + const val RETRY_ENABLED = true + const val MAX_RETRIES = 2 + const val MODEL_FALLBACK = false + const val MCP_PROJECT_CONFIG = true + const val MCP_RENDER_MARKDOWN = true + const val MCP_NOTIFICATIONS = false +} + +/** + * The persisted native OMP settings as the server returns them + * (`GET /api/omp-settings` → `{path, settings}`). + * + * The server exposes only the keys it understands, but a settings file is the + * user's real `config.yml`: [root] therefore keeps every unknown key + * untouched, and every setter returns a new document instead of mutating. + */ +data class NativeSettingsDocument(val root: JsonObject = JsonObject(emptyMap())) { + + fun string(key: String): String? = (root[key] as? JsonPrimitive)?.takeIf { it.isString }?.content + + fun bool(key: String): Boolean? = (root[key] as? JsonPrimitive)?.takeUnless { it.isString }?.booleanOrNull + + fun int(key: String): Int? = (root[key] as? JsonPrimitive)?.takeUnless { it.isString }?.intOrNull + + fun array(key: String): List? = stringList(root[key]) + + fun section(name: String): JsonObject = root[name] as? JsonObject ?: JsonObject(emptyMap()) + + fun sectionString(name: String, key: String): String? = + (section(name)[key] as? JsonPrimitive)?.takeIf { it.isString }?.content + + fun sectionBool(name: String, key: String): Boolean? = + (section(name)[key] as? JsonPrimitive)?.takeUnless { it.isString }?.booleanOrNull + + fun sectionInt(name: String, key: String): Int? = + (section(name)[key] as? JsonPrimitive)?.takeUnless { it.isString }?.intOrNull + + fun withString(key: String, value: String?): NativeSettingsDocument = withValue(key, value?.let(::JsonPrimitive)) + + fun withBool(key: String, value: Boolean?): NativeSettingsDocument = withValue(key, value?.let(::JsonPrimitive)) + + /** Replaces an arbitrary top-level value (used by the registry/fallback panels). */ + fun withValue(key: String, value: JsonElement?): NativeSettingsDocument = + if (value == null) NativeSettingsDocument(JsonObject(root - key)) + else NativeSettingsDocument(JsonObject(root + (key to value))) + + fun withSectionString(name: String, key: String, value: String?): NativeSettingsDocument = + withSectionValue(name, key, value?.let(::JsonPrimitive)) + + fun withSectionBool(name: String, key: String, value: Boolean?): NativeSettingsDocument = + withSectionValue(name, key, value?.let(::JsonPrimitive)) + + fun withSectionInt(name: String, key: String, value: Int?): NativeSettingsDocument = + withSectionValue(name, key, value?.let(::JsonPrimitive)) + + /** Merges one key into a section object; `null` removes it and empties disappear. */ + fun withSectionValue(name: String, key: String, value: JsonElement?): NativeSettingsDocument { + val current = section(name) + val next = if (value == null) JsonObject(current - key) else JsonObject(current + (key to value)) + return if (next.isEmpty()) NativeSettingsDocument(JsonObject(root - name)) else withValue(name, next) + } + + /** Merges one key into an object nested inside a section (`tools.approval.bash`). */ + fun withNestedSectionString( + name: String, + sectionKey: String, + key: String, + value: String?, + ): NativeSettingsDocument { + val current = section(name)[sectionKey] as? JsonObject ?: JsonObject(emptyMap()) + val next = if (value == null) JsonObject(current - key) else JsonObject(current + (key to JsonPrimitive(value))) + return withSectionValue(name, sectionKey, next.takeIf { it.isNotEmpty() }) + } + + // --- Typed rows (web display defaults) --------------------------------- + + fun thinkingLevel(): String = string("defaultThinkingLevel") ?: NativeSettingsDefaults.THINKING_LEVEL + + fun autoThinkingSource(): String = + sectionString("providers", "autoThinkingSource") ?: NativeSettingsDefaults.AUTO_THINKING_SOURCE + + fun textVerbosity(): String = string("textVerbosity") ?: NativeSettingsDefaults.TEXT_VERBOSITY + + fun personality(): String = string("personality") ?: NativeSettingsDefaults.PERSONALITY + + fun hideThinkingBlock(): Boolean = + bool("hideThinkingBlock") ?: NativeSettingsDefaults.HIDE_THINKING_BLOCK + + fun externalThinking(): Boolean = bool("externalThinking") ?: NativeSettingsDefaults.EXTERNAL_THINKING + + fun approvalMode(): String = sectionString("tools", "approvalMode") ?: NativeSettingsDefaults.APPROVAL_MODE + + fun bashApproval(): String = + nestedString("tools", "approval", "bash") ?: NativeSettingsDefaults.BASH_APPROVAL + + fun extensionApproval(): String = + nestedString("tools", "approval", "extension") ?: NativeSettingsDefaults.EXTENSION_APPROVAL + + fun showStartupDiagnostics(): Boolean = + sectionBool("skills", "showStartupDiagnostics") ?: NativeSettingsDefaults.SHOW_STARTUP_DIAGNOSTICS + + fun compactionEnabled(): Boolean = + sectionBool("compaction", "enabled") ?: NativeSettingsDefaults.COMPACTION_ENABLED + + fun compactionAutoContinue(): Boolean = + sectionBool("compaction", "autoContinue") ?: NativeSettingsDefaults.COMPACTION_AUTO_CONTINUE + + fun compactionMidTurn(): Boolean = + sectionBool("compaction", "midTurnEnabled") ?: NativeSettingsDefaults.COMPACTION_MID_TURN + + /** + * omp's effective compaction order for this document: a valid `methodOrder` + * list (unknown ids dropped, first occurrences kept, like omp) or the web + * display default when the key is unset. + */ + fun compactionMethodOrder(): List { + val raw = section("compaction")["methodOrder"] + if (raw is JsonArray) { + val known = raw.mapNotNull { (it as? JsonPrimitive)?.takeUnless { p -> p.isString.not() }?.content } + .filter { it in COMPACTION_METHODS } + return known.distinct() + } + return DEFAULT_COMPACTION_METHOD_ORDER + } + + fun memoryBackend(): String = sectionString("memory", "backend") ?: NativeSettingsDefaults.MEMORY_BACKEND + + fun autolearnEnabled(): Boolean = + sectionBool("autolearn", "enabled") ?: NativeSettingsDefaults.AUTOLEARN_ENABLED + + fun autolearnAutoContinue(): Boolean = + sectionBool("autolearn", "autoContinue") ?: NativeSettingsDefaults.AUTOLEARN_AUTO_CONTINUE + + fun memoryScope(): String = sectionString("mnemopi", "scoping") ?: NativeSettingsDefaults.MEMORY_SCOPE + + fun autoRecall(): Boolean = sectionBool("mnemopi", "autoRecall") ?: NativeSettingsDefaults.AUTO_RECALL + + fun autoRetain(): Boolean = sectionBool("mnemopi", "autoRetain") ?: NativeSettingsDefaults.AUTO_RETAIN + + fun retryEnabled(): Boolean = sectionBool("retry", "enabled") ?: NativeSettingsDefaults.RETRY_ENABLED + + fun maxRetries(): Int = sectionInt("retry", "maxRetries") ?: NativeSettingsDefaults.MAX_RETRIES + + fun modelFallback(): Boolean = sectionBool("retry", "modelFallback") ?: NativeSettingsDefaults.MODEL_FALLBACK + + fun mcpProjectConfig(): Boolean = + sectionBool("mcp", "enableProjectConfig") ?: NativeSettingsDefaults.MCP_PROJECT_CONFIG + + fun mcpRenderMarkdown(): Boolean = + sectionBool("mcp", "renderMarkdownResults") ?: NativeSettingsDefaults.MCP_RENDER_MARKDOWN + + fun mcpNotifications(): Boolean = + sectionBool("mcp", "notifications") ?: NativeSettingsDefaults.MCP_NOTIFICATIONS + + private fun nestedString(section: String, nested: String, key: String): String? { + val inner = section(section)[nested] as? JsonObject ?: return null + return (inner[key] as? JsonPrimitive)?.takeIf { it.isString }?.content + } +} + +private fun stringList(element: JsonElement?): List? { + val array = element as? JsonArray ?: return null + return array.mapNotNull { (it as? JsonPrimitive)?.takeIf { p -> p.isString }?.content } +} + +/** + * Validation of the writable native settings, mirroring + * `lib/omp/settings-config.ts` `writeNativeSettings` (the server rejects the + * whole PUT when one value is invalid). An empty list means the document can + * be sent. + */ +fun validateNativeSettings(document: NativeSettingsDocument): List { + val root = document.root + val issues = mutableListOf() + fun fail(message: String) { + issues += message + } + + fun checkRecord(name: String, value: JsonElement?) { + if (value != null && value !is JsonObject) fail("$name must be an object") + } + + checkRecord("providers", root["providers"]) + checkRecord("advisor", root["advisor"]) + checkRecord("tools", root["tools"]) + checkRecord("retry", root["retry"]) + checkRecord("compaction", root["compaction"]) + checkRecord("memory", root["memory"]) + checkRecord("autolearn", root["autolearn"]) + checkRecord("mnemopi", root["mnemopi"]) + checkRecord("mcp", root["mcp"]) + checkRecord("skills", root["skills"]) + val tools = root["tools"] as? JsonObject + checkRecord("tools.approval", tools?.get("approval")) + if (issues.isNotEmpty()) return issues + + val skills = root["skills"] as? JsonObject + if (skills != null && skills.keys.any { it != "showStartupDiagnostics" }) fail("Unsupported skills setting") + + root["defaultThinkingLevel"]?.let { + if (!isChoice(it, THINKING_LEVELS)) fail("Invalid default thinking level") + } + root["hideThinkingBlock"]?.let { if (!isPlainBoolean(it)) fail("hideThinkingBlock must be a boolean") } + root["externalThinking"]?.let { if (!isPlainBoolean(it)) fail("externalThinking must be a boolean") } + root["textVerbosity"]?.let { + if (!isChoice(it, TEXT_VERBOSITIES)) fail("Invalid text verbosity") + } + root["personality"]?.let { + if (!isChoice(it, PERSONALITIES)) fail("Invalid personality") + } + + val providers = root["providers"] as? JsonObject + providers?.get("autoThinkingSource")?.let { + if (!isChoice(it, listOf("classifier", "vendor"))) fail("Invalid Auto thinking source") + } + + val advisor = root["advisor"] as? JsonObject + advisor?.get("enabled")?.let { if (!isPlainBoolean(it)) fail("advisor.enabled must be a boolean") } + advisor?.get("subagents")?.let { if (!isPlainBoolean(it)) fail("advisor.subagents must be a boolean") } + advisor?.get("syncBacklog")?.let { + if (!isChoice(it, ADVISOR_BACKLOGS)) fail("Invalid advisor sync backlog") + } + advisor?.get("immuneTurns")?.let { + val value = plainInt(it) + if (value == null || value < 0 || value > 20) fail("Advisor immune turns must be an integer between 0 and 20") + } + + tools?.get("approvalMode")?.let { + if (!isChoice(it, APPROVAL_MODES)) fail("Invalid approval mode") + } + val approval = tools?.get("approval") as? JsonObject + approval?.get("bash")?.let { + if (!isChoice(it, BASH_APPROVAL_POLICIES)) fail("Invalid Bash approval policy") + } + approval?.get("extension")?.let { + if (!isChoice(it, EXTENSION_APPROVAL_POLICIES)) { + fail("Invalid extension tool approval policy") + } + } + + skills?.get("showStartupDiagnostics")?.let { + if (!isPlainBoolean(it)) fail("skills.showStartupDiagnostics must be a boolean") + } + + for (key in listOf("enabledModels", "disabledProviders", "modelProviderOrder")) { + val value = root[key] ?: continue + val list = value as? JsonArray ?: run { fail("$key must contain non-empty strings"); continue } + if (list.any { !isNonBlankString(it) }) fail("$key must contain non-empty strings") + } + + val retry = root["retry"] as? JsonObject + retry?.get("enabled")?.let { if (!isPlainBoolean(it)) fail("retry.enabled must be a boolean") } + retry?.get("modelFallback")?.let { if (!isPlainBoolean(it)) fail("retry.modelFallback must be a boolean") } + retry?.get("maxRetries")?.let { + val value = plainInt(it) + if (value == null || value < 0 || value > 20) fail("Retry attempts must be an integer between 0 and 20") + } + retry?.get("fallbackRevertPolicy")?.let { + if (!isChoice(it, FALLBACK_REVERT_POLICIES)) fail("Invalid fallback revert policy") + } + (retry?.get("fallbackChains") as? JsonObject)?.forEach { (role, chain) -> + val list = chain as? JsonArray + if (role.isBlank() || list == null || list.any { !isNonBlankString(it) }) { + fail("Fallback chains require non-empty role and model selectors") + } + } + + val compaction = root["compaction"] as? JsonObject + compaction?.get("enabled")?.let { if (!isPlainBoolean(it)) fail("compaction.enabled must be a boolean") } + compaction?.get("midTurnEnabled")?.let { if (!isPlainBoolean(it)) fail("compaction.midTurnEnabled must be a boolean") } + compaction?.get("autoContinue")?.let { if (!isPlainBoolean(it)) fail("compaction.autoContinue must be a boolean") } + compaction?.get("methodOrder")?.let { + val list = it as? JsonArray + if (list == null) { + fail("Invalid compaction method order") + } else { + val methods = list.mapNotNull { item -> (item as? JsonPrimitive)?.takeIf(::isPlainString)?.content } + if (methods.size != list.size || methods.any { it !in COMPACTION_METHODS } || methods.distinct() != methods) { + fail("Invalid compaction method order") + } + } + } + compaction?.get("keepRecentTokens")?.let { + val value = plainInt(it) + if (value == null || value < 1_000 || value > 1_000_000) { + fail("Compaction retained tokens must be an integer between 1,000 and 1,000,000") + } + } + + (root["memory"] as? JsonObject)?.get("backend")?.let { + if (!isChoice(it, MEMORY_BACKENDS)) fail("Invalid memory backend") + } + (root["autolearn"] as? JsonObject)?.let { autolearn -> + autolearn["enabled"]?.let { if (!isPlainBoolean(it)) fail("autolearn.enabled must be a boolean") } + autolearn["autoContinue"]?.let { if (!isPlainBoolean(it)) fail("autolearn.autoContinue must be a boolean") } + autolearn["minToolCalls"]?.let { + val value = plainInt(it) + if (value == null || value < 0 || value > 100) { + fail("Auto-learn minimum tool calls must be an integer between 0 and 100") + } + } + } + (root["mnemopi"] as? JsonObject)?.let { mnemopi -> + mnemopi["scoping"]?.let { + if (!isChoice(it, MEMORY_SCOPES)) fail("Invalid Mnemopi memory scope") + } + mnemopi["autoRecall"]?.let { if (!isPlainBoolean(it)) fail("mnemopi.autoRecall must be a boolean") } + mnemopi["autoRetain"]?.let { if (!isPlainBoolean(it)) fail("mnemopi.autoRetain must be a boolean") } + mnemopi["noEmbeddings"]?.let { if (!isPlainBoolean(it)) fail("mnemopi.noEmbeddings must be a boolean") } + } + (root["mcp"] as? JsonObject)?.let { mcp -> + mcp["enableProjectConfig"]?.let { if (!isPlainBoolean(it)) fail("mcp.enableProjectConfig must be a boolean") } + mcp["renderMarkdownResults"]?.let { if (!isPlainBoolean(it)) fail("mcp.renderMarkdownResults must be a boolean") } + mcp["notifications"]?.let { if (!isPlainBoolean(it)) fail("mcp.notifications must be a boolean") } + mcp["notificationDebounceMs"]?.let { + val value = plainInt(it) + if (value == null || value < 0 || value > 60_000) { + fail("MCP notification debounce must be an integer between 0 and 60,000") + } + } + } + return issues +} + +private fun isPlainString(element: JsonElement): Boolean = element is JsonPrimitive && element.isString + +/** + * The server's URL rule (`new URL(...)` + protocol check): a scheme of http or + * https (case-insensitively) and a non-empty host. `new URL` strips leading and + * trailing C0 controls and spaces before parsing, so the Android rule trims the + * same ASCII whitespace and accepts `" https://host "` like the server does. + */ +@Suppress("TrimLambda") // `new URL` strips only C0 controls and spaces, not all Unicode whitespace. +private fun isHttpUrl(value: String?): Boolean { + val text = value?.trim { it <= ' ' } ?: return false + if (text.isEmpty()) return false + val uri = try { + java.net.URI(text) + } catch (_: Exception) { + return false + } + val scheme = uri.scheme?.lowercase(java.util.Locale.ROOT) + return (scheme == "http" || scheme == "https") && !uri.host.isNullOrEmpty() +} + +private fun isChoice(element: JsonElement, allowed: Collection): Boolean { + val value = (element as? JsonPrimitive)?.takeIf { it.isString }?.content + return value != null && value in allowed +} + +private fun isNonBlankString(element: JsonElement): Boolean = + (element as? JsonPrimitive)?.takeIf { it.isString }?.content?.isNotBlank() == true + +private fun isPlainBoolean(element: JsonElement): Boolean = + element is JsonPrimitive && !element.isString && element.booleanOrNull != null + +private fun plainInt(element: JsonElement): Int? = + (element as? JsonPrimitive)?.takeUnless { it.isString }?.intOrNull + +// --- Compaction method order ------------------------------------------------ + +/** + * The web's compaction-order control: checked methods in their order, then + * every unchecked method below. Toggling appends a method to the end; moving + * swaps a checked method with its checked neighbour only. + */ +fun toggleCompactionMethod(order: List, method: String, enabled: Boolean): List { + val next = order.filter { it in COMPACTION_METHODS }.distinct().toMutableList() + if (enabled) { + if (method !in next) next += method + } else { + next -= method + } + return next +} + +/** Moves [method] by [delta] positions inside the checked list; no-ops at the edge. */ +fun moveCompactionMethod(order: List, method: String, delta: Int): List { + val next = order.toMutableList() + val index = next.indexOf(method) + val target = index + delta + if (index < 0 || target !in next.indices) return next + val moved = next.removeAt(index) + next.add(target, moved) + return next +} + +/** Checked first (in order), then unchecked methods in omp's menu order. */ +fun compactionMethodRows(order: List): List> { + val checked = order.filter { it in COMPACTION_METHODS }.distinct() + val rest = COMPACTION_METHODS.filter { it !in checked } + return checked.mapIndexed { index, method -> method to index } + rest.map { it to -1 } +} + +// --- Serialized save queue -------------------------------------------------- + +/** + * The web's autosave drain (`saveNativeSettings`): edits are coalesced into + * one pending snapshot, snapshots are PUT one at a time, and a failed PUT + * discards the pending draft and keeps the last snapshot the server + * confirmed — the UI then shows the truth instead of an unsaved edit. + */ +class NativeSettingsSaveQueue(initial: NativeSettingsDocument = NativeSettingsDocument()) { + + var confirmed: NativeSettingsDocument = initial + private set + + private var draft: NativeSettingsDocument? = null + private var sent: NativeSettingsDocument? = null + + /** The snapshot the UI shows: the pending edit, else the confirmed one. */ + val current: NativeSettingsDocument get() = draft ?: confirmed + + val hasPending: Boolean get() = draft != null + + /** Records a new snapshot; returns it so the caller can update its state. */ + fun edit(next: NativeSettingsDocument): NativeSettingsDocument { + draft = next + return next + } + + /** Claims the current draft for sending (null when nothing is pending). */ + fun take(): NativeSettingsDocument? { + val next = draft + draft = null + sent = next + return next + } + + /** + * The server's canonical settings after a successful PUT. A draft created + * while that PUT was in flight stays pending; a draft that was never sent + * (a confirm without [take]) is superseded by the canonical snapshot. + */ + fun confirm(server: NativeSettingsDocument) { + confirmed = server + if (sent == null) draft = null + sent = null + } + + /** Drops the pending draft and returns the last confirmed snapshot. */ + fun fail(): NativeSettingsDocument { + draft = null + sent = null + return confirmed + } +} + +// --- MCP server JSON -------------------------------------------------------- + +/** The editor's starting text for a new server (web `newServer`). */ +val NEW_MCP_SERVER_JSON: String = """ + { + "type": "stdio", + "command": "", + "args": [] + } +""".trimIndent() + +sealed interface McpServerParse { + data class Valid(val server: JsonObject) : McpServerParse + + /** [message] is shown under the editor; a syntax error keeps the parser's wording. */ + data class Invalid(val message: String) : McpServerParse +} + +private val McpEditorJson = kotlinx.serialization.json.Json + +/** Parses the editor text: a JSON object is required (web `parse()`). */ +fun parseMcpServerJson(text: String): McpServerParse { + val element = try { + McpEditorJson.parseToJsonElement(text) + } catch (error: Exception) { + val detail = error.message?.lineSequence()?.firstOrNull()?.take(200) ?: "syntax error" + return McpServerParse.Invalid("Invalid JSON: $detail") + } + return if (element is JsonObject) McpServerParse.Valid(element) + else McpServerParse.Invalid("Server configuration must be a JSON object") +} + +/** + * Mirrors `validateMcpServer` of `lib/omp/mcp-config.ts`: the first problem as + * the server would word it, or null when `PUT /api/mcp` accepts the pair. + */ +fun validateMcpServer(name: String, server: JsonObject): String? { + if (!OMP_RESOURCE_NAME_RE.matches(name)) { + return "Server name may contain letters, numbers, dots, dashes, and underscores" + } + val type = server["type"] + if (type != null && !isChoice(type, listOf("stdio", "http", "sse"))) { + return "Server type must be stdio, http, or sse" + } + val typeName = (type as? JsonPrimitive)?.takeIf(::isPlainString)?.content + val command = (server["command"] as? JsonPrimitive)?.takeIf(::isPlainString)?.content?.trim().orEmpty() + val rawUrl = (server["url"] as? JsonPrimitive)?.takeIf(::isPlainString)?.content + val url = rawUrl?.trim().orEmpty() + val hasCommand = command.isNotEmpty() + val hasUrl = url.isNotEmpty() + if (hasCommand == hasUrl) return "Provide exactly one of command or url" + if ((typeName == null || typeName == "stdio") && !hasCommand) return "A stdio server requires a command" + if ((typeName == "http" || typeName == "sse") && !hasUrl) return "An HTTP or SSE server requires a URL" + if (hasUrl && !isHttpUrl(rawUrl)) { + return "Server URL must be an http or https URL" + } + server["args"]?.let { args -> + val list = args as? JsonArray + if (list == null || list.any { !isPlainString(it) }) return "args must be an array of strings" + } + server["env"]?.let { env -> + val record = env as? JsonObject + if (record == null || record.values.any { !isPlainString(it) }) return "env must map strings to strings" + } + server["headers"]?.let { headers -> + val record = headers as? JsonObject + if (record == null || record.values.any { !isPlainString(it) }) return "headers must map strings to strings" + } + server["cwd"]?.let { if (!isPlainString(it)) return "cwd must be a string" } + server["enabled"]?.let { if (!isPlainBoolean(it)) return "enabled must be a boolean" } + server["timeout"]?.let { + val value = plainInt(it) + if (value == null || value < 0 || value > 600_000) return "timeout must be an integer between 0 and 600000" + } + server["requestIdFormat"]?.let { + if (!isChoice(it, listOf("number", "string"))) { + return "requestIdFormat must be number or string" + } + } + return null +} + +/** + * Summary shown next to a server row: type, target and whether the stored + * config is usable (web `serverSummary`). + */ +data class McpServerSummary(val type: String, val target: String, val enabled: Boolean, val valid: Boolean) + +fun summarizeMcpServer(server: JsonObject): McpServerSummary { + val type = (server["type"] as? JsonPrimitive)?.takeIf(::isPlainString)?.content + ?.takeIf { it != "stdio" } ?: "stdio" + val command = (server["command"] as? JsonPrimitive)?.takeIf(::isPlainString)?.content?.trim().orEmpty() + val url = (server["url"] as? JsonPrimitive)?.takeIf(::isPlainString)?.content?.trim().orEmpty() + val hasCommand = command.isNotEmpty() + val hasUrl = url.isNotEmpty() + val valid = (hasCommand || hasUrl) && !(hasCommand && hasUrl) && + (if (type == "http" || type == "sse") hasUrl else hasCommand) + val args = (server["args"] as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.takeIf(::isPlainString)?.content } + val target = if (type == "http" || type == "sse") url + else (command + args.orEmpty().joinToString("") { " $it" }).trim() + val enabled = (server["enabled"] as? JsonPrimitive)?.takeUnless { it.isString }?.booleanOrNull ?: true + return McpServerSummary(type, target, enabled, valid) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/NavigationHistory.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/NavigationHistory.kt new file mode 100644 index 000000000..ddc90e27a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/NavigationHistory.kt @@ -0,0 +1,110 @@ +package io.github.kahme247.ompweb.domain + +/** + * In-app view history for navigate back/forward. + * + * Kotlin port of `lib/navigation-history.ts`. omp-web keeps its own stack + * instead of the browser History API (it replaces, never pushes, `?session=` + * URLs); on Android the system back stack already owns screen navigation, so + * the session view history is kept here in memory. Entries are main views + * only — a session, or the new-chat composer for a cwd. + */ +data class NavigationEntry( + /** Session id, or null for the new-chat (composer) view. */ + val sessionId: String?, + /** The view's cwd — the session's cwd, or the new-chat cwd. */ + val cwd: String?, +) + +data class NavigationHistory( + val entries: List = emptyList(), + /** Index of the currently displayed entry inside [entries]. */ + val index: Int = -1, +) { + val current: NavigationEntry? + get() = entries.getOrNull(index) + + val canBack: Boolean + get() = index > 0 + + val canForward: Boolean + get() = index >= 0 && index < entries.size - 1 + + /** Record a visited view; re-recording the current entry is a no-op. */ + fun push(entry: NavigationEntry): NavigationHistory { + if (entries.isEmpty()) return NavigationHistory(listOf(entry), 0) + val current = entries.getOrNull(index) + if (current != null && navigationEntriesEqual(current, entry)) return this + val kept = entries.take(index + 1).toMutableList() + kept.add(entry) + if (kept.size > NAVIGATION_HISTORY_MAX_ENTRIES) { + kept.subList(0, kept.size - NAVIGATION_HISTORY_MAX_ENTRIES).clear() + } + return NavigationHistory(kept, kept.size - 1) + } + + /** Move one entry; out-of-bounds steps return this same instance. */ + fun step(direction: Int): NavigationHistory { + val next = index + direction + if (next < 0 || next >= entries.size) return this + return copy(index = next) + } + + /** + * Drop one entry (e.g. its session no longer exists). Used against the + * neighbor about to be visited, never the current entry: removing an + * entry before the cursor shifts the cursor down so it keeps pointing at + * the same entry. + */ + fun removeAt(target: Int): NavigationHistory { + if (target < 0 || target >= entries.size) return this + val remaining = entries.filterIndexed { i, _ -> i != target } + var nextIndex = index + if (target < index) nextIndex -= 1 + return NavigationHistory(remaining, nextIndex.coerceAtMost(remaining.size - 1)) + } +} + +/** Match the browser's about:config history cap. Oldest entries drop first. */ +const val NAVIGATION_HISTORY_MAX_ENTRIES = 50 + +/** + * Session views compare by id (a session keeps its identity while its cwd + * metadata refreshes); new-chat views compare by cwd. + */ +fun navigationEntriesEqual(a: NavigationEntry, b: NavigationEntry): Boolean { + if (a.sessionId != b.sessionId) return false + if (a.sessionId != null) return true + return a.cwd == b.cwd +} + +/** Minimal structural keyboard event, testable without a DOM. */ +data class NavigationShortcutEvent( + val metaKey: Boolean = false, + val ctrlKey: Boolean = false, + val altKey: Boolean = false, + val shiftKey: Boolean = false, + val key: String = "", + val isComposing: Boolean = false, +) + +/** + * -1 = back, 1 = forward, 0 = no match. Mirrors + * `navigateShortcutDirection` (`lib/navigation-history.ts`) for the keys an + * Android hardware keyboard can produce: Alt+←/→ on Windows/Linux, Cmd+[/] + * on macOS, Ctrl+[/] accepted as a non-macOS alias, shift chords excluded. + * The web's BrowserBack/BrowserForward are browser mouse buttons; Compose + * key events never carry them. + */ +fun navigateShortcutDirection(event: NavigationShortcutEvent, isMac: Boolean): Int { + if (event.isComposing) return 0 + if ((event.metaKey || (!isMac && event.ctrlKey)) && !event.altKey && !event.shiftKey) { + if (event.key == "[") return -1 + if (event.key == "]") return 1 + } + if (!isMac && event.altKey && !event.metaKey && !event.ctrlKey && !event.shiftKey) { + if (event.key == "ArrowLeft") return -1 + if (event.key == "ArrowRight") return 1 + } + return 0 +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/NotificationHistory.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/NotificationHistory.kt new file mode 100644 index 000000000..b4a6a6dab --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/NotificationHistory.kt @@ -0,0 +1,300 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.longOrNull + +/** + * The server's notification history and the rules the Notifications tab + * mirrors from the web (`lib/notification-history.ts`, + * `components/NotificationList.tsx`, `app/api/notifications/history/route.ts`). + * + * The server owns the list. This file only defines what one entry is, how an + * untrusted entry is validated, and the local rules: a delivered event joins + * the list without a refetch, marking read is optimistic, and a stale snapshot + * never overwrites a local change that the server has not seen yet. + */ + +/** Server cap (`MAX_HISTORY`, `lib/notification-store.ts`). */ +const val NOTIFICATION_HISTORY_LIMIT = 100 + +/** `MAX_ID_LENGTH` in `app/api/notifications/history/route.ts`. */ +const val NOTIFICATION_HISTORY_MAX_ID_LENGTH = 256 + +/** `MAX_IDS` in `app/api/notifications/history/route.ts`; a PATCH above it is rejected. */ +const val NOTIFICATION_HISTORY_MAX_IDS = 100 + +/** Web clips a session name to 100 characters before showing it as a title. */ +const val NOTIFICATION_TITLE_MAX = 100 + +/** Web clips a question/error/reason detail to 180 characters. */ +const val NOTIFICATION_DETAIL_MAX = 180 + +/** Wire types the server emits today; unknown values still render (leniently). */ +object NotificationHistoryType { + const val COMPLETED = "completed" + const val INPUT = "input" + const val ERROR = "error" + const val MODEL_SWITCH = "modelSwitch" + const val TEST = "test" +} + +/** + * The event fields the history list needs. The wire event carries more + * (`dialog`, `requestId`, `aliases`); those belong to the notification + * renderer and the answer flow, not to this list. + */ +data class NotificationHistoryEvent( + val type: String, + val sessionId: String, + val sessionName: String? = null, + val detail: String? = null, + val from: String? = null, + val to: String? = null, +) + +/** + * One persisted notification. `id` is the server's replace-tag + * `:`, so a newer event of the same kind replaces the old + * entry instead of stacking. + */ +data class NotificationHistoryEntry( + val id: String, + val event: NotificationHistoryEvent, + /** Wall-clock milliseconds, as the server recorded them. */ + val at: Long, + val read: Boolean, +) + +/** True when [id] may be sent to the history route. */ +fun isValidNotificationHistoryId(id: String): Boolean = + id.isNotEmpty() && id.length <= NOTIFICATION_HISTORY_MAX_ID_LENGTH + +/** Flatten and clip display text the same way the web renders a notification. */ +fun clipNotificationText(value: String, max: Int = NOTIFICATION_DETAIL_MAX): String { + val flat = value.replace(Regex("\\s+"), " ").trim() + return if (flat.length > max) flat.take(max - 1) + "…" else flat +} + +/** + * Parse one entry from `GET /api/notifications/history` or from a live + * `notification` frame. Anything that does not carry the documented shape is + * dropped rather than guessed at (web `isEntry`). + */ +fun parseNotificationHistoryEntry(element: JsonElement?): NotificationHistoryEntry? { + val root = element as? JsonObject ?: return null + val id = root.string("id") ?: return null + if (!isValidNotificationHistoryId(id)) return null + val event = parseNotificationHistoryEvent(root["event"]) ?: return null + val at = root["at"]?.strictLong() ?: return null + val read = root["read"]?.strictBoolean() ?: return null + return NotificationHistoryEntry(id = id, event = event, at = at, read = read) +} + +/** + * Parse a live notification event (`{"type":"notification","event":{…}}`'s + * `event`) into a history entry the list can show immediately. The server + * records the same entry before delivering the frame, so [nowMs] is only used + * until the next `refresh()` replaces the list with the server's timestamps. + */ +fun notificationHistoryEntryFromEvent(event: JsonElement?, nowMs: Long): NotificationHistoryEntry? { + val parsed = parseNotificationHistoryEvent(event) ?: return null + if (parsed.type.isBlank()) return null + return NotificationHistoryEntry( + id = historyEntryId(parsed), + event = parsed, + at = nowMs, + read = false, + ) +} + +/** The replace-tag of [event], exactly like `renderNotification`'s `tag`. */ +fun historyEntryId(event: NotificationHistoryEvent): String = "${event.sessionId}:${event.type}" + +/** The server's `{entries:[…]}` answer; invalid entries are skipped, order kept. */ +fun parseNotificationHistoryEntries(root: JsonElement?): List { + val entries = (root as? JsonObject)?.get("entries") as? JsonArray ?: return emptyList() + return entries.mapNotNull(::parseNotificationHistoryEntry) +} + +/** + * A live event joins the already loaded list without a refetch: it replaces + * the entry with the same tag and goes to the front, keeping at most + * [NOTIFICATION_HISTORY_LIMIT] entries (web `notificationHistory.add`). + */ +fun mergeDeliveredNotification( + entries: List, + delivered: NotificationHistoryEntry, +): List { + val merged = ArrayList(entries.size + 1) + merged += delivered + entries.forEach { entry -> if (entry.id != delivered.id) merged += entry } + return if (merged.size > NOTIFICATION_HISTORY_LIMIT) merged.subList(0, NOTIFICATION_HISTORY_LIMIT) else merged +} + +/** Mark exactly [ids] read; unknown and already read ids are left alone. */ +fun markNotificationHistoryRead( + entries: List, + ids: Collection, +): List { + if (ids.isEmpty()) return entries + val wanted = ids.toSet() + var changed = false + val next = entries.map { entry -> + if (entry.read || entry.id !in wanted) { + entry + } else { + changed = true + entry.copy(read = true) + } + } + return if (changed) next else entries +} + +/** Remove one entry; the id is the server's key, so at most one row matches. */ +fun removeNotificationHistoryEntry( + entries: List, + id: String, +): List { + if (entries.none { it.id == id }) return entries + return entries.filterNot { it.id == id } +} + +/** Unread entries; the bell badge and the mark-all-read enablement read this. */ +fun unreadNotificationCount(entries: List): Int = entries.count { !it.read } + +/** + * The ids a PATCH may carry: unread, unique, valid and capped to the + * server's [NOTIFICATION_HISTORY_MAX_IDS]. The web sends the ids the tab shows. + */ +fun notificationReadIds(entries: List): List = + entries.asSequence() + .filter { !it.read } + .map { it.id } + .filter(::isValidNotificationHistoryId) + .distinct() + .take(NOTIFICATION_HISTORY_MAX_IDS) + .toList() + +private fun parseNotificationHistoryEvent(element: JsonElement?): NotificationHistoryEvent? { + val root = element as? JsonObject ?: return null + val type = root.string("type")?.takeIf { it.isNotBlank() } ?: return null + val sessionId = root.string("sessionId") ?: return null + return NotificationHistoryEvent( + type = type, + sessionId = sessionId, + sessionName = root.string("sessionName"), + detail = root.string("detail"), + from = root.string("from"), + to = root.string("to"), + ) +} + +private fun JsonObject.string(key: String): String? { + val primitive = this[key] as? JsonPrimitive ?: return null + if (!primitive.isString) return null + val value = primitive.content + return value.takeIf { it.isNotEmpty() } +} + +/** JSON numbers only; a stringified number is not what the route documents. */ +private fun JsonElement.strictLong(): Long? = + (this as? JsonPrimitive)?.takeIf { !it.isString }?.longOrNull + +/** JSON booleans only (`typeof value === "boolean"` in the web's `isEntry`). */ +private fun JsonElement.strictBoolean(): Boolean? = + (this as? JsonPrimitive)?.takeIf { !it.isString }?.booleanOrNull + +/** + * Stale-snapshot bookkeeping from the web store. A load's answer is applied + * only when no local change and no change request happened since it started; + * otherwise the answer is dropped and the list is fetched again once the + * pending requests settle. That is what keeps a dismissed entry from coming + * back or a mark-read from flickering off. + */ +class NotificationHistorySync { + private var changes = 0 + private var requestsInFlight = 0 + private var loads = 0 + private var reloadWhenSettled = false + + /** A local change (delivered, read, removed, cleared) happened. */ + fun localChange() { + changes++ + } + + /** Snapshot a new load; the newest token answers, older answers are ignored. */ + fun beginLoad(): LoadToken { + loads++ + return LoadToken(sequence = loads, changesAtStart = changes) + } + + fun beginRequest() { + requestsInFlight++ + } + + /** A change request failed: the server's list is the truth, so reload once idle. */ + fun requestFailed() { + reloadWhenSettled = true + } + + /** A request settled; true means the caller must reload now. */ + fun endRequest(): Boolean { + if (requestsInFlight > 0) requestsInFlight-- + if (requestsInFlight > 0 || !reloadWhenSettled) return false + reloadWhenSettled = false + return true + } + + /** True while [token] is still the newest load (an older failure keeps the newer error). */ + fun isLatest(token: LoadToken): Boolean = token.sequence == loads + + /** What to do with one load's answer. */ + fun decide(token: LoadToken): SnapshotDecision { + if (token.sequence != loads) return SnapshotDecision.Ignore + if (changes != token.changesAtStart) { + if (requestsInFlight > 0) { + reloadWhenSettled = true + return SnapshotDecision.ReloadWhenSettled + } + return SnapshotDecision.Reload + } + if (requestsInFlight > 0) { + reloadWhenSettled = true + return SnapshotDecision.ReloadWhenSettled + } + return SnapshotDecision.Apply + } + + /** + * The list belongs to another server now (sign-out, server switch): forget + * every in-flight load and request, so their answers are dropped instead + * of landing in the new installation's list. + */ + fun reset() { + changes++ + loads++ + requestsInFlight = 0 + reloadWhenSettled = false + } + + data class LoadToken(val sequence: Int, val changesAtStart: Int) +} + +/** What a refresh does with its answer (web `notificationHistory.load`). */ +sealed interface SnapshotDecision { + /** Replace the shown list with the server's. */ + data object Apply : SnapshotDecision + + /** A local change raced the load; fetch again instead of applying. */ + data object Reload : SnapshotDecision + + /** A change request is still in flight; reload when it settles. */ + data object ReloadWhenSettled : SnapshotDecision + + /** A newer load answers for this one. */ + data object Ignore : SnapshotDecision +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/Notifications.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Notifications.kt new file mode 100644 index 000000000..4b428c047 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Notifications.kt @@ -0,0 +1,668 @@ +package io.github.kahme247.ompweb.domain + +import java.security.SecureRandom +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.put + +/** + * Notification domain shared by the foreground service, the renderer and the + * settings screen. Pure: no Android types, so every decision (prefs parsing, + * tag/channel mapping, action selection per dialog shape, presence) is + * unit-tested on the JVM exactly like `lib/notification-events.ts`. + */ + +/** The four event types the server publishes (`NOTIFICATION_TYPES`). */ +enum class NotifyType(val wire: String) { + Completed("completed"), + Input("input"), + Error("error"), + ModelSwitch("modelSwitch"), + ; + + companion object { + fun fromWire(value: String?): NotifyType? = entries.firstOrNull { it.wire == value } + } +} + +/** Android notification channels; ids are stable, the renderer maps them to resources. */ +enum class NotifyChannel(val id: String) { + NeedsInput("needs_input"), + Errors("errors"), + Completed("completed"), + ModelSwitch("model_switch"), + ConnectionStatus("connection_status"), + AgentMessages("agent_messages"), +} + +/** Channel a live event belongs to (PLAN §4 "Inhalt und Aktionen"). */ +fun NotifyType.channel(): NotifyChannel = when (this) { + NotifyType.Completed -> NotifyChannel.Completed + NotifyType.Input -> NotifyChannel.NeedsInput + NotifyType.Error -> NotifyChannel.Errors + NotifyType.ModelSwitch -> NotifyChannel.ModelSwitch +} + +/** One pulse of the completed event, and of each half of the input double pulse. */ +private const val VIBRATION_SHORT_PULSE = 120L + +/** Silence between the two pulses of the input event. */ +private const val VIBRATION_GAP = 120L + +/** One long pulse of the error event. */ +private const val VIBRATION_LONG_PULSE = 600L + +/** + * The user's per-event vibration waveform, in the shape + * `NotificationChannel.vibrationPattern` (and `VibrationEffect.createWaveform`) + * expects: delay, pulse, pause, pulse, … in milliseconds. + * + * Completed = one short pulse, input = a double pulse, error = one long pulse. + * A model switch is informational and stays silent. Sending a prompt, answering + * a dialog and the "Sent"/"Failed" result updates never vibrate. + */ +fun NotifyType.vibrationPattern(): LongArray? = when (this) { + NotifyType.Completed -> longArrayOf(0, VIBRATION_SHORT_PULSE) + NotifyType.Input -> longArrayOf(0, VIBRATION_SHORT_PULSE, VIBRATION_GAP, VIBRATION_SHORT_PULSE) + NotifyType.Error -> longArrayOf(0, VIBRATION_LONG_PULSE) + NotifyType.ModelSwitch -> null +} + +/** The waveform a channel's notifications use, or null for the silent channels. */ +fun NotifyChannel.vibrationPattern(): LongArray? = when (this) { + NotifyChannel.NeedsInput -> NotifyType.Input.vibrationPattern() + NotifyChannel.Errors -> NotifyType.Error.vibrationPattern() + NotifyChannel.Completed -> NotifyType.Completed.vibrationPattern() + NotifyChannel.ModelSwitch, NotifyChannel.ConnectionStatus, NotifyChannel.AgentMessages -> null +} + +/** Whether the channel has a vibration twin; the other channels keep one stable id. */ +val NotifyChannel.supportsVibration: Boolean get() = vibrationPattern() != null + +/** + * Channel ids are immutable after the first creation, so a channel that must + * vibrate has its own twin id: the legacy (pre-1.0.2) channel ids could never + * gain a pattern. The `_v2` twin vibrates, the `_quiet` twin never does (the + * in-app "Vibrate" setting turned off, and the answer-result update). + */ +val NotifyChannel.vibratingId: String get() = if (supportsVibration) "${id}_v2" else id + +val NotifyChannel.quietId: String get() = if (supportsVibration) "${id}_quiet" else id + +/** The channel id one post uses: the vibrating twin or its quiet twin. */ +fun NotifyChannel.androidChannelId(vibrate: Boolean): String = when { + !supportsVibration -> id + vibrate -> vibratingId + else -> quietId +} + +/** + * Maps a stored channel id back to its event channel: the logical id, the + * `_v2`/`_quiet` twins and the pre-1.0.2 legacy ids all name the same channel. + */ +fun notifyChannelFor(channelId: String?): NotifyChannel? { + val value = channelId?.takeIf { it.isNotBlank() } ?: return null + return NotifyChannel.entries.firstOrNull { + value == it.id || value == it.vibratingId || value == it.quietId + } +} + +/** + * The project a session's notifications are grouped under on Android. [key] is + * the canonical project root (worktrees collapse into their repository), [label] + * is what the group summary calls it ("3 sessions in ompwebandroid"). + */ +data class NotifyProject(val key: String, val label: String) + +/** + * Group identity for a session's cwd/project root, or null when the server did + * not tell us where the session lives (the notification then stays ungrouped + * instead of landing in a made-up group). + */ +fun notifyProject(path: String?): NotifyProject? { + val value = path?.takeIf { it.isNotBlank() } ?: return null + val key = comparableProjectPath(value) + if (key.isEmpty()) return null + return NotifyProject(key = key, label = projectPathLabel(value).ifBlank { key }) +} + +/** `whenActive` wire values from `NotificationPrefs`. */ +enum class NotifyWhenActive(val wire: String) { + Toast("toast"), + System("system"), + ; + + companion object { + /** Missing or unknown values fall back to the native default, not the web toast. */ + fun fromWire(value: String?): NotifyWhenActive = + if (value == Toast.wire) Toast else System + } +} + +/** + * Per-device prefs (`omp-notify-prefs`). Native defaults follow PLAN §4: + * notifications on, completed/input/error on, model switch off, system + * notifications while active elsewhere, English. [vibrate] is this device's + * own output choice; the server ignores the extra field. + */ +data class NotifyPrefs( + val enabled: Boolean = DEFAULT_ENABLED, + val completed: Boolean = true, + val input: Boolean = true, + val error: Boolean = true, + val modelSwitch: Boolean = false, + val whenActive: NotifyWhenActive = NotifyWhenActive.System, + val locale: String = "en", + val vibrate: Boolean = true, +) { + fun isEnabled(type: NotifyType): Boolean = when (type) { + NotifyType.Completed -> completed + NotifyType.Input -> input + NotifyType.Error -> error + NotifyType.ModelSwitch -> modelSwitch + } + + fun withType(type: NotifyType, enabled: Boolean): NotifyPrefs = when (type) { + NotifyType.Completed -> copy(completed = enabled) + NotifyType.Input -> copy(input = enabled) + NotifyType.Error -> copy(error = enabled) + NotifyType.ModelSwitch -> copy(modelSwitch = enabled) + } + + fun toJson(): JsonObject = buildJsonObject { + put("enabled", enabled) + put( + "types", + buildJsonObject { + put("completed", completed) + put("input", input) + put("error", error) + put("modelSwitch", modelSwitch) + }, + ) + put("whenActive", whenActive.wire) + put("locale", locale) + put("vibrate", vibrate) + } + + companion object { + /** PLAN §4 registration defaults, not the browser's opt-in default. */ + const val DEFAULT_ENABLED = true + + val DEFAULT = NotifyPrefs() + + /** Field-by-field fallback, mirroring `parseNotificationPrefs`. */ + fun parse(value: JsonElement?): NotifyPrefs { + val root = value as? JsonObject ?: return DEFAULT + val types = root["types"] as? JsonObject + return NotifyPrefs( + enabled = (root["enabled"] as? JsonPrimitive)?.contentOrNull?.toBooleanStrictOrNull() == true, + completed = types?.bool("completed") ?: DEFAULT.completed, + input = types?.bool("input") ?: DEFAULT.input, + error = types?.bool("error") ?: DEFAULT.error, + modelSwitch = types?.bool("modelSwitch") ?: DEFAULT.modelSwitch, + whenActive = NotifyWhenActive.fromWire(root.string("whenActive")), + locale = root.string("locale") + ?.takeIf { LOCALE_PATTERN.matches(it) } + ?: DEFAULT.locale, + vibrate = root.bool("vibrate") ?: DEFAULT.vibrate, + ) + } + } +} + +private val LOCALE_PATTERN = Regex("^[A-Za-z-]{2,10}$") + +/** One question of an `ask` dialog, reduced to what a notification can answer. */ +data class NotifyQuestion( + val id: String, + val options: List = emptyList(), + val allowCustom: Boolean = true, +) + +/** The blocking extension dialog behind an `input` event. */ +data class NotifyDialog( + val requestId: String, + val method: String, + val options: List = emptyList(), + val questions: List = emptyList(), +) + +/** `sessionName` is null when the server could not read the header. */ +data class NotifyEvent( + val type: NotifyType, + val sessionId: String, + val sessionName: String? = null, + val detail: String? = null, + val from: String? = null, + val to: String? = null, + val requestId: String? = null, + val dialog: NotifyDialog? = null, +) { + /** `:` replaces an older notification of the same session/kind. */ + val tag: String get() = "$sessionId:${type.wire}" + + /** The id an answer must echo; the event carries it even without a dialog. */ + val answerRequestId: String? get() = requestId ?: dialog?.requestId +} + +/** `notification-dismiss` frame of the `notifications` channel. */ +data class NotifyDismiss(val tag: String, val requestId: String) + +/** A rendered notification, ready for `NotificationCompat`. */ +data class RenderedNotification( + val title: String, + val body: String, + val tag: String, + val channel: NotifyChannel, +) + +/** Title, body, tag and channel for one event; text matches `renderNotification`. */ +fun renderNotification(event: NotifyEvent): RenderedNotification { + val detail = event.detail?.takeIf { it.isNotBlank() }?.let { clipNotificationText(it) } + val body = when (event.type) { + NotifyType.Completed -> "Task finished." + NotifyType.Input -> detail?.let { "Waiting for your answer: $it" } ?: "Waiting for your answer." + NotifyType.Error -> detail?.let { "Run failed: $it" } ?: "Run failed." + NotifyType.ModelSwitch -> when { + event.from != null && event.to != null && detail != null -> "Switched ${event.from} → ${event.to}: $detail" + event.from != null && event.to != null -> "Switched ${event.from} → ${event.to}." + else -> detail.orEmpty() + } + } + val title = event.sessionName?.takeIf { it.isNotBlank() } + ?.let { clipNotificationText(it, NOTIFICATION_TITLE_MAX) } ?: "omp session" + return RenderedNotification(title = title, body = body, tag = event.tag, channel = event.type.channel()) +} + +/** The settings "Send test notification" entry rendered locally. */ +fun renderTestNotification(title: String, body: String): RenderedNotification = RenderedNotification( + title = title, + body = body, + tag = "ompweb:test", + channel = NotifyChannel.AgentMessages, +) + +/** + * Where one live `notification` frame goes (web `useNotifications.deliver`): + * `os` posts a system notification, `toast` stays inside the app, and an + * unknown kind is ignored. + */ +enum class NoticeRoute { None, System, InApp } + +fun noticeRoute(kind: String?): NoticeRoute = when (kind) { + "os" -> NoticeRoute.System + "toast" -> NoticeRoute.InApp + else -> NoticeRoute.None +} + +/** + * A refused system post (permission withdrawn, channel blocked, service gone) + * must still reach the user: the web falls back to its in-app toast, so the + * native app falls back to an in-app notice instead of dropping the event. + */ +fun noticeRouteAfterPost(route: NoticeRoute, posted: Boolean): NoticeRoute = + if (route == NoticeRoute.System && !posted) NoticeRoute.InApp else route + +/** + * An in-app delivery shown in the shell (web toast). It carries the session + * and the history id so a tap can open the session and mark that entry read. + * [requestId] is the dialog request the notice stands for; a dismiss frame + * closes only that request and never a newer one of the same tag. + */ +data class InAppNotice( + val id: String, + val sessionId: String, + val title: String, + val body: String, + val error: Boolean, + val requestId: String?, +) { + /** Newest first, one notice per `tag` (= [id]), like the web's toast ids. */ + fun addTo(current: List, limit: Int): List = + (listOf(this) + current.filterNot { it.id == id }).take(limit) + + /** True when [tag]/[requestId] of a dismiss frame names this notice. */ + fun matches(tag: String, requestId: String): Boolean = id == tag && this.requestId == requestId +} + +fun inAppNotice(event: NotifyEvent): InAppNotice { + val rendered = renderNotification(event) + return InAppNotice( + id = rendered.tag, + sessionId = event.sessionId, + title = rendered.title, + body = rendered.body, + error = event.type == NotifyType.Error, + requestId = event.answerRequestId, + ) +} + +/** One answer value accepted by `POST /api/notifications/answer`. */ +sealed interface NotifyAnswer { + fun toJson(): JsonObject + + data class Value(val value: String) : NotifyAnswer { + override fun toJson(): JsonObject = buildJsonObject { put("value", value) } + } + + data class Confirmed(val confirmed: Boolean) : NotifyAnswer { + override fun toJson(): JsonObject = buildJsonObject { put("confirmed", confirmed) } + } + + data class Answers(val answers: List) : NotifyAnswer { + override fun toJson(): JsonObject = buildJsonObject { + put("answers", buildJsonArray { answers.forEach { add(it.toJson()) } }) + } + } +} + +/** Parses the pre-built answer a notification button carries (one variant, nothing else). */ +fun parseNotifyAnswer(json: String?): NotifyAnswer? { + if (json.isNullOrBlank()) return null + val root = try { + Json.parseToJsonElement(json) as? JsonObject + } catch (_: Exception) { + null + } ?: return null + val keys = root.keys + if (keys.size != 1) return null + return when (keys.first()) { + "value" -> root.string("value")?.let { NotifyAnswer.Value(it) } + "confirmed" -> root.bool("confirmed")?.let { NotifyAnswer.Confirmed(it) } + "answers" -> { + val entries = root["answers"] as? JsonArray ?: return null + if (entries.isEmpty()) return null + val answers = entries.map { entry -> + val answer = entry as? JsonObject ?: return null + val id = answer.string("id")?.takeIf { it.isNotEmpty() } ?: return null + AskAnswer( + id = id, + selectedOptions = answer.stringList("selectedOptions"), + customInput = answer.string("customInput"), + ) + } + NotifyAnswer.Answers(answers) + } + else -> null + } +} + +/** Wire form of a per-question answer (`RpcAskDialogAnswer`). */ +private fun AskAnswer.toJson(): JsonObject = buildJsonObject { + put("id", id) + put("selectedOptions", buildJsonArray { selectedOptions.forEach { add(JsonPrimitive(it)) } }) + customInput?.let { put("customInput", it) } +} + +/** A notification action button (Yes/No or one dialog option). */ +data class NotifyActionButton(val label: String, val answer: NotifyAnswer) + +/** Where typed notification text goes. */ +sealed interface ReplyTarget { + /** `input` dialog: `{value}`. */ + data object Input : ReplyTarget + + /** One `ask` question without options: `{answers:[{id, selectedOptions:[], customInput}]}`. */ + data class Ask(val questionId: String) : ReplyTarget + + /** Completed event: a regular follow-up prompt through `POST /api/agent/{id}`. */ + data object FollowUp : ReplyTarget +} + +data class NotifyReplyAction(val target: ReplyTarget, val label: String) + +/** + * What a notification offers; tapping it always opens the session. + * + * PLAN §4: confirm → Yes/No, select and single-question ask with ≤3 options → + * option buttons, input / free-text ask → Reply, completed → Reply, everything + * else only Open. Android allows at most three actions, so a fourth choice or + * a second ask question omits the actions entirely instead of dropping one. + */ +data class NotifyActionPlan( + val buttons: List = emptyList(), + val reply: NotifyReplyAction? = null, +) + +private const val MAX_ACTION_OPTIONS = 3 + +fun notificationActions(event: NotifyEvent, replyLabel: String): NotifyActionPlan = when (event.type) { + NotifyType.Completed -> + if (event.sessionId.isNotBlank()) { + NotifyActionPlan(reply = NotifyReplyAction(ReplyTarget.FollowUp, replyLabel)) + } else { + NotifyActionPlan() + } + + NotifyType.Error, NotifyType.ModelSwitch -> NotifyActionPlan() + + NotifyType.Input -> { + val dialog = event.dialog + if (dialog == null || event.answerRequestId == null) { + NotifyActionPlan() + } else { + when (dialog.method) { + "confirm" -> NotifyActionPlan( + buttons = listOf( + NotifyActionButton("Yes", NotifyAnswer.Confirmed(true)), + NotifyActionButton("No", NotifyAnswer.Confirmed(false)), + ), + ) + "select" -> NotifyActionPlan( + buttons = dialog.options + .takeIf { it.size in 1..MAX_ACTION_OPTIONS } + ?.map { NotifyActionButton(it, NotifyAnswer.Value(it)) } + .orEmpty(), + ) + "input" -> NotifyActionPlan(reply = NotifyReplyAction(ReplyTarget.Input, replyLabel)) + "ask" -> askActions(dialog, replyLabel) + // `editor` needs the full editor; `open_url` never carries a dialog. + else -> NotifyActionPlan() + } + } + } +} + +private fun askActions(dialog: NotifyDialog, replyLabel: String): NotifyActionPlan { + val question = dialog.questions.singleOrNull() ?: return NotifyActionPlan() + if (question.options.isNotEmpty()) { + if (question.options.size > MAX_ACTION_OPTIONS) return NotifyActionPlan() + return NotifyActionPlan( + buttons = question.options.map { option -> + NotifyActionButton( + option, + NotifyAnswer.Answers(listOf(AskAnswer(question.id, listOf(option), null))), + ) + }, + ) + } + // An ask option "Other" is free text for exactly this question. + return if (question.allowCustom) { + NotifyActionPlan(reply = NotifyReplyAction(ReplyTarget.Ask(question.id), replyLabel)) + } else { + NotifyActionPlan() + } +} + +/** Response for typed text, or null when the text belongs to an agent prompt. */ +fun replyAnswer(target: ReplyTarget, text: String): NotifyAnswer? = when (target) { + ReplyTarget.Input -> NotifyAnswer.Value(text) + is ReplyTarget.Ask -> NotifyAnswer.Answers(listOf(AskAnswer(target.questionId, emptyList(), text))) + ReplyTarget.FollowUp -> null +} + +/** A reply typed on the lock screen is untrusted: cap it like a prompt would be. */ +const val MAX_REPLY_LENGTH = 4_000 + +/** + * The Reply text of a notification action: trimmed, blank rejected, capped to + * [MAX_REPLY_LENGTH] characters (a reply is copied into a prompt/answer, so an + * unbounded paste would bloat the request and the agent's context). + */ +fun sanitizeReplyText(raw: CharSequence?): String? = + raw?.toString() + ?.trim() + ?.takeIf { it.isNotEmpty() } + ?.let { if (it.length > MAX_REPLY_LENGTH) it.take(MAX_REPLY_LENGTH) else it } + +/** `{clientId, deviceId, visible, sessionId, seq}` of `POST /api/notifications/presence`. */ +data class PresenceReport( + val clientId: String, + val deviceId: String, + val visible: Boolean, + val sessionId: String?, + val seq: Long, +) + +/** The app is visible only while it is in the foreground and a chat is on screen. */ +data class PresenceVisibility(val visible: Boolean, val sessionId: String?) + +/** + * The chat the user is actually looking at. The selection alone is not + * enough: on a phone "back to the list" keeps the selected session while the + * detail pane is gone, and an open overlay covers the chat. + */ +fun visibleChatSession( + selectedSessionId: String?, + detailPaneVisible: Boolean, + coveredByOverlay: Boolean, +): String? = selectedSessionId?.takeIf { it.isNotBlank() && detailPaneVisible && !coveredByOverlay } + +fun resolvePresence(foreground: Boolean, visibleSessionId: String?): PresenceVisibility = + if (foreground && !visibleSessionId.isNullOrBlank()) { + PresenceVisibility(visible = true, sessionId = visibleSessionId) + } else { + PresenceVisibility(visible = false, sessionId = null) + } + +/** What one settings snapshot means for the foreground service and the registration. */ +sealed interface NotifyRuntimeAction { + /** The first DataStore emission has not arrived; decide nothing (never stop a restart). */ + data object Idle : NotifyRuntimeAction + + /** Start the service and send the current prefs. */ + data object Start : NotifyRuntimeAction + + /** + * Stop the service. [syncDevice] is true when a server is configured and + * signed in, so the master switch being off reaches the server too. + */ + data class Stop(val syncDevice: Boolean) : NotifyRuntimeAction +} + +/** + * The single decision point for the service: enabled + signed in + an origin + * starts it; everything else stops it. An unloaded snapshot must not stop a + * service the system just restarted, and a configured server must still learn + * about `enabled:false` (otherwise it keeps routing to this device). + */ +fun notificationRuntimeAction( + loaded: Boolean, + hasOrigin: Boolean, + enabled: Boolean, + signedIn: Boolean, +): NotifyRuntimeAction = when { + !loaded -> NotifyRuntimeAction.Idle + hasOrigin && enabled && signedIn -> NotifyRuntimeAction.Start + else -> NotifyRuntimeAction.Stop(syncDevice = hasOrigin && signedIn) +} + +private val NOTIFICATION_ID_PATTERN = Regex("^[A-Za-z0-9_-]{8,64}$") + +/** Client and device ids the server accepts (`isValidNotificationId`). */ +fun isValidNotificationId(value: String?): Boolean = value != null && NOTIFICATION_ID_PATTERN.matches(value) + +private val random = SecureRandom() + +/** A fresh server-accepted id (24 random bytes, base64url without padding). */ +fun randomNotificationId(): String { + val bytes = ByteArray(24).also(random::nextBytes) + return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(bytes) +} + +/** `PUT /api/notifications/devices` identity. */ +data class NotifyIdentity(val deviceId: String, val clientId: String) + +/** Parses one `notification` frame's `event`; null for unknown/malformed frames. */ +fun parseNotifyEvent(value: JsonElement?): NotifyEvent? { + val root = value as? JsonObject ?: return null + val type = NotifyType.fromWire(root.string("type")) ?: return null + val sessionId = root.string("sessionId") ?: return null + val dialog = parseNotifyDialog(root["dialog"]) + return NotifyEvent( + type = type, + sessionId = sessionId, + sessionName = root.string("sessionName"), + detail = root.string("detail"), + from = root.string("from"), + to = root.string("to"), + requestId = root.string("requestId")?.takeIf { it.isNotEmpty() }, + dialog = dialog, + ) +} + +fun parseNotifyDialog(value: JsonElement?): NotifyDialog? { + val root = value as? JsonObject ?: return null + val requestId = root.string("requestId")?.takeIf { it.isNotEmpty() } ?: return null + val method = root.string("method")?.takeIf { it.isNotEmpty() } ?: return null + val options = root.stringList("options", strict = true) + val questions = (root["questions"] as? JsonArray)?.mapNotNull { parseNotifyQuestion(it) }.orEmpty() + return NotifyDialog( + requestId = requestId, + method = method, + options = options, + questions = questions, + ) +} + +private fun parseNotifyQuestion(value: JsonElement): NotifyQuestion? { + val root = value as? JsonObject ?: return null + val id = root.string("id")?.takeIf { it.isNotEmpty() } ?: return null + return NotifyQuestion( + id = id, + options = root.stringList("options", strict = true), + // The server sends `allowCustom: true` unless omp forbids "Other". + allowCustom = root.bool("allowCustom") != false, + ) +} + +/** Parses a `notification-dismiss` frame; the server always sends all three ids. */ +fun parseNotifyDismiss(value: JsonElement?): NotifyDismiss? { + val root = value as? JsonObject ?: return null + val tag = root.string("tag")?.takeIf { it.isNotEmpty() } ?: return null + root.string("sessionId")?.takeIf { it.isNotEmpty() } ?: return null + val requestId = root.string("requestId")?.takeIf { it.isNotEmpty() } ?: return null + return NotifyDismiss(tag, requestId) +} + +/** + * Number of running agents from a `running` frame, or null when the frame + * carries no id list (the server omits it on some transitions). + */ +fun runningSessionCount(value: JsonObject): Int? = (value["runningSessionIds"] as? JsonArray)?.size + +/** `seq` only grows, even across process restarts, so the server never drops a newer report. */ +fun nextPresenceSeq(previous: Long, nowMillis: Long): Long = + if (nowMillis > previous) nowMillis else previous + 1 + +private fun JsonObject.string(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf { it !is JsonNull }?.contentOrNull + +/** Only a JSON boolean counts; stringly-typed legacy values fall back to defaults. */ +private fun JsonObject.bool(key: String): Boolean? = + (this[key] as? JsonPrimitive)?.takeIf { !it.isString }?.booleanOrNull + +private fun JsonObject.stringList(key: String, strict: Boolean = false): List { + val array = this[key] as? JsonArray ?: return emptyList() + val values = array.mapNotNull { (it as? JsonPrimitive)?.takeIf { primitive -> primitive.isString }?.content } + return if (strict && values.size != array.size) emptyList() else values +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/OAuthLogin.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/OAuthLogin.kt new file mode 100644 index 000000000..bf322fc71 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/OAuthLogin.kt @@ -0,0 +1,319 @@ +package io.github.kahme247.ompweb.domain + +import java.net.URLDecoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.intOrNull + +/** + * The OAuth login flow of `GET /api/auth/login/{provider}` as a pure state + * machine, ported from `ModelsConfig.tsx` (`OAuthDetail`) plus the Android-specific + * loopback detection. + * + * The server streams SSE frames: `auth`, `device_code`, `prompt_request`, + * `select_request`, `progress`, `success`, `error`, `cancelled` + * (`app/api/auth/login/[provider]/route.ts`). The user's answer goes back with + * `POST /api/auth/login/{provider} {token, code}`; [OAuthLoginMachine] decides + * which input the current phase accepts and when the flow is terminal. + */ + +/** One option of a `select_request` frame. */ +data class OAuthSelectOption(val id: String, val label: String) + +/** One parsed SSE frame of the login stream. */ +sealed interface OAuthFrame { + /** The provider's authorization URL plus the token the answer must carry. */ + data class Auth(val url: String, val instructions: String?, val token: String) : OAuthFrame + + /** Device-code providers: show [userCode], open [verificationUri], poll happens server-side. */ + data class DeviceCode( + val userCode: String, + val verificationUri: String, + val intervalSeconds: Int?, + val expiresInSeconds: Int?, + ) : OAuthFrame + + /** The provider asks for a pasted code or redirect URL. */ + data class PromptRequest(val message: String, val placeholder: String?, val token: String) : OAuthFrame + + /** The provider asks the user to pick one of several accounts/organizations. */ + data class SelectRequest( + val message: String, + val options: List, + val token: String, + ) : OAuthFrame + + /** Progress text from omp (`notify` frames). */ + data class Progress(val message: String) : OAuthFrame + + data object Success : OAuthFrame + + data class Failed(val message: String) : OAuthFrame + + data object Cancelled : OAuthFrame +} + +private val oauthJson = Json { + ignoreUnknownKeys = true + isLenient = true +} + +private fun JsonObject.string(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull + +private fun JsonObject.int(key: String): Int? = (this[key] as? JsonPrimitive)?.intOrNull + +/** + * Parses one `data:` payload of the login stream. Malformed JSON, unknown + * frame types and frames missing their required field are dropped (`null`), so + * a broken frame never tears the flow down — exactly like the web handler. + */ +fun parseOAuthFrame(data: String): OAuthFrame? { + val root = try { + oauthJson.parseToJsonElement(data) + } catch (_: Exception) { + return null + } as? JsonObject ?: return null + return when (root.string("type")) { + "auth" -> { + val url = root.string("url")?.takeIf { it.isNotBlank() } ?: return null + OAuthFrame.Auth( + url = url, + instructions = root.string("instructions")?.takeIf { it.isNotBlank() }, + token = root.string("token").orEmpty(), + ) + } + "device_code" -> { + val userCode = root.string("userCode")?.takeIf { it.isNotBlank() } ?: return null + val uri = root.string("verificationUri")?.takeIf { it.isNotBlank() } ?: return null + OAuthFrame.DeviceCode( + userCode = userCode, + verificationUri = uri, + intervalSeconds = root.int("intervalSeconds"), + expiresInSeconds = root.int("expiresInSeconds"), + ) + } + "prompt_request" -> OAuthFrame.PromptRequest( + message = root.string("message")?.takeIf { it.isNotBlank() } ?: return null, + placeholder = root.string("placeholder")?.takeIf { it.isNotBlank() }, + token = root.string("token").orEmpty(), + ) + "select_request" -> { + val options = (root["options"] as? JsonArray).orEmpty().mapNotNull { element -> + val option = element as? JsonObject ?: return@mapNotNull null + val id = option.string("id")?.takeIf { it.isNotBlank() } ?: return@mapNotNull null + OAuthSelectOption(id = id, label = option.string("label")?.takeIf { it.isNotBlank() } ?: id) + } + OAuthFrame.SelectRequest( + message = root.string("message")?.takeIf { it.isNotBlank() } ?: return null, + options = options, + token = root.string("token").orEmpty(), + ) + } + "progress" -> OAuthFrame.Progress(root.string("message").orEmpty()) + "success" -> OAuthFrame.Success + "error" -> OAuthFrame.Failed(root.string("message").orEmpty()) + "cancelled" -> OAuthFrame.Cancelled + else -> null + } +} + +/** What the login panel renders. Mirrors the web's `OAuthLoginState` union. */ +sealed interface OAuthLoginState { + data object Idle : OAuthLoginState + + /** The stream is being opened; omp has not answered yet. */ + data object Connecting : OAuthLoginState + + data class Auth(val url: String, val instructions: String?, val token: String) : OAuthLoginState + + data class DeviceCode( + val userCode: String, + val verificationUri: String, + val intervalSeconds: Int?, + val expiresInSeconds: Int?, + ) : OAuthLoginState + + data class Prompt(val message: String, val placeholder: String?, val token: String) : OAuthLoginState + + data class Select( + val message: String, + val options: List, + val token: String, + ) : OAuthLoginState + + /** The answer was posted and the server is verifying it. */ + data object Submitting : OAuthLoginState + + data class Progress(val message: String) : OAuthLoginState + + data object Success : OAuthLoginState + + /** [message] is the server's text; blank means "show the generic failure". */ + data class Error(val message: String) : OAuthLoginState + + /** The stream ended without a terminal frame (server restart, network loss). */ + data object TransportLost : OAuthLoginState +} + +/** Whether the flow still owns a stream / expects user interaction. */ +val OAuthLoginState.isWorking: Boolean + get() = when (this) { + OAuthLoginState.Idle, OAuthLoginState.Success, is OAuthLoginState.Error, OAuthLoginState.TransportLost -> false + else -> true + } + +/** The value the panel's paste field submits (the token the frame carried). */ +val OAuthLoginState.submitToken: String? + get() = when (this) { + is OAuthLoginState.Auth -> token + is OAuthLoginState.Prompt -> token + is OAuthLoginState.Select -> token + else -> null + } + +/** + * The login flow's transitions. Immutable so a view model can hand it to the + * UI without copies of mutable state; terminal states ignore later frames the + * same way the web closes its `EventSource` on success/error. + */ +data class OAuthLoginMachine(val state: OAuthLoginState = OAuthLoginState.Idle) { + + fun start(): OAuthLoginMachine = copy(state = OAuthLoginState.Connecting) + + /** Cancel is local: closing the stream makes the server dispose its child. */ + fun cancel(): OAuthLoginMachine = copy(state = OAuthLoginState.Idle) + + /** The user (or a Select row) posted an answer; the server verifies it now. */ + fun submitting(): OAuthLoginMachine = + if (state.submitToken == null) this else copy(state = OAuthLoginState.Submitting) + + fun onFrame(frame: OAuthFrame): OAuthLoginMachine = when (frame) { + is OAuthFrame.Auth -> + if (isTerminal()) this else copy(state = OAuthLoginState.Auth(frame.url, frame.instructions, frame.token)) + is OAuthFrame.DeviceCode -> + if (isTerminal()) this + else copy( + state = OAuthLoginState.DeviceCode( + userCode = frame.userCode, + verificationUri = frame.verificationUri, + intervalSeconds = frame.intervalSeconds, + expiresInSeconds = frame.expiresInSeconds, + ), + ) + is OAuthFrame.PromptRequest -> + if (isTerminal()) this + else copy(state = OAuthLoginState.Prompt(frame.message, frame.placeholder, frame.token)) + is OAuthFrame.SelectRequest -> + if (isTerminal()) this + else copy(state = OAuthLoginState.Select(frame.message, frame.options, frame.token)) + is OAuthFrame.Progress -> + if (isTerminal()) this else copy(state = OAuthLoginState.Progress(frame.message)) + OAuthFrame.Success -> copy(state = OAuthLoginState.Success) + is OAuthFrame.Failed -> copy(state = OAuthLoginState.Error(frame.message)) + // A server-side cancel only happens for a flow this panel owns; the + // user's own Cancel already moved to Idle before the frame could arrive. + OAuthFrame.Cancelled -> copy(state = OAuthLoginState.Idle) + } + + /** `POST` refused the answer (expired token, wrong provider, network error). */ + fun submitFailed(message: String): OAuthLoginMachine = copy(state = OAuthLoginState.Error(message)) + + /** The SSE connection ended; settled screens (success, error) stay as they are. */ + fun streamEnded(): OAuthLoginMachine = when (state) { + OAuthLoginState.Success, is OAuthLoginState.Error -> this + else -> copy(state = OAuthLoginState.TransportLost) + } + + /** 401/403 from the stream request itself: the panel is not signed in. */ + fun streamFailed(message: String): OAuthLoginMachine = + if (state == OAuthLoginState.Success) this else copy(state = OAuthLoginState.Error(message)) + + private fun isTerminal(): Boolean = + state == OAuthLoginState.Success || state is OAuthLoginState.Error || state == OAuthLoginState.TransportLost +} + +/** A callback target that points at the phone's own loopback interface. */ +data class LoopbackCallback(val host: String, val port: Int?) + +private val IPV4 = Regex("""^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$""") + +/** `localhost`, `*.localhost`, `127.0.0.0/8`, `0.0.0.0` and `::1`. */ +fun isLoopbackHost(host: String): Boolean { + val normalized = host.trim().lowercase().removePrefix("[").removeSuffix("]").substringBefore('%') + if (normalized.isEmpty()) return false + if (normalized == "localhost" || normalized.endsWith(".localhost")) return true + if (normalized == "::1" || normalized == "::" || normalized == "0.0.0.0") return true + val match = IPV4.matchEntire(normalized) ?: return false + val octets = match.groupValues.drop(1).map { it.toIntOrNull() ?: return false } + if (octets.any { it > 255 }) return false + // 127.0.0.0/8 is the whole IPv4 loopback block; 0.0.0.0 is "this host". + return octets[0] == 127 || octets.all { it == 0 } +} + +/** + * Returns the loopback target of [url] when the phone itself cannot complete + * the flow, or `null` for a normal public URL. + * + * Two shapes count: + * - the URL's own host is loopback (`http://localhost:1455/auth/callback?code=…`); + * - the URL's `redirect_uri`/`redirect_url` query parameter is loopback, i.e. + * the provider will send the browser to a server that runs on the PC. + * + * The web UI can open both in a browser on the same machine; a phone would + * open its own loopback and land on a dead port, so the panel explains that + * the flow must be finished on the PC and accepts the pasted redirect URL. + */ +fun loopbackCallbackTarget(url: String): LoopbackCallback? { + val trimmed = url.trim() + if (trimmed.isEmpty()) return null + val schemeEnd = trimmed.indexOf("://") + if (schemeEnd <= 0) return null + val afterScheme = trimmed.substring(schemeEnd + 3) + val authority = afterScheme.substringBefore('/').substringBefore('?').substringBefore('#') + if (authority.isEmpty()) return null + val ownTarget = parseHostPort(authority.substringAfterLast('@')) + if (ownTarget != null && isLoopbackHost(ownTarget.host)) return ownTarget + + val query = afterScheme.substringAfter('?', "").substringBefore('#') + for (pair in query.split('&')) { + val separator = pair.indexOf('=') + if (separator <= 0) continue + val key = pair.substring(0, separator) + if (key != "redirect_uri" && key != "redirect_url" && key != "callback") continue + val decoded = decodeQueryValue(pair.substring(separator + 1)) ?: continue + val nested = loopbackCallbackTarget(decoded) ?: continue + return nested + } + return null +} + +private fun decodeQueryValue(raw: String): String? = try { + URLDecoder.decode(raw.replace("+", "%2B"), "UTF-8") +} catch (_: Exception) { + null +} + +private fun parseHostPort(authority: String): LoopbackCallback? { + if (authority.isEmpty()) return null + if (authority.startsWith("[")) { + val close = authority.indexOf(']') + if (close < 0) return null + val host = authority.substring(1, close) + val portText = authority.substring(close + 1).removePrefix(":") + return LoopbackCallback(host, portText.toIntOrNull()) + } + val host = authority.substringBefore(':') + val portText = authority.substringAfter(':', "") + return LoopbackCallback(host, portText.toIntOrNull()) +} + +/** The loopback target of the current phase, when the phone cannot complete it. */ +fun OAuthLoginState.loopbackTarget(): LoopbackCallback? = when (this) { + is OAuthLoginState.Auth -> loopbackCallbackTarget(url) + is OAuthLoginState.DeviceCode -> loopbackCallbackTarget(verificationUri) + else -> null +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/PathLabels.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/PathLabels.kt new file mode 100644 index 000000000..05fa2fdaa --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/PathLabels.kt @@ -0,0 +1,15 @@ +package io.github.kahme247.ompweb.domain + +/** + * Final folder name of a path, portable across `/` and `\` separators. + * Port of `projectLabel`/`displayCwd` in `components/SessionSidebar-helpers.ts`. + */ +fun projectPathLabel(projectPath: String): String { + val trimmed = projectPath.trimEnd('/', '\\') + val index = maxOf(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\')) + return if (index >= 0) trimmed.substring(index + 1) else trimmed +} + +/** Substitute the home dir prefix with `~` (no path truncation). */ +fun displayCwd(cwd: String, home: String?): String = + if (!home.isNullOrEmpty() && cwd.startsWith(home)) "~" + cwd.substring(home.length) else cwd diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ProjectLabels.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ProjectLabels.kt new file mode 100644 index 000000000..cc8974fa8 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ProjectLabels.kt @@ -0,0 +1,7 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.ManagedProject + +/** Display label for a project: the alias, else the final folder name. */ +fun projectLabel(project: ManagedProject): String = + project.alias?.takeIf { it.isNotBlank() } ?: projectPathLabel(project.path) diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ProjectOrdering.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ProjectOrdering.kt new file mode 100644 index 000000000..364b95132 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ProjectOrdering.kt @@ -0,0 +1,127 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.ManagedProject +import io.github.kahme247.ompweb.net.SessionSummary + +/** + * Pure ordering/grouping helpers shared by the sidebar. + * + * Kotlin port of `lib/project-ordering.ts` (plus `comparableProjectPath` from + * `lib/comparable-path.ts` and `workspaceKeyOf` from + * `lib/workspace-memory.ts`). All keys are canonical projectRoot paths + * (worktrees collapse into their main repo via the server), so worktree + * sessions group under their project. + * + * The project list is ordered by when each project was added (addedAt desc = + * most recently added first), NOT by session activity: activity changes on + * every session refresh and would make project rows jump around constantly. + * Session-discovered projects (no addedAt) follow registered ones in path + * order, which is also stable. + */ + +private val WINDOWS_ABSOLUTE_RE = Regex("""^[a-zA-Z]:[\\/]""") +private val UNC_PREFIX_RE = Regex("^//") + +/** + * Case-insensitive comparable form on Windows-form paths (NTFS is + * case-insensitive); case-sensitive for POSIX-form paths. Windows-ness is + * decided by the path's own form, exactly like `lib/comparable-path.ts`. + */ +fun comparableProjectPath(value: String): String { + val normalized = value.replace('\\', '/') + val windowsForm = WINDOWS_ABSOLUTE_RE.containsMatchIn(normalized) || UNC_PREFIX_RE.containsMatchIn(normalized) + val collapsed = Regex("/{2,}").replace(normalized, "/").trimEnd('/') + return if (windowsForm) collapsed.lowercase() else collapsed +} + +/** The bucket a session belongs to: projectKey, else projectRoot, else cwd. */ +fun workspaceKeyOf(session: SessionSummary): String? = + session.projectKey ?: session.projectRoot ?: session.cwd + +/** + * Sort projects by most-recently-added (addedAt desc), manual order first, + * then by path for a deterministic order. Projects without addedAt + * (session-discovered) always sort below registered ones. The order never + * depends on session activity. + */ +fun sortManagedProjects(projects: List): List = projects.sortedWith { a, b -> + val aManual = a.sortOrder != null + val bManual = b.sortOrder != null + when { + aManual != bManual -> if (aManual) -1 else 1 + aManual && bManual && a.sortOrder != b.sortOrder -> a.sortOrder!! - b.sortOrder!! + else -> { + val aHas = a.addedAt != null + val bHas = b.addedAt != null + when { + aHas != bHas -> if (aHas) -1 else 1 + aHas && bHas -> { + val byAdded = b.addedAt!!.compareTo(a.addedAt!!) + if (byAdded != 0) byAdded else a.path.compareTo(b.path) + } + else -> a.path.compareTo(b.path) + } + } + } +} + +/** Running/unread/exited session counts for one project row. */ +data class ProjectActivity( + val running: Int = 0, + val unread: Int = 0, + val exited: Int = 0, +) + +/** + * Running/unread/exit session counts per project. Keys are the case-folded + * comparable form of the project root, so casing-only differences + * (Windows/NTFS) still resolve — look up with + * `comparableProjectPath(project.path)`. + */ +fun projectActivityCounts( + sessions: List, + runningIds: Collection, + unreadIds: Collection, + exitedIds: Collection = emptyList(), +): Map { + val running = runningIds.toSet() + val unread = unreadIds.toSet() + val exited = exitedIds.toSet() + val result = LinkedHashMap() + for (session in sessions) { + val key = workspaceKeyOf(session) ?: continue + val folded = comparableProjectPath(key) + val current = result[folded] ?: ProjectActivity() + result[folded] = current.copy( + running = current.running + if (session.id in running) 1 else 0, + unread = current.unread + if (session.id in unread) 1 else 0, + exited = current.exited + if (session.id in exited) 1 else 0, + ) + } + return result +} + +/** + * Group sessions under their project. Every project in [projects] gets an + * entry (possibly empty) so empty managed projects render their empty state. + * Buckets are keyed by the exact project path for callers; sessions are + * matched through a case-folded lookup so casing-only differences land in the + * right bucket instead of silently dropping the session. + */ +fun groupSessionsByProject( + projects: List, + sessions: List, +): Map> { + val grouped = LinkedHashMap>() + val bucketByKey = HashMap>() + for (project in projects) { + val bucket = mutableListOf() + grouped[project.path] = bucket + bucketByKey[comparableProjectPath(project.path)] = bucket + } + for (session in sessions) { + val key = workspaceKeyOf(session) ?: continue + bucketByKey[comparableProjectPath(key)]?.add(session) + } + return grouped +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ProviderUsageWarning.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ProviderUsageWarning.kt new file mode 100644 index 000000000..4a862797c --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ProviderUsageWarning.kt @@ -0,0 +1,75 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.ProviderUsageReport +import io.github.kahme247.ompweb.net.ProviderUsageWindow + +/** + * The share of a limit from which the sessions list warns. The provider block + * paints the same value as an error (web `usageTone`), so the warning row and + * the full block agree on when a window is tight. + */ +const val PROVIDER_USAGE_WARNING_PERCENT: Double = 80.0 + +/** One account's windows in the block's fixed order (`5H`, `7D`, `30D`). */ +fun providerUsageWindows(report: ProviderUsageReport): List> = + buildList { + report.fiveHour?.let { add("5H" to it) } + report.sevenDay?.let { add("7D" to it) } + report.monthly?.let { add("30D" to it) } + } + +/** The account line the usage block prints: label (provider), plan, `#index`. */ +fun providerUsageAccountLabel(report: ProviderUsageReport): String = buildString { + append(report.accountLabel ?: report.provider) + if (report.plan != null) append(" · ").append(report.plan) + if (report.accountIndex != null) append(" #").append(report.accountIndex) +} + +/** The account's most-used window, or null when it reports no limit at all. */ +fun providerUsageWorstWindow(report: ProviderUsageReport): Pair? = + providerUsageWindows(report).maxByOrNull { it.second.percent } + +/** + * The single line the sessions list shows while a limit is tight. + * + * [percent] is the raw share rounded to whole percent; [account] stays the + * full label, the row ellipsizes it in layout so the window and the percentage + * never scroll out of view. + */ +data class ProviderUsageWarning( + val account: String, + val window: String, + val percent: Int, +) + +/** + * The worst account window across [reports], as long as at least one window + * reached [threshold] (>=, so exactly 80 % warns). Returns null otherwise — + * the sessions list then shows no row at all and keeps its full height. + * + * Ties keep the first account/window in the report order, which is also the + * order the Settings block prints. + */ +fun providerUsageWarning( + reports: List, + threshold: Double = PROVIDER_USAGE_WARNING_PERCENT, +): ProviderUsageWarning? { + var bestAccount: String? = null + var bestWindow: String? = null + var bestPercent = Double.NEGATIVE_INFINITY + for (report in reports) { + for ((short, window) in providerUsageWindows(report)) { + if (window.percent < threshold || window.percent <= bestPercent) continue + bestAccount = providerUsageAccountLabel(report) + bestWindow = short + bestPercent = window.percent + } + } + val account = bestAccount ?: return null + val window = bestWindow ?: return null + return ProviderUsageWarning( + account = account, + window = window, + percent = Math.round(bestPercent).toInt(), + ) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ResourceNameRules.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ResourceNameRules.kt new file mode 100644 index 000000000..154be2b7a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ResourceNameRules.kt @@ -0,0 +1,12 @@ +package io.github.kahme247.ompweb.domain + +/** + * Name rule shared by MCP servers and agents, used by both settings editors. + * + * The server declares the same pattern twice: `SERVER_NAME` in + * `lib/omp/mcp-config.ts` and `AGENT_NAME_RE` in `lib/omp/agents-service.ts` + * are both `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`. Android keeps one constant so + * the two forms cannot drift apart; if the server ever splits the rules, split + * this constant instead of adding a second copy. + */ +val OMP_RESOURCE_NAME_RE: Regex = Regex("^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$") diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/SessionSearch.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SessionSearch.kt new file mode 100644 index 000000000..32daa3c11 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SessionSearch.kt @@ -0,0 +1,63 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.ManagedProject +import io.github.kahme247.ompweb.net.SessionSummary +import java.util.Locale + +/** + * Sidebar search, ported from `components/SessionSidebar.tsx` + * (`visibleProjectEntries`). + * + * A session matches on its stored title (`name`) or the **full** first user + * message. Sessions without a stored title are therefore still findable, no + * matter where in the first message the term appears. The path, cwd and id are + * not searchable — the web does not match them either. + */ +fun sessionMatchesSearch(session: SessionSummary, query: String): Boolean { + val q = query.trim().lowercase(Locale.ROOT) + if (q.isEmpty()) return true + return (session.name ?: "").lowercase(Locale.ROOT).contains(q) || + session.firstMessage.lowercase(Locale.ROOT).contains(q) +} + +/** + * A workspace with no matching sessions stays visible while its folder label + * or alias matches the query (the web's "custom workspace name is findable" + * clause). + */ +fun projectMatchesSearch(project: ManagedProject, query: String): Boolean { + val q = query.trim().lowercase(Locale.ROOT) + if (q.isEmpty()) return false + return projectPathLabel(project.path).lowercase(Locale.ROOT).contains(q) || + (project.alias ?: "").lowercase(Locale.ROOT).contains(q) +} + +/** + * The sidebar's filtered project/session entries in project order. + * + * Mirrors the web filter exactly: `runningOnly` first, then the search term; + * a project is dropped when it has no sessions left and either `runningOnly` + * is active or the term matches neither its label nor its alias. Without any + * filter every project stays visible, even without sessions. + */ +fun visibleSessionEntries( + projects: List, + sessionsByProject: Map>, + runningOnly: Boolean, + runningIds: Set, + query: String, +): List>> { + val q = query.trim() + val entries = ArrayList>>(projects.size) + for (project in projects) { + var list = sessionsByProject[project.path].orEmpty() + if (runningOnly) list = list.filter { it.id in runningIds } + if (q.isNotEmpty()) list = list.filter { sessionMatchesSearch(it, q) } + if (list.isEmpty()) { + if (runningOnly) continue + if (q.isNotEmpty() && !projectMatchesSearch(project, q)) continue + } + entries += project to list + } + return entries +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/SessionTree.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SessionTree.kt new file mode 100644 index 000000000..c1f1486a9 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SessionTree.kt @@ -0,0 +1,57 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.SessionSummary + +/** One node of the fork tree the sidebar renders (`buildSessionTree`). */ +data class SessionTreeNode( + val session: SessionSummary, + val children: List = emptyList(), +) + +/** + * Nest forked sessions under the session they forked from. + * + * Kotlin port of `buildSessionTree` in `components/SessionSidebar-helpers.ts`: + * a parentSessionId that no longer resolves (archived/deleted) attaches the + * child to the nearest existing ancestor instead of losing it, cycles are + * guarded, and every level sorts by modified descending. + */ +fun buildSessionTree(sessions: List): List { + val byId = LinkedHashMap() + for (session in sessions) byId[session.id] = MutableSessionNode(session) + + val parentOf = HashMap() + for (session in sessions) { + session.parentSessionId?.takeIf { it.isNotBlank() }?.let { parentOf[session.id] = it } + } + + fun resolveAncestor(id: String): String? { + var current = parentOf[id] + val visited = HashSet() + while (current != null) { + if (!visited.add(current)) return null + if (byId.containsKey(current)) return current + current = parentOf[current] + } + return null + } + + val roots = mutableListOf() + for (node in byId.values) { + val ancestor = resolveAncestor(node.session.id) + if (ancestor != null) byId.getValue(ancestor).children.add(node) else roots.add(node) + } + + fun freeze(nodes: List): List { + val sorted = nodes.sortedWith { a, b -> b.session.modified.compareTo(a.session.modified) } + return sorted.map { SessionTreeNode(it.session, freeze(it.children)) } + } + return freeze(roots) +} + +private class MutableSessionNode(val session: SessionSummary) { + val children = mutableListOf() +} + +/** The sidebar shows only this many top-level (non-fork) rows per project. */ +const val MAX_PROJECT_SESSIONS = 5 diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/SettingsSearch.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SettingsSearch.kt new file mode 100644 index 000000000..8b8bf98e4 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SettingsSearch.kt @@ -0,0 +1,405 @@ +package io.github.kahme247.ompweb.domain + +/** + * Settings tabs in the web's sidebar order (`SettingsTabs.tsx`). + * + * [needsWorkspace] is the invariant that a workspace-bound tab (Agents, MCP) + * stays disabled until a session/project directory is known. The web's + * legacy `extensions`/`skills`/`plugins` ids all normalize to [MCP]. + */ +enum class SettingsTab(val wire: String, val needsWorkspace: Boolean = false) { + GENERAL("general"), + SAFETY("safety"), + MODELS("models"), + PROVIDERS("providers"), + USAGE("usage"), + INTELLIGENCE("intelligence"), + AGENTS("agents", true), + MCP("mcp"), + NOTIFICATIONS("notifications"), + SYSTEM("system"), + ; + + companion object { + fun fromWire(value: String?): SettingsTab? = + entries.firstOrNull { it.wire == value } + } +} + +/** The web's per-row provenance chip. */ +enum class SettingsScope { + UI, + NATIVE_OMP, + WORKSPACE, +} + +/** + * One searchable settings row, mirroring the web's `SettingIndexEntry` + * (`components/SettingsConfig.tsx:154-224`). + * + * Slice A owns the type and its own rows; the native slice appends its rows + * with [SettingsTab] and [SettingsScope] instead of raw web strings. + */ +data class SettingsSearchEntry( + val id: String, + val tab: SettingsTab, + val section: String, + val label: String, + val description: String, + val scope: SettingsScope? = null, +) + +/** Sidebar category, searchable by its localized label and description. */ +data class SettingsCategory( + val tab: SettingsTab, + val label: String, + val description: String, +) + +/** + * The sidebar order (the web's `SettingsTabs.tsx`). Labels and descriptions + * live in the string resources so the navigation and the search index cannot + * drift apart; [SettingsScreen] builds the searchable [SettingsCategory] list + * from them. + */ +val SETTINGS_CATEGORIES: List = SettingsTab.entries + +private const val SECTION_INTERFACE = "Interface & Behavior" +private const val SECTION_USAGE = "Usage" +private const val SECTION_SYSTEM = "System & Updates" +private const val SECTION_NOTIFICATIONS = "Notifications" + +/** + * Slice A's rows: the local UI preferences, the Usage tab's sections, the + * app-side System & Updates rows and the notification rows. Native OMP and + * the providers/agents rows come from the other slices and are merged by + * [settingsSearchEntries]. + */ +val LOCAL_SETTINGS_SEARCH_ENTRIES: List = listOf( + // Interface & Behavior (General tab) + SettingsSearchEntry( + "completion-sound", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Completion sound", + "Play a tone when the agent completes a run.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "keep-tool-calls-collapsed", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Keep tool calls collapsed", + "Show only compact headers while tools execute.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "expand-thinking-by-default", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Expand thinking blocks by default", + "Open every thinking block as it appears, including while it streams, so reasoning is readable without clicking each one. Collapsing a block by hand still works.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "hide-tool-activity", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Hide tool activity while working", + "Fold a running turn's tool calls into one Process details row, the same way a finished turn folds them. Expand the row to see each call and its result.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "open-url-automatically", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Open agent links without asking", + "Links the agent opens from the session you are viewing open right away. Links from other sessions always ask first.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "tts-autoplay", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Auto-read assistant responses", + "Automatically read aloud new assistant replies when completed.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "tts-voice", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Speech Voice", + "Select the installed device voice for text-to-speech reading.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "chat-font-size", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Chat Font Size", + "Adjust text size for conversation messages, code blocks, and markdown output.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "ui-scale", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Interface Scale", + "Adjust overall UI zoom and display density across dialogs, buttons, and toolbars.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "touch-targets", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Touch Targets", + "Adjust interactive target sizes for buttons and toolbar controls.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "message-during-active-run", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Message during active run", + "What the composer does on submit while the agent runs. Steer interrupts; Queue follow-up delivers after finish.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "word-completion", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Word completion", + "Ghost text from omp's word prediction. Auto enables it only with a hardware keyboard.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "dictation-engine", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Dictation engine", + "Auto uses the connected server and falls back to this phone. On-device recognition types the words into the message while you speak; the server path uploads a recording.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "dictation-language", + SettingsTab.GENERAL, + SECTION_INTERFACE, + "Dictation language", + "Language for on-device speech recognition. Default follows the device language.", + SettingsScope.UI, + ), + // Usage & Analytics + SettingsSearchEntry( + "usage-summary", + SettingsTab.USAGE, + SECTION_USAGE, + "Usage & Analytics", + "Tokens, costs, cache analytics, and model breakdown", + SettingsScope.UI, + ), + SettingsSearchEntry( + "token-cost", + SettingsTab.USAGE, + SECTION_USAGE, + "Raw Token Cost", + "Token expenditure across providers and models", + SettingsScope.UI, + ), + SettingsSearchEntry( + "cache-savings", + SettingsTab.USAGE, + SECTION_USAGE, + "Cache Savings", + "Prompt caching savings and cost quality breakdown", + SettingsScope.UI, + ), + SettingsSearchEntry( + "usage-time-series", + SettingsTab.USAGE, + SECTION_USAGE, + "Spend over time", + "Token spend over the selected usage range, as cost or tokens.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "model-breakdown", + SettingsTab.USAGE, + SECTION_USAGE, + "Model Breakdown", + "Historical token usage and cost per model, day, and project", + SettingsScope.UI, + ), + // System & Updates + SettingsSearchEntry( + "auto-resume-sessions", + SettingsTab.SYSTEM, + SECTION_SYSTEM, + "Resume running sessions after a restart", + "When omp-web restarts while agents are working, restart those sessions and tell each agent: \"Session interrupted and resumed. Continue as you would have done without the interruption.\" Work in progress at the moment of the restart, such as a running command, is lost.", + ), + SettingsSearchEntry( + "agent-env", + SettingsTab.SYSTEM, + SECTION_SYSTEM, + "Agent environment variables", + "Extra environment variables passed to the omp process, one KEY=VALUE per line. Applied to sessions started after saving.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "system-app-update", + SettingsTab.SYSTEM, + SECTION_SYSTEM, + "App update", + "The connected server's version, the available release and the update command.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "system-omp-update", + SettingsTab.SYSTEM, + SECTION_SYSTEM, + "omp runtime update", + "Check the installed omp version and copy its update command.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "system-restart-sessions", + SettingsTab.SYSTEM, + SECTION_SYSTEM, + "Restart active sessions", + "Restart every live RPC session after an update so agents load fresh code.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "system-mobile", + SettingsTab.SYSTEM, + SECTION_SYSTEM, + "Mobile app", + "The server's binding, reachable addresses, password state and setup commands.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "windows-service-autostart", + SettingsTab.SYSTEM, + "Windows Background Service & System Tray", + "Start with Windows", + "Launch background service quietly in system tray when logging into Windows.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "windows-service-shortcuts", + SettingsTab.SYSTEM, + "Windows Background Service & System Tray", + "Install Service & Shortcuts", + "Manage background service execution, system tray monitor, Windows logon autostart, and Desktop shortcuts.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "about-app", + SettingsTab.SYSTEM, + "About", + "App version and updates", + "The installed Android app version, its update check against GitHub Releases, and the connected server's version and API.", + SettingsScope.UI, + ), + // Notifications + SettingsSearchEntry( + "notifications-enable", + SettingsTab.NOTIFICATIONS, + SECTION_NOTIFICATIONS, + "Enable notifications", + "Asks the system for permission to show notifications.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "notifications-completed", + SettingsTab.NOTIFICATIONS, + SECTION_NOTIFICATIONS, + "Task finished", + "A prompt and the background work it started are done. Subagents never notify.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "notifications-input", + SettingsTab.NOTIFICATIONS, + SECTION_NOTIFICATIONS, + "Waiting for input", + "The agent asks a question or needs an approval.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "notifications-error", + SettingsTab.NOTIFICATIONS, + SECTION_NOTIFICATIONS, + "Run failed", + "A run stops with an error after retries and fallbacks, or the omp process exits.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "notifications-modelSwitch", + SettingsTab.NOTIFICATIONS, + SECTION_NOTIFICATIONS, + "Model switched automatically", + "omp switches models on its own: a fallback after a refusal, rate limit, or usage limit, or a prewalk / plan hand-off.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "notifications-vibrate", + SettingsTab.NOTIFICATIONS, + SECTION_NOTIFICATIONS, + "Vibrate", + "One short pulse when a task finishes, a double pulse when an agent waits for input, one long pulse on errors. Never when you send.", + SettingsScope.UI, + ), + SettingsSearchEntry( + "notifications-when-active", + SettingsTab.NOTIFICATIONS, + SECTION_NOTIFICATIONS, + "When I'm using omp-web in another tab", + "Nothing is shown for the session you are viewing.", + SettingsScope.UI, + ), +) + +/** + * Merges the slices' row lists (order preserved) and drops duplicate ids so + * two slices cannot make the same row appear twice. First occurrence wins. + */ +fun settingsSearchEntries(vararg slices: List): List { + val seen = HashSet() + val merged = ArrayList() + for (slice in slices) { + for (entry in slice) { + if (seen.add(entry.id)) merged.add(entry) + } + } + return merged +} + +/** + * Case-insensitive substring search over label, description and section, the + * same haystack the web searches (`SettingsConfig.tsx:1228`). A blank query + * finds nothing; categories are matched separately by [searchSettingsCategories]. + */ +fun searchSettings(query: String, entries: List): List { + val needle = query.trim().lowercase() + if (needle.isEmpty()) return emptyList() + return entries.filter { entry -> + "${entry.label} ${entry.description} ${entry.section}".lowercase().contains(needle) + } +} + +fun searchSettingsCategories(query: String, categories: List): List { + val needle = query.trim().lowercase() + if (needle.isEmpty()) return emptyList() + return categories.filter { category -> + "${category.label} ${category.description}".lowercase().contains(needle) + } +} + +/** + * Phone layout: a selected tab or a non-blank query replaces the category + * list. On a tablet both stay visible. + */ +fun settingsShowsDetail(wide: Boolean, selected: SettingsTab?, query: String): Boolean = + wide || selected != null || query.isNotBlank() diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/SkillDiagnostics.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SkillDiagnostics.kt new file mode 100644 index 000000000..9e4024276 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SkillDiagnostics.kt @@ -0,0 +1,114 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull + +/** + * The skill-resolution report omp publishes in `get_state.skillDiagnostics` + * and pushes as `skill_diagnostics_update` (web `lib/skill-diagnostics.ts`). + * + * The parser is allowlisted and all-or-nothing: one malformed entry rejects + * the snapshot, because a half-rendered report would name the wrong skill as + * the winner. Both the server and the UI treat `null` as "unavailable". + */ +data class SkillEntry( + val name: String, + val filePath: String, + val source: String, + val pluginName: String? = null, +) + +data class SkillDuplicate( + val skill: SkillEntry, + val retained: SkillEntry, +) + +/** Why a skill name resolved the way it did (wire values are omp's). */ +enum class SkillDiagnosticReason(val wireName: String) { + SOURCE_ORDER("source-order"), + CUSTOM_DIRECTORY("custom-directory"), + AUTHORED_OVER_INSTALLED("authored-over-installed"), + ; + + companion object { + fun fromWire(value: String?): SkillDiagnosticReason? = + entries.firstOrNull { it.wireName == value } + } +} + +data class SkillDiagnostic( + val name: String, + val reason: SkillDiagnosticReason, + val skills: List, + val duplicates: List, +) + +data class SkillDiagnosticsSnapshot( + val cwd: String, + val showStartupDiagnostics: Boolean, + val diagnostics: List, +) { + /** Names with more than one candidate, i.e. what the notice calls conflicts. */ + val conflictCount: Int + get() = diagnostics.count { it.skills.size > 1 } + + val duplicateCount: Int + get() = diagnostics.sumOf { it.duplicates.size } +} + +/** Parse one report; `null` when the payload is absent or not exactly the shape omp documents. */ +fun parseSkillDiagnosticsSnapshot(element: JsonElement?): SkillDiagnosticsSnapshot? { + val root = element as? JsonObject ?: return null + val cwd = (root["cwd"] as? JsonPrimitive)?.takeIf { it.isString }?.content ?: return null + val showStartup = (root["showStartupDiagnostics"] as? JsonPrimitive)?.booleanOrNull ?: return null + val rawDiagnostics = root["diagnostics"] as? JsonArray ?: return null + val diagnostics = ArrayList(rawDiagnostics.size) + for (raw in rawDiagnostics) { + diagnostics += parseDiagnostic(raw) ?: return null + } + return SkillDiagnosticsSnapshot( + cwd = cwd, + showStartupDiagnostics = showStartup, + diagnostics = diagnostics, + ) +} + +private fun parseDiagnostic(element: JsonElement): SkillDiagnostic? { + val root = element as? JsonObject ?: return null + val name = (root["name"] as? JsonPrimitive)?.takeIf { it.isString }?.content ?: return null + val reason = SkillDiagnosticReason.fromWire( + (root["reason"] as? JsonPrimitive)?.takeIf { it.isString }?.content, + ) ?: return null + val rawSkills = root["skills"] as? JsonArray ?: return null + val skills = ArrayList(rawSkills.size) + for (rawSkill in rawSkills) { + skills += parseEntry(rawSkill) ?: return null + } + val rawDuplicates = root["duplicates"] as? JsonArray ?: return null + val duplicates = ArrayList(rawDuplicates.size) + for (rawDuplicate in rawDuplicates) { + val pair = rawDuplicate as? JsonObject ?: return null + duplicates += SkillDuplicate( + skill = parseEntry(pair["skill"] ?: return null) ?: return null, + retained = parseEntry(pair["retained"] ?: return null) ?: return null, + ) + } + return SkillDiagnostic(name = name, reason = reason, skills = skills, duplicates = duplicates) +} + +private fun parseEntry(element: JsonElement): SkillEntry? { + val root = element as? JsonObject ?: return null + val name = (root["name"] as? JsonPrimitive)?.takeIf { it.isString }?.content ?: return null + val filePath = (root["filePath"] as? JsonPrimitive)?.takeIf { it.isString }?.content ?: return null + val source = (root["source"] as? JsonPrimitive)?.takeIf { it.isString }?.content ?: return null + val rawPlugin = root["pluginName"] + val pluginName = if (rawPlugin == null) { + null + } else { + (rawPlugin as? JsonPrimitive)?.takeIf { it.isString }?.content ?: return null + } + return SkillEntry(name = name, filePath = filePath, source = source, pluginName = pluginName) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/SlashCommands.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SlashCommands.kt new file mode 100644 index 000000000..9890da445 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/SlashCommands.kt @@ -0,0 +1,420 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull + +/** + * The composer's slash command palette and its `@` file autocomplete, ported + * from `lib/web-slash-commands.ts`, `components/ChatInput-slash-commands.ts`, + * `lib/slash-autocomplete.ts` and `lib/file-fuzzy.ts`. + * + * Web-native prompt commands exist because omp's own `/goal`, `/plan`, … are + * TUI-only: over the RPC prompt path they would reach the model as literal + * slash text. The client expands them into a real instruction instead. + */ + +/** One entry of the composer palette; [description] may be empty. */ +data class SlashCommandItem( + val name: String, + val description: String = "", + val argumentHint: String? = null, + val source: SlashCommandSource = SlashCommandSource.BUILTIN, +) + +enum class SlashCommandSource(val wireName: String) { + BUILTIN("builtin"), + EXTENSION("extension"), + PROMPT("prompt"), + SKILL("skill"), + OMP_BUILTIN("ompBuiltin"), +} + +/** Client-side slash commands: the request/response router traces this set. */ +val BUILTIN_SLASH_COMMAND_NAMES: List = listOf( + "goal", "plan", "review", "fix", "test", "explain", "simplify", "commit", "advisor", "loop", + "btw", "compact", "reload", "name", "session", "copy", +) + +fun isClientBuiltinCommand(name: String): Boolean = name in BUILTIN_SLASH_COMMAND_NAMES + +/** A web prompt command: [name], whether it needs arguments and its expansion. */ +data class WebSlashCommand( + val name: String, + val requiresArgs: Boolean, + /** Resource-key suffix for the bracketed usage hint (e.g. "Goal"). */ + val argumentHintKey: String, + val buildPrompt: (String) -> String, +) + +private const val LOOP_MAX_ATTEMPTS = 10 +private const val LOOP_DEFAULT_ATTEMPTS = 3 + +private fun loopPrompt(args: String): String { + val match = Regex("^(\\d+)\\s+([\\s\\S]+)$").find(args) + val attempts = if (match != null) { + // Digits can exceed Int (and a plain toIntOrNull would silently fall + // back to the default): compare as a BigInteger, capped at the max. + val digits = match.groupValues[1].trimStart('0') + if (digits.isEmpty()) { + LOOP_DEFAULT_ATTEMPTS + } else { + val parsed = digits.toBigIntegerOrNull()?.min(java.math.BigInteger.TEN)?.toInt() + (parsed ?: LOOP_MAX_ATTEMPTS).coerceIn(1, LOOP_MAX_ATTEMPTS) + } + } else { + LOOP_DEFAULT_ATTEMPTS + } + val task = if (match != null) match.groupValues[2].trim() else args + return "Attempt the following task, verifying the result concretely after each attempt " + + "(run the relevant checks or tests). If it is not fully done, try again from where it stands, " + + "building on what you learned — up to $attempts attempts in total. Stop early once it is done. " + + "Do not ask for confirmation between attempts.\n\nTask:\n\n$task" +} + +val WEB_SLASH_COMMANDS: List = listOf( + WebSlashCommand("goal", requiresArgs = true, argumentHintKey = "composer_cmd_goal_arg") { args -> + "Work toward this goal for the rest of the session:\n\n$args\n\n" + + "Treat it as the objective to prioritize when deciding what to do next." + }, + WebSlashCommand("plan", requiresArgs = true, argumentHintKey = "composer_cmd_plan_arg") { args -> + "Create a plan for this task before doing anything else:\n\n$args\n\n" + + "Think it through step by step, list concrete steps, and state what you will verify when done." + }, + WebSlashCommand("review", requiresArgs = false, argumentHintKey = "composer_cmd_review_arg") { args -> + if (args.isNotEmpty()) { + "Review $args for bugs, security issues, and opportunities to simplify. " + + "Summarize what you find, then fix anything clearly wrong." + } else { + "Review the current project state and recent changes for bugs, security issues, and " + + "opportunities to simplify. Summarize what you find, then fix anything clearly wrong." + } + }, + WebSlashCommand("fix", requiresArgs = true, argumentHintKey = "composer_cmd_fix_arg") { args -> + "Fix this issue:\n\n$args\n\nReproduce the problem, apply the smallest correct fix, " + + "and verify it works before finishing." + }, + WebSlashCommand("test", requiresArgs = true, argumentHintKey = "composer_cmd_test_arg") { args -> + "Write tests for $args. Follow the project's test conventions, cover the important behavior " + + "and edge cases, and run the tests to confirm they pass." + }, + WebSlashCommand("explain", requiresArgs = true, argumentHintKey = "composer_cmd_explain_arg") { args -> + "Explain $args concisely: what it does, how it works, and the key details worth knowing." + }, + WebSlashCommand("simplify", requiresArgs = true, argumentHintKey = "composer_cmd_simplify_arg") { args -> + "Simplify $args. Remove unnecessary complexity while preserving behavior, keep the change " + + "focused, and verify nothing breaks." + }, + WebSlashCommand("commit", requiresArgs = false, argumentHintKey = "composer_cmd_commit_arg") { args -> + if (args.isNotEmpty()) { + "Stage the relevant files and commit the current changes with this message: " + + "${JsonPrimitive(args)}. Run the project's checks first so the commit is green." + } else { + "Stage the relevant files and commit the current changes with a clear conventional " + + "commit message. Run the project's checks first so the commit is green." + } + }, + WebSlashCommand("advisor", requiresArgs = false, argumentHintKey = "composer_cmd_advisor_arg") { args -> + if (args.isNotEmpty()) { + "Act as an independent advisor reviewing this work: $args. Assess the approach, point out " + + "risks, gaps, and better alternatives, and give concrete recommendations without changing any code." + } else { + "Act as an independent advisor reviewing the current work. Assess the recent changes and " + + "overall direction, point out risks, gaps, and better alternatives, and give concrete " + + "recommendations without changing any code." + } + }, + WebSlashCommand("loop", requiresArgs = true, argumentHintKey = "composer_cmd_loop_arg", buildPrompt = ::loopPrompt), +) + +private val WEB_SLASH_COMMAND_LOOKUP: Map = + WEB_SLASH_COMMANDS.associateBy { it.name } + +fun getWebSlashCommand(name: String): WebSlashCommand? = WEB_SLASH_COMMAND_LOOKUP[name] + +sealed interface WebSlashExpansion { + data class Expand(val prompt: String) : WebSlashExpansion + + data class UsageError(val command: String, val argumentHint: String) : WebSlashExpansion + + data object NotWeb : WebSlashExpansion +} + +/** + * Resolve a full command line (`/goal ship the export`) into the expanded + * prompt, a usage error for a required-arg command without args, or + * [WebSlashExpansion.NotWeb] for commands the client does not own. + */ +fun expandWebSlashCommand(text: String): WebSlashExpansion { + if (!text.startsWith("/")) return WebSlashExpansion.NotWeb + val match = Regex("^/(\\S+)(?:\\s+([\\s\\S]*))?$").find(text) ?: return WebSlashExpansion.NotWeb + val def = getWebSlashCommand(match.groupValues[1]) ?: return WebSlashExpansion.NotWeb + val args = match.groupValues[2].trim() + if (def.requiresArgs && args.isEmpty()) { + return WebSlashExpansion.UsageError("/${def.name}", def.argumentHintKey) + } + return WebSlashExpansion.Expand(def.buildPrompt(args)) +} + +/** Rank used by both the palette and the mid-prompt skill lookup. */ +fun slashMatchRank(command: SlashCommandItem, query: String): Int { + val name = command.name.lowercase() + val description = command.description.lowercase() + return when { + name == query -> 0 + name.startsWith(query) -> 1 + name.contains(query) -> 2 + description.contains(query) -> 3 + else -> 4 + } +} + +/** + * External (omp-reported) commands: `builtin` entries are hidden (the client + * owns those names) and everything else keeps its source for grouping. + */ +fun toSlashCommandItem(root: JsonObject): SlashCommandItem? { + val name = (root["name"] as? JsonPrimitive)?.contentOrNull?.trim().orEmpty() + if (name.isEmpty()) return null + val source = (root["source"] as? JsonPrimitive)?.contentOrNull + if (source == "builtin") return null + val mapped = when (source) { + "extension" -> SlashCommandSource.EXTENSION + "skill" -> SlashCommandSource.SKILL + else -> SlashCommandSource.PROMPT + } + val description = (root["description"] as? JsonPrimitive)?.contentOrNull.orEmpty() + val hint = ((root["input"] as? JsonObject)?.get("hint") as? JsonPrimitive)?.contentOrNull + return SlashCommandItem(name = name, description = description, argumentHint = hint, source = mapped) +} + +/** Parse a `get_commands` reply or an `available_commands_update` frame. */ +fun parseSlashCommands(element: kotlinx.serialization.json.JsonElement?): List { + val array = element as? JsonArray ?: return emptyList() + return array.mapNotNull { (it as? JsonObject)?.let(::toSlashCommandItem) } +} + +// ---------------------------------------------------------------- autocomplete + +data class SlashQuery(val mode: SlashMode, val start: Int, val query: String) + +enum class SlashMode { LEADING, MID_PROMPT } + +/** + * Detect a slash token immediately before the caret. A URL never matches, and + * whitespace after `/` closes the lookup (web `extractSlashQuery`). + */ +fun extractSlashQuery(textBeforeCursor: String): SlashQuery? { + if (textBeforeCursor.isEmpty()) return null + Regex("^\\s*/([^\\s/]*)$").find(textBeforeCursor)?.let { match -> + return SlashQuery( + mode = SlashMode.LEADING, + start = textBeforeCursor.indexOf('/'), + query = match.groupValues[1].lowercase(), + ) + } + val trailing = Regex("(^|\\s)/([^\\s/]*)$").find(textBeforeCursor) ?: return null + val slashIndex = trailing.range.first + trailing.value.indexOf('/') + if (textBeforeCursor.substring(0, slashIndex).trim().isEmpty()) return null + return SlashQuery( + mode = SlashMode.MID_PROMPT, + start = slashIndex, + query = trailing.groupValues[2].lowercase(), + ) +} + +fun buildSlashInsertText(commandName: String, mode: SlashMode): String = + if (mode == SlashMode.MID_PROMPT) { + "/skill:${commandName.replace(Regex("^skill:", RegexOption.IGNORE_CASE), "")} " + } else { + "/$commandName " + } + +/** The palette order: client built-ins first, then omp's groups, then name. */ +private val SLASH_SOURCE_ORDER = mapOf( + SlashCommandSource.BUILTIN to 0, + SlashCommandSource.EXTENSION to 1, + SlashCommandSource.PROMPT to 2, + SlashCommandSource.SKILL to 3, + SlashCommandSource.OMP_BUILTIN to 4, +) + +/** + * Filter and rank [commands] for [query]. A mid-prompt lookup only shows + * skills, matched on the bare skill name (web `ChatInput`). + */ +fun filterSlashCommands( + commands: List, + query: String, + midPrompt: Boolean, +): List { + val candidates = if (midPrompt) { + commands.filter { it.source == SlashCommandSource.SKILL || it.name.startsWith("skill:") } + } else { + commands + } + val needle = query.lowercase() + return candidates + .mapNotNull { command -> + val name = command.name.lowercase() + val bare = name.removePrefix("skill:") + val description = command.description.lowercase() + val matches = if (midPrompt) { + bare.contains(needle) || name.contains(needle) || description.contains(needle) + } else { + name.contains(needle) || description.contains(needle) + } + if (!matches) return@mapNotNull null + val rank = if (midPrompt) { + slashMatchRank(command.copy(name = command.name.removePrefix("skill:")), needle) + } else { + slashMatchRank(command, needle) + } + command to rank + } + .sortedWith( + compareBy> { it.second } + .thenBy { SLASH_SOURCE_ORDER[it.first.source] ?: 99 } + .thenBy { it.first.name.lowercase() }, + ) + .map { it.first } +} + +// ------------------------------------------------------------------ @ mentions + +data class AtQuery(val start: Int, val query: String, val quoted: Boolean) + +/** Detect the `@` file token before the caret (line start or after whitespace). */ +fun extractAtQuery(textBeforeCursor: String): AtQuery? { + Regex("(?:^|\\s)@\"([^\"\\n]*)$").find(textBeforeCursor)?.let { match -> + val query = match.groupValues[1] + return AtQuery( + start = textBeforeCursor.length - (query.length + 2), + query = query, + quoted = true, + ) + } + Regex("(?:^|\\s)@([^\\s\"]*)$").find(textBeforeCursor)?.let { match -> + val query = match.groupValues[1] + return AtQuery( + start = textBeforeCursor.length - (query.length + 1), + query = query, + quoted = false, + ) + } + return null +} + +data class FileIndexEntry(val path: String, val isDir: Boolean) + +private fun pathDepth(path: String): Int = path.count { it == '/' } + +/** Derive directory entries from the server's flat file list, shallow first. */ +fun buildEntriesFromFiles(files: List): List { + val dirs = LinkedHashSet() + for (file in files) { + var index = file.indexOf('/') + while (index != -1) { + dirs += file.substring(0, index) + index = file.indexOf('/', index + 1) + } + } + val entries = ArrayList(dirs.size + files.size) + dirs.forEach { entries += FileIndexEntry(it, isDir = true) } + files.forEach { if (it.isNotEmpty()) entries += FileIndexEntry(it, isDir = false) } + return entries.sortedWith(compareBy({ pathDepth(it.path) }, { it.path })) +} + +private fun isSubsequence(needle: String, haystack: String): Boolean { + if (needle.isEmpty()) return true + var index = 0 + for (char in haystack) { + if (index < needle.length && char == needle[index]) index++ + } + return index == needle.length +} + +/** The TUI's `scoreEntry` ladder plus a low-weight subsequence fallback. */ +private fun scoreEntry(entry: FileIndexEntry, lowerQuery: String): Int { + val lowerPath = entry.path.lowercase() + var score = 0 + if ('/' in lowerQuery) { + score = when { + lowerPath == lowerQuery -> 100 + lowerPath.startsWith(lowerQuery) -> 80 + lowerPath.contains(lowerQuery) -> 50 + isSubsequence(lowerQuery, lowerPath) -> 10 + else -> 0 + } + } else { + val name = lowerPath.substringAfterLast('/') + score = when { + name == lowerQuery -> 100 + name.startsWith(lowerQuery) -> 80 + name.contains(lowerQuery) -> 50 + lowerPath.contains(lowerQuery) -> 30 + isSubsequence(lowerQuery, lowerPath) -> 10 + else -> 0 + } + } + if (entry.isDir && score > 0) score += 10 + return score +} + +const val AT_RESULT_LIMIT: Int = 20 + +fun filterFileEntries( + entries: List, + query: String, + limit: Int = AT_RESULT_LIMIT, +): List { + val needle = query.lowercase() + if (needle.isEmpty()) return entries.take(limit) + return entries + .mapNotNull { entry -> + val score = scoreEntry(entry, needle) + if (score > 0) entry to score else null + } + .sortedWith( + compareByDescending> { it.second } + .thenBy { pathDepth(it.first.path) } + .thenBy { it.first.path }, + ) + .take(limit) + .map { it.first } +} + +data class AtInsertion(val text: String, val cursorOffset: Int) + +/** + * Replacement for the `@` token when a suggestion is confirmed: files close + * the token (`@path `, quoted when needed), directories keep it open for + * drill-down (`@dir/`). + */ +fun buildAtInsertText(entryPath: String, isDir: Boolean, forceQuotes: Boolean = false): AtInsertion { + val path = if (isDir) "$entryPath/" else entryPath + val needsQuotes = forceQuotes || ' ' in path + return if (isDir) { + val text = if (needsQuotes) "@\"$path\"" else "@$path" + AtInsertion(text, if (needsQuotes) text.length - 1 else text.length) + } else { + val text = if (needsQuotes) "@\"$path\" " else "@$path " + AtInsertion(text, text.length) + } +} + +/** Replace the `@` token [query] spans with [entry]; returns the new text/caret. */ +fun applyAtCompletion( + text: String, + caret: Int, + query: AtQuery, + entry: FileIndexEntry, +): TextInsertion { + val position = caret.coerceIn(0, text.length) + var after = text.substring(position) + if (query.quoted && after.startsWith("\"")) after = after.drop(1) + val insert = buildAtInsertText(entry.path, entry.isDir, query.quoted) + val before = text.substring(0, query.start.coerceIn(0, text.length)) + return TextInsertion(before + insert.text + after, before.length + insert.cursorOffset) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/Subagents.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Subagents.kt new file mode 100644 index 000000000..f6ce42fa4 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Subagents.kt @@ -0,0 +1,536 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.longOrNull + +/** + * Subagent roster + telemetry shared by the composer panels and the transcript + * dialog (web `lib/subagent-types.ts`, `lib/subagent-format.ts`, + * `lib/subagent-history.ts`). + * + * Every wire field is optional: omp payloads are parsed leniently and a + * malformed frame is ignored rather than breaking the run. + */ + +/** A subagent id as omp writes it into artifact filenames; rejects separators and `..`. */ +val SUBAGENT_ID_RE = Regex("^[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)*$") + +/** The transcript route's id bound (server-side `SUBAGENT_ID_MAX_LENGTH`). */ +const val SUBAGENT_ID_MAX_LENGTH = 100 + +/** Per-subagent live activity entries kept for the transcript dialog. */ +const val SUBAGENT_ACTIVITY_BUFFER_MAX = 50 + +/** + * Distinct subagent ids retained in the activity/version maps. Each per-id + * list is capped, but a long turn can spawn unbounded ids (repeated or + * recursive task calls), so the outer maps are bounded too. + */ +const val SUBAGENT_ACTIVITY_MAX_IDS = 64 + +/** Keep only the most recently inserted [SUBAGENT_ACTIVITY_MAX_IDS] entries. */ +fun pruneSubagentIds(map: Map): Map { + if (map.size <= SUBAGENT_ACTIVITY_MAX_IDS) return map + val keep = map.entries.drop(map.size - SUBAGENT_ACTIVITY_MAX_IDS) + return LinkedHashMap(keep.size).apply { keep.forEach { put(it.key, it.value) } } +} + +/** True when [id] may address a subagent transcript (dotted ids included). */ +fun isValidSubagentId(id: String): Boolean = + id.length <= SUBAGENT_ID_MAX_LENGTH && SUBAGENT_ID_RE.matches(id) + +enum class SubagentAgentSource(val wireName: String) { + BUNDLED("bundled"), + USER("user"), + PROJECT("project"), + ; + + companion object { + fun fromWire(value: String?): SubagentAgentSource? = + entries.firstOrNull { it.wireName == value } + } +} + +enum class SubagentStatus(val wireName: String) { + STARTED("started"), + COMPLETED("completed"), + FAILED("failed"), + ABORTED("aborted"), + ; + + companion object { + /** Roster statuses only; `pending`/`running` map to [STARTED] separately. */ + fun fromWire(value: String?): SubagentStatus? = entries.firstOrNull { it.wireName == value } + } +} + +enum class SubagentSource { LIVE, HISTORY } + +/** Snapshot statuses that mean "running" on the wire. */ +private fun snapshotStatus(value: String?): SubagentStatus? = when (value) { + "pending", "running" -> SubagentStatus.STARTED + else -> SubagentStatus.fromWire(value) +} + +data class SubagentRetryState( + val attempt: Int, + val maxAttempts: Int, + val delayMs: Long, + val errorMessage: String, + val startedAtMs: Long, +) + +data class SubagentRetryFailure(val attempt: Int, val errorMessage: String) + +/** Live per-subagent progress snapshot (oh-my-pi `AgentProgress`). */ +data class SubagentProgress( + val index: Int? = null, + val id: String? = null, + val agent: String? = null, + val agentSource: SubagentAgentSource? = null, + val status: SubagentStatus? = null, + val task: String? = null, + val assignment: String? = null, + val description: String? = null, + val lastIntent: String? = null, + val currentTool: String? = null, + val tokens: Long? = null, + val contextTokens: Long? = null, + val contextWindow: Long? = null, + val cost: Double? = null, + val durationMs: Long? = null, + val resolvedModel: String? = null, + val retryState: SubagentRetryState? = null, + val retryFailure: SubagentRetryFailure? = null, + val inflightTaskDetails: JsonElement? = null, + val extractedToolData: Map? = null, +) + +/** Settled result of a subagent run, as the session file records it. */ +data class SubagentResult( + val error: String? = null, + val abortReason: String? = null, +) + +/** + * One roster entry. [batchSeq] is the ordinal of the spawning `task` call as + * the session file recorded it; only history entries carry it, so live frames + * sort after the calls the file already knows. + */ +data class SubagentInfo( + val id: String, + val agent: String, + val status: SubagentStatus, + val index: Int, + val agentSource: SubagentAgentSource? = null, + val description: String? = null, + val task: String? = null, + val assignment: String? = null, + val sessionFile: String? = null, + val parentToolCallId: String? = null, + val detached: Boolean? = null, + val progress: SubagentProgress? = null, + val lastUpdate: Long? = null, + val result: SubagentResult? = null, + val source: SubagentSource = SubagentSource.LIVE, + val batchSeq: Int? = null, +) { + val running: Boolean + get() = source != SubagentSource.HISTORY && status == SubagentStatus.STARTED + + /** The task line a chip shows; assignment is the prompt, task the headline. */ + val headline: String? + get() = task ?: description ?: assignment +} + +enum class SubagentActivityKind { TOOL, TEXT, NOTICE } + +/** A compact live-activity entry derived from a `subagent_event` frame. */ +data class SubagentActivityEvent(val kind: SubagentActivityKind, val label: String, val ts: Long) + +/** True when [id] can address an artifact inside the artifacts directory. */ +private fun JsonObject.stringOrNull(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull + +private fun JsonObject.intOrNull(key: String): Int? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString.not() }?.intOrNull + +private fun JsonObject.longOrNull(key: String): Long? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString.not() }?.longOrNull + +private fun JsonObject.doubleOrNull(key: String): Double? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString.not() }?.doubleOrNull + +private fun JsonObject.boolOrNull(key: String): Boolean? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString.not() }?.booleanOrNull + +private fun JsonObject.objOrNull(key: String): JsonObject? = this[key] as? JsonObject + +/** Defensively copy an `AgentProgress`-shaped object; null when it carries nothing. */ +fun parseSubagentProgress(element: JsonElement?): SubagentProgress? { + val value = element as? JsonObject ?: return null + val out = SubagentProgress( + index = value.intOrNull("index"), + id = value.stringOrNull("id"), + agent = value.stringOrNull("agent"), + agentSource = SubagentAgentSource.fromWire(value.stringOrNull("agentSource")), + status = snapshotStatus(value.stringOrNull("status")), + task = value.stringOrNull("task"), + assignment = value.stringOrNull("assignment"), + description = value.stringOrNull("description"), + lastIntent = value.stringOrNull("lastIntent"), + currentTool = value.stringOrNull("currentTool"), + tokens = value.longOrNull("tokens"), + contextTokens = value.longOrNull("contextTokens"), + contextWindow = value.longOrNull("contextWindow"), + cost = value.doubleOrNull("cost"), + durationMs = value.longOrNull("durationMs"), + resolvedModel = value.stringOrNull("resolvedModel"), + inflightTaskDetails = value["inflightTaskDetails"], + extractedToolData = (value.objOrNull("extractedToolData"))?.let { data -> + data.mapNotNull { (key, entry) -> (entry as? JsonArray)?.let { key to it } }.toMap() + }?.takeIf { it.isNotEmpty() }, + ) + // All retry fields are documented as required upstream; fabricating + // defaults for a partial frame would render a false "retrying" state. + val retryState = value.objOrNull("retryState")?.let { retry -> + val attempt = retry.intOrNull("attempt") + val maxAttempts = retry.intOrNull("maxAttempts") + val delayMs = retry.longOrNull("delayMs") + val errorMessage = retry.stringOrNull("errorMessage") + val startedAtMs = retry.longOrNull("startedAtMs") + if (attempt != null && maxAttempts != null && delayMs != null && + errorMessage != null && startedAtMs != null + ) { + SubagentRetryState(attempt, maxAttempts, delayMs, errorMessage, startedAtMs) + } else { + null + } + } + val failure = value.objOrNull("retryFailure")?.let { failureRaw -> + val attempt = failureRaw.intOrNull("attempt") + val message = failureRaw.stringOrNull("errorMessage") + if (attempt != null && message != null) SubagentRetryFailure(attempt, message) else null + } + val filled = out.copy(retryState = retryState, retryFailure = failure) + return if (filled == SubagentProgress()) null else filled +} + +/** Map a `get_subagents` snapshot to roster form (live source). */ +fun parseSubagentSnapshot(element: JsonElement?, now: Long): SubagentInfo? { + val value = element as? JsonObject ?: return null + val id = value.stringOrNull("id")?.takeIf { it.isNotEmpty() } ?: return null + val agent = value.stringOrNull("agent")?.takeIf { it.isNotEmpty() } ?: return null + // An unknown/future lifecycle status must not fabricate a live chip. + val status = snapshotStatus(value.stringOrNull("status")) ?: return null + return SubagentInfo( + id = id, + agent = agent, + status = status, + index = value.intOrNull("index") ?: -1, + agentSource = SubagentAgentSource.fromWire(value.stringOrNull("agentSource")), + description = value.stringOrNull("description"), + task = value.stringOrNull("task"), + assignment = value.stringOrNull("assignment"), + sessionFile = value.stringOrNull("sessionFile"), + parentToolCallId = value.stringOrNull("parentToolCallId"), + progress = parseSubagentProgress(value["progress"]), + // A missing lastUpdate would read as infinitely new and let back-to-back + // stale snapshots regress a terminal status; stamp the read time. + lastUpdate = value.longOrNull("lastUpdate") ?: now, + source = SubagentSource.LIVE, + ) +} + +/** + * Map a `subagent_lifecycle` payload. Stricter than a snapshot: requires id + + * status, and a missing `agent` defaults to `"subagent"`. + */ +fun parseSubagentLifecycle(element: JsonElement?, now: Long): SubagentInfo? { + val value = element as? JsonObject ?: return null + val id = value.stringOrNull("id")?.takeIf { it.isNotEmpty() } ?: return null + val status = SubagentStatus.fromWire(value.stringOrNull("status")) ?: return null + return SubagentInfo( + id = id, + agent = value.stringOrNull("agent") ?: "subagent", + status = status, + index = value.intOrNull("index") ?: -1, + agentSource = SubagentAgentSource.fromWire(value.stringOrNull("agentSource")), + description = value.stringOrNull("description"), + sessionFile = value.stringOrNull("sessionFile"), + parentToolCallId = value.stringOrNull("parentToolCallId"), + detached = value.boolOrNull("detached"), + lastUpdate = now, + source = SubagentSource.LIVE, + ) +} + +/** Extract a compact live-activity entry from a `subagent_event` payload. */ +fun parseSubagentActivityEvent(element: JsonElement?, now: Long): SubagentActivityEvent? { + val payload = element as? JsonObject ?: return null + val event = payload.objOrNull("event") ?: return null + when (event.stringOrNull("type")) { + "tool_execution_start" -> { + val toolName = event.stringOrNull("toolName") ?: "tool" + val intent = event.stringOrNull("intent")?.trim() + if (!intent.isNullOrEmpty()) { + return SubagentActivityEvent(SubagentActivityKind.TOOL, "→ $toolName — $intent", now) + } + val args = (event["args"] as? JsonObject)?.keys?.take(3)?.joinToString(", ") + val label = if (args.isNullOrEmpty()) "→ $toolName" else "→ $toolName ($args)" + return SubagentActivityEvent(SubagentActivityKind.TOOL, label, now) + } + + "message_end" -> { + val message = event.objOrNull("message") ?: return null + if (message.stringOrNull("role") != "assistant") return null + val text = assistantText(message["content"]).trim() + if (text.isEmpty()) return null + return SubagentActivityEvent(SubagentActivityKind.TEXT, text.take(140), now) + } + + "notice" -> { + val message = event.stringOrNull("message") ?: return null + return SubagentActivityEvent(SubagentActivityKind.NOTICE, message.take(140), now) + } + } + return null +} + +private fun assistantText(content: JsonElement?): String = when (content) { + is JsonPrimitive -> content.contentOrNull.orEmpty() + is JsonArray -> content.mapNotNull { block -> + val obj = block as? JsonObject ?: return@mapNotNull null + if (obj.stringOrNull("type") == "text") obj.stringOrNull("text") else null + }.joinToString("\n") + else -> "" +} + +/** + * Map an on-disk history entry (`GET /api/sessions/{id}/subagents`) to roster + * form. Flat telemetry fields become the same [SubagentProgress] the live + * frames carry, so chips render identically from either source. + */ +fun parseSubagentHistoryEntry(element: JsonElement?): SubagentInfo? { + val value = element as? JsonObject ?: return null + val id = value.stringOrNull("id")?.takeIf { it.isNotEmpty() } ?: return null + val agent = value.stringOrNull("agent")?.takeIf { it.isNotEmpty() } ?: return null + val status = SubagentStatus.fromWire(value.stringOrNull("status")) ?: return null + val retryFailure = value.objOrNull("retryFailure")?.let { raw -> + val attempt = raw.intOrNull("attempt") + val message = raw.stringOrNull("errorMessage") + if (attempt != null && message != null) SubagentRetryFailure(attempt, message) else null + } + val progress = SubagentProgress( + id = id, + agent = agent, + agentSource = SubagentAgentSource.fromWire(value.stringOrNull("agentSource")), + status = status, + task = value.stringOrNull("task"), + assignment = value.stringOrNull("assignment"), + description = value.stringOrNull("description"), + lastIntent = value.stringOrNull("lastIntent"), + tokens = value.longOrNull("tokens"), + contextTokens = value.longOrNull("contextTokens"), + contextWindow = value.longOrNull("contextWindow"), + cost = value.doubleOrNull("cost"), + durationMs = value.longOrNull("durationMs"), + resolvedModel = value.stringOrNull("resolvedModel"), + retryFailure = retryFailure, + ) + val result = value.objOrNull("result")?.let { raw -> + SubagentResult( + error = raw.stringOrNull("error"), + abortReason = raw.stringOrNull("abortReason"), + ) + } + return SubagentInfo( + id = id, + agent = agent, + status = status, + index = value.intOrNull("index") ?: -1, + agentSource = progress.agentSource, + description = progress.description, + task = progress.task, + assignment = progress.assignment, + sessionFile = value.stringOrNull("sessionFile"), + parentToolCallId = value.stringOrNull("parentToolCallId"), + detached = value.boolOrNull("detached"), + progress = if (progress == SubagentProgress()) null else progress, + result = result, + source = SubagentSource.HISTORY, + batchSeq = value.intOrNull("batchSeq"), + ) +} + +/** Ordinal for sorting: a batch the file has not recorded yet sorts last. */ +private fun batchSeqOf(subagent: SubagentInfo): Int = subagent.batchSeq ?: Int.MAX_VALUE + +/** Spawning call first, then position inside that call, then id. */ +fun compareSubagents(a: SubagentInfo, b: SubagentInfo): Int { + batchSeqOf(a).compareTo(batchSeqOf(b)).let { if (it != 0) return it } + a.index.compareTo(b.index).let { if (it != 0) return it } + return a.id.compareTo(b.id) +} + +/** + * Fold [incoming] into [prev], ordered by spawning call then position ([prev]'s + * slot for an existing id is kept, never replaced by list position). + * + * Live frames win over on-disk history for the same id; `skipNewerThan` refuses + * to overwrite entries live frames touched after a point-in-time snapshot was + * requested, so a stale snapshot cannot regress a child's terminal status. + * `batchSeq` survives every precedence rule: only the session file knows the + * launch order, and the history entry carrying it often loses to a newer live + * frame. + */ +fun mergeSubagentRoster( + prev: List, + incoming: List, + skipNewerThan: Long? = null, +): List { + val byId = LinkedHashMap(prev.size) + prev.forEach { byId[it.id] = it } + for (entry in incoming) { + val existing = byId[entry.id] + if (existing == null) { + byId[entry.id] = entry + continue + } + val batchSeq = existing.batchSeq ?: entry.batchSeq + if (skipNewerThan != null && (existing.lastUpdate ?: 0L) >= skipNewerThan) { + if (batchSeq != existing.batchSeq) byId[entry.id] = existing.copy(batchSeq = batchSeq) + continue + } + if (entry.source == SubagentSource.HISTORY && existing.source != SubagentSource.HISTORY) { + if (batchSeq != existing.batchSeq) byId[entry.id] = existing.copy(batchSeq = batchSeq) + continue + } + if (entry.source != SubagentSource.HISTORY && existing.source == SubagentSource.HISTORY) { + byId[entry.id] = entry.copy(batchSeq = batchSeq) + continue + } + byId[entry.id] = mergeFields(existing, entry).copy(batchSeq = batchSeq) + } + return byId.values.sortedWith(Comparator(::compareSubagents)) +} + +/** Field-wise replacement: an absent (null) incoming field never erases data. */ +private fun mergeFields(existing: SubagentInfo, entry: SubagentInfo): SubagentInfo = existing.copy( + agent = entry.agent.ifBlank { existing.agent }, + status = entry.status, + index = if (entry.index >= 0) entry.index else existing.index, + agentSource = entry.agentSource ?: existing.agentSource, + description = entry.description ?: existing.description, + task = entry.task ?: existing.task, + assignment = entry.assignment ?: existing.assignment, + sessionFile = entry.sessionFile ?: existing.sessionFile, + parentToolCallId = entry.parentToolCallId ?: existing.parentToolCallId, + detached = entry.detached ?: existing.detached, + progress = entry.progress ?: existing.progress, + lastUpdate = entry.lastUpdate ?: existing.lastUpdate, + result = entry.result ?: existing.result, + source = entry.source, +) + +/** Count of nested (grandchild) subagents an agent currently has in flight. */ +fun countNestedSubagents(progress: SubagentProgress?): Int { + if (progress == null) return 0 + var count = 0 + (progress.inflightTaskDetails as? JsonObject)?.let { details -> + count += (details["progress"] as? JsonArray)?.size ?: 0 + } + // Upstream records one TaskToolDetails per task call: + // extractedToolData.task = [TaskToolDetails], each with progress[]. + progress.extractedToolData?.get("task")?.forEach { item -> + val details = item as? JsonObject ?: return@forEach + count += (details["progress"] as? JsonArray)?.size ?: 0 + } + return count +} + +/** One raw message of a subagent transcript page (`get_subagent_messages`). */ +data class SubagentTranscriptMessage( + val role: String, + /** Blocks already flattened into the dialog's single text column. */ + val text: String, +) + +/** One `get_subagent_messages` / disk-fallback page (web `SubagentMessagesPage`). */ +data class SubagentTranscriptPage( + val sessionFile: String?, + val fromByte: Long, + val nextByte: Long, + val reset: Boolean, + val totalBytes: Long?, + val messages: List, +) { + /** No further bytes exist: the pager hides "Load more" (web rule). */ + val exhausted: Boolean + get() = if (totalBytes != null) nextByte >= totalBytes else messages.isEmpty() +} + +/** Web `SubagentTranscriptRow`: labels a raw child message for the transcript. */ +fun parseSubagentTranscriptPage(element: JsonElement?): SubagentTranscriptPage? { + val value = element as? JsonObject ?: return null + val fromByte = value.longOrNull("fromByte") ?: 0L + val nextByte = value.longOrNull("nextByte") ?: fromByte + val messages = (value["messages"] as? JsonArray) + ?.mapNotNull { parseSubagentTranscriptMessage(it) } + .orEmpty() + return SubagentTranscriptPage( + sessionFile = value.stringOrNull("sessionFile"), + fromByte = fromByte, + nextByte = nextByte, + reset = value.boolOrNull("reset") == true, + totalBytes = value.longOrNull("totalBytes"), + messages = messages, + ) +} + +private const val TRANSCRIPT_TEXT_LIMIT = 400 + +private fun parseSubagentTranscriptMessage(element: JsonElement): SubagentTranscriptMessage? { + val value = element as? JsonObject ?: return null + val role = value.stringOrNull("role") ?: return null + val raw = value["content"] + val blocks: List = when { + raw is JsonArray -> raw + raw is JsonPrimitive && raw.isString -> listOf(raw) + else -> emptyList() + } + fun textOf(block: JsonElement): String? { + val obj = block as? JsonObject ?: return null + if ((obj.stringOrNull("type") ?: "") != "text") return null + return obj.stringOrNull("text") + } + val joined = blocks.mapNotNull(::textOf).joinToString("\n") + val isError = value.boolOrNull("isError") == true + val text = if (role == "assistant" && raw is JsonArray) { + // Assistant turns interleave prose and tool calls in source order. + blocks.joinToString("\n") { block -> + val obj = block as? JsonObject ?: return@joinToString "" + when (obj.stringOrNull("type")) { + "toolCall" -> "→ ${obj.stringOrNull("toolName") ?: "tool"} " + (obj["input"]?.toString() ?: "{}") + "text" -> obj.stringOrNull("text").orEmpty() + else -> "" + } + } + } else { + joined.take(TRANSCRIPT_TEXT_LIMIT) + } + val body = when { + text.isNotEmpty() -> text + role == "user" || role == "assistant" -> "" + isError -> "(error)" + else -> "(no output)" + } + return SubagentTranscriptMessage(role = role, text = body) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ThinkingLevels.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ThinkingLevels.kt new file mode 100644 index 000000000..0d3266d0e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ThinkingLevels.kt @@ -0,0 +1,74 @@ +package io.github.kahme247.ompweb.domain + +/** + * Thinking-level resolution for the current composer model, ported from + * `lib/thinking-levels.ts` and `hooks/useAgentSession-stream.ts`. + * + * `/api/models` carries each pickable model's baked effort ladder as + * `["off", ...efforts]`. A running session's `get_state` resolves the model omp + * is ACTUALLY using — which can differ from the session file's model entry (a + * disabled/renamed provider falls back to the default model, and the catalog + * only lists enabled providers). When the catalog misses the current model, the + * live metadata is the authoritative fallback, so the dropdown never falls back + * to the generic ladder and lists unsupported efforts. + */ + +/** The composer's current model identity. */ +data class ModelKey(val provider: String, val modelId: String) + +/** Model-level thinking metadata read off the live session state (`get_state.model`). */ +data class ThinkingModelMeta( + val provider: String, + val modelId: String, + val name: String? = null, + val reasoning: Boolean? = null, + /** `model.thinking.efforts`; a missing object and an empty list both mean "no concrete efforts". */ + val efforts: List? = null, +) + +/** The familiar levels, in display order. */ +val DEFAULT_THINKING_LEVELS: List = + listOf("auto", "off", "minimal", "low", "medium", "high", "xhigh", "max") + +/** Keep familiar levels ordered while preserving provider-defined additions. */ +fun selectableThinkingLevels(available: List?): List { + if (available == null) return DEFAULT_THINKING_LEVELS.toList() + val remaining = LinkedHashSet(available.filter { it.isNotEmpty() && it != "auto" }) + val ordered = ArrayList(DEFAULT_THINKING_LEVELS.size + remaining.size) + for (level in DEFAULT_THINKING_LEVELS) { + if (level == "auto" || remaining.remove(level)) ordered += level + } + ordered += remaining + return ordered +} + +/** "off" is always a valid selector; concrete efforts come from the model. */ +fun thinkingLevelsForMeta(meta: ThinkingModelMeta): List { + if (meta.reasoning != true) return listOf("off") + return listOf("off") + meta.efforts.orEmpty() +} + +/** + * Levels offered for the current model: the catalog's baked ladder wins; the + * live session model backs non-catalog models. Returns null when neither source + * knows the model — callers then fall back to the generic ladder. + */ +fun resolveAvailableThinkingLevels( + catalogLevels: List?, + model: ModelKey?, + liveModel: ThinkingModelMeta?, +): List? { + if (!catalogLevels.isNullOrEmpty()) return catalogLevels + if (model != null && liveModel != null && + liveModel.provider == model.provider && liveModel.modelId == model.modelId + ) { + return thinkingLevelsForMeta(liveModel) + } + return null +} + +/** omp's "inherit" sentinel means "no explicit selection" — show as auto. */ +fun normalizeThinkingLevel(level: String?): String { + if (level.isNullOrEmpty() || level == "inherit") return "auto" + return level +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/Todos.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Todos.kt new file mode 100644 index 000000000..3c98b58e1 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Todos.kt @@ -0,0 +1,122 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +/** + * The `todoPhases` omp publishes in `get_state` and the panel renders + * (web `components/TodoList.tsx`). Wire shapes are parsed leniently: a + * malformed task must not hide the rest of the plan. + */ +enum class TodoStatus(val wireName: String) { + PENDING("pending"), + IN_PROGRESS("in_progress"), + COMPLETED("completed"), + BLOCKED("blocked"), + ABANDONED("abandoned"), + + /** A status from a newer omp; rendered like `pending`. */ + UNKNOWN("unknown"), + ; + + companion object { + fun fromWire(value: String?): TodoStatus = + entries.firstOrNull { it.wireName == value && it != UNKNOWN } ?: UNKNOWN + } +} + +data class TodoItem( + val id: String?, + val content: String, + val status: TodoStatus, + val blocker: String?, +) + +data class TodoPhase( + val id: String?, + val name: String, + val tasks: List, +) + +/** + * Parse omp's `todoPhases` array; empty when absent or the wrong shape. + * + * All-or-nothing like the web parser (`lib/session-reader.ts` + * `parseTodoPhases`): one malformed phase or task rejects the snapshot instead + * of rendering a partial plan. An unknown *status* string is the exception — + * it renders as [TodoStatus.UNKNOWN] so a newer omp cannot hide the plan. + */ +fun parseTodoPhases(element: JsonElement?): List { + val array = element as? JsonArray ?: return emptyList() + val phases = ArrayList(array.size) + for (raw in array) { + val phase = raw as? JsonObject ?: return emptyList() + val name = phase["name"].asStringOrNull() ?: return emptyList() + val tasksRaw = phase["tasks"] as? JsonArray ?: return emptyList() + val tasks = ArrayList(tasksRaw.size) + for (taskRaw in tasksRaw) { + val task = taskRaw as? JsonObject ?: return emptyList() + val content = task["content"].asStringOrNull() ?: return emptyList() + val status = task["status"].asStringOrNull() ?: return emptyList() + val blocker = task["blocker"] + if (blocker != null && blocker.asStringOrNull() == null) return emptyList() + tasks += TodoItem( + id = task["id"].asStringOrNull(), + content = content, + status = TodoStatus.fromWire(status), + blocker = blocker.asStringOrNull()?.takeIf { it.isNotBlank() }, + ) + } + phases += TodoPhase(id = phase["id"].asStringOrNull(), name = name, tasks = tasks) + } + return phases +} + +private fun JsonElement?.asStringOrNull(): String? = + (this as? JsonPrimitive)?.takeIf { it.isString }?.content + +/** `done/total` over every phase, as the panel header shows it. */ +data class TodoProgress(val done: Int, val total: Int) + +fun todoProgress(phases: List): TodoProgress { + val tasks = phases.flatMap { it.tasks } + return TodoProgress(done = tasks.count { it.status == TodoStatus.COMPLETED }, total = tasks.size) +} + +/** The phases the collapsed panel previews and whether anything was cut. */ +data class TodoPreview(val phases: List, val truncated: Boolean) + +/** Collapsed preview window: 4 phases and 5 tasks in total (web `TodoList`). */ +const val TODO_PREVIEW_PHASES = 4 +const val TODO_PREVIEW_TASKS = 5 + +/** + * Collapsed-preview order inside one phase: in-progress work first, then + * completed, so a long first phase cannot hide the live work behind "Show all". + */ +fun todoPreviewTaskOrder(status: TodoStatus): Int = when (status) { + TodoStatus.IN_PROGRESS -> 0 + TodoStatus.COMPLETED -> 1 + else -> 2 +} + +/** + * The preview rows for [phases]: first four phases, at most five tasks across + * them, per-phase ordered by [todoPreviewTaskOrder], empty phases dropped and + * [TodoPreview.truncated] when the preview hides work. + */ +fun todoPreview(phases: List, expanded: Boolean): TodoPreview { + val tasks = phases.flatMap { it.tasks } + if (expanded) return TodoPreview(phases, truncated = false) + var remaining = TODO_PREVIEW_TASKS + val displayed = phases.take(TODO_PREVIEW_PHASES).mapNotNull { phase -> + val ordered = phase.tasks.sortedBy { todoPreviewTaskOrder(it.status) } + val shown = ordered.take(remaining.coerceAtLeast(0)) + remaining -= shown.size + if (shown.isEmpty()) null else phase.copy(tasks = shown) + } + val displayedCount = displayed.sumOf { it.tasks.size } + return TodoPreview(displayed, truncated = displayedCount < tasks.size) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/ToolPresets.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ToolPresets.kt new file mode 100644 index 000000000..eff876ca9 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/ToolPresets.kt @@ -0,0 +1,26 @@ +package io.github.kahme247.ompweb.domain + +/** + * Tool presets for a newly created session. + * + * Wire contract (`lib/tool-presets.ts` `getToolNamesForPreset`): `none` sends + * an explicit empty array, `default` sends `read,bash,edit,write`, and `full` + * omits `toolNames` entirely so the server keeps omp's complete toolset. + */ +enum class ToolPreset(val wireName: String) { + NONE("none"), + DEFAULT("default"), + FULL("full"); + + /** `null` means "omit the field", not "no tools". */ + fun toolNames(): List? = when (this) { + NONE -> emptyList() + DEFAULT -> listOf("read", "bash", "edit", "write") + FULL -> null + } + + companion object { + fun fromWire(value: String?): ToolPreset = + entries.firstOrNull { it.wireName == value } ?: FULL + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/TranscriptMarkdown.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/TranscriptMarkdown.kt new file mode 100644 index 000000000..5b70463e7 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/TranscriptMarkdown.kt @@ -0,0 +1,219 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive + +/** + * Markdown export for "Copy transcript". + * + * Port of `lib/transcript.ts` (the Tauri desktop session-menu formatter): + * User/Assistant sections, thinking details, Tool/Shell sections, with + * matching tool results nested under their tool call. Pure, no platform + * dependencies; images degrade to a placeholder because binary bytes cannot + * travel through the clipboard. + */ +const val TRANSCRIPT_BLOCK_CHAR_LIMIT = 20000 +const val TRANSCRIPT_TOTAL_CHAR_LIMIT = 500000 + +private val PRETTY_JSON = Json { + prettyPrint = true + prettyPrintIndent = " " +} + +fun transcriptToMarkdown(messages: List, title: String? = null, cwd: String? = null): String { + val sections = mutableListOf() + if (!title.isNullOrBlank()) sections += "# $title" + if (!cwd.isNullOrBlank()) sections += "_${cwd}_" + + val pendingResults = LinkedHashMap() + for (message in messages) { + if (message.str("role") != "toolResult") continue + val callId = message.str("toolCallId") ?: continue + pendingResults[callId] = PendingToolResult( + name = message.str("toolName"), + text = textOf(message["content"]), + isError = message.bool("isError"), + ) + } + val consumedResults = HashSet() + + for (message in messages) { + when (message.str("role")) { + "user" -> { + val text = textOf(message["content"]).trim() + if (text.isNotEmpty()) sections += "## User\n\n${truncateBlock(text)}" + } + + "assistant" -> { + val blocks = message["content"] as? JsonArray ?: JsonArray(emptyList()) + val provider = message.str("provider") + val model = message.str("model") + val qualifier = listOfNotNull(provider, model).filter { it.isNotBlank() }.joinToString("/") + val header = if (qualifier.isNotEmpty()) "## Assistant ($qualifier)" else "## Assistant" + val body = mutableListOf() + val text = assistantText(blocks).trim() + if (text.isNotEmpty()) body += truncateBlock(text) + body += assistantThinking(blocks) + for (block in blocks) { + val blockObject = block as? JsonObject ?: continue + if (blockObject.str("type") != "toolCall") continue + val toolName = blockObject.str("toolName") ?: "tool" + val callId = blockObject.str("toolCallId") ?: "" + val lines = mutableListOf("### Tool: $toolName `$callId`") + toolInputSummary(blockObject["input"])?.let(lines::add) + val result = pendingResults[callId] + if (result != null) { + consumedResults += callId + val label = if (result.isError) "Tool result (error)" else "Tool result" + lines += "**$label: ${result.name ?: toolName}**" + lines += if (result.text.trim().isNotEmpty()) fenced(truncateBlock(result.text.trim())) else "_(empty)_" + } + body += lines.joinToString("\n\n") + } + val errorMessage = message.str("errorMessage") + if (!errorMessage.isNullOrBlank()) { + val isInterrupted = message.str("stopReason") == "aborted" || + Regex("""^interrupted(\s+by\s+user)?""", RegexOption.IGNORE_CASE).containsMatchIn(errorMessage) + body += if (isInterrupted) "_Generation stopped by user_" else "**Error:** $errorMessage" + } + if (body.isNotEmpty()) sections += "$header\n\n${body.joinToString("\n\n")}" + } + + "toolResult" -> { + val callId = message.str("toolCallId") ?: "" + if (callId in consumedResults) continue + val text = textOf(message["content"]).trim() + val label = if (message.bool("isError")) "### Tool result (error)" else "### Tool result" + val name = message.str("toolName") ?: callId + sections += if (text.isNotEmpty()) { + "$label: $name\n\n${fenced(truncateBlock(text))}" + } else { + "$label: $name\n\n_(empty)_" + } + } + + "bashExecution" -> { + val lines = mutableListOf("### Shell: `${message.str("command") ?: ""}`") + val cancelled = message.bool("cancelled") + if (message["exitCode"] != null || cancelled) { + lines += "_exit ${if (cancelled) "cancelled" else message.int("exitCode")}_" + } + val output = message.str("output")?.trim().orEmpty() + if (output.isNotEmpty()) lines += fenced(truncateBlock(output)) + sections += lines.joinToString("\n\n") + } + + "pythonExecution" -> { + val lines = mutableListOf("### Python") + lines += fenced(truncateBlock(message.str("code")?.trim().orEmpty()), "python") + val output = message.str("output")?.trim().orEmpty() + if (output.isNotEmpty()) lines += fenced(truncateBlock(output)) + sections += lines.joinToString("\n\n") + } + + "fileMention" -> { + val lines = mutableListOf("### Attached files") + (message["files"] as? JsonArray)?.forEach { element -> + val file = element as? JsonObject ?: return@forEach + val reason = file.str("skippedReason") + lines += "- `${file.str("path") ?: ""}`${if (reason != null) " _($reason)_" else ""}" + } + sections += lines.joinToString("\n") + } + + "developer" -> { + val text = textOf(message["content"]).trim() + if (text.isNotEmpty()) sections += "### System note\n\n${truncateBlock(text)}" + } + + "custom" -> { + if (!message.bool("display")) continue + val text = textOf(message["content"]).trim() + if (text.isNotEmpty()) sections += "### ${message.str("customType") ?: ""}\n\n${truncateBlock(text)}" + } + } + } + + val joined = sections.joinToString("\n\n---\n\n") + "\n" + return if (joined.length > TRANSCRIPT_TOTAL_CHAR_LIMIT) { + joined.take(TRANSCRIPT_TOTAL_CHAR_LIMIT) + + "\n\n…(transcript truncated at $TRANSCRIPT_TOTAL_CHAR_LIMIT chars)\n" + } else { + joined + } +} + +private data class PendingToolResult(val name: String?, val text: String, val isError: Boolean) + +private fun truncateBlock(text: String): String = + if (text.length <= TRANSCRIPT_BLOCK_CHAR_LIMIT) { + text + } else { + text.take(TRANSCRIPT_BLOCK_CHAR_LIMIT) + + "\n\n…(truncated ${text.length - TRANSCRIPT_BLOCK_CHAR_LIMIT} chars)" + } + +private fun pickFence(text: String): String = if ("```" in text) "~~~~" else "```" + +private fun fenced(text: String, language: String = ""): String { + val fence = pickFence(text) + return "$fence$language\n$text\n$fence" +} + +private fun textOf(content: JsonElement?): String { + if (content == null) return "" + if (content is JsonPrimitive) return content.contentOrNull ?: "" + if (content !is JsonArray) return "" + val parts = mutableListOf() + for (block in content) { + val objectBlock = block as? JsonObject ?: continue + when (objectBlock.str("type")) { + "text" -> parts += objectBlock.str("text") ?: "" + "image" -> parts += "[attached image]" + } + } + return parts.joinToString("\n\n") +} + +private fun assistantText(blocks: JsonArray): String { + val parts = mutableListOf() + for (block in blocks) { + val objectBlock = block as? JsonObject ?: continue + when (objectBlock.str("type")) { + "text" -> parts += objectBlock.str("text") ?: "" + "image" -> parts += "[attached image]" + } + } + return parts.joinToString("\n\n") +} + +private fun assistantThinking(blocks: JsonArray): List { + val out = mutableListOf() + for (block in blocks) { + val objectBlock = block as? JsonObject ?: continue + if (objectBlock.str("type") != "thinking") continue + val thinking = objectBlock.str("thinking") ?: continue + if (thinking.trim().isEmpty()) continue + out += "

    Thinking\n\n${truncateBlock(thinking)}\n\n
    " + } + return out +} + +private fun toolInputSummary(input: JsonElement?): String? { + val objectInput = input as? JsonObject ?: return null + if (objectInput.isEmpty()) return "" + val rendered = PRETTY_JSON.encodeToString(JsonElement.serializer(), objectInput) + return fenced(truncateBlock(rendered), "json") +} + +private fun JsonObject.str(key: String): String? = (this[key] as? JsonPrimitive)?.contentOrNull + +private fun JsonObject.bool(key: String): Boolean = + (this[key] as? JsonPrimitive)?.contentOrNull?.equals("true", ignoreCase = true) == true + +private fun JsonObject.int(key: String): Int? = (this[key] as? JsonPrimitive)?.jsonPrimitive?.content?.toIntOrNull() diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/UnreadSessions.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/UnreadSessions.kt new file mode 100644 index 000000000..c726ba466 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/UnreadSessions.kt @@ -0,0 +1,29 @@ +package io.github.kahme247.ompweb.domain + +/** + * Unread badge bookkeeping for sidebar sessions. + * + * Semantics (web `components/SessionSidebar*.tsx` and `useSessions`): a + * session that is running when the user is not reading it and then stops + * becomes unread; starting to run again clears the badge. The set is pruned + * to sessions that still exist so deleted files cannot leave permanent dots. + */ +fun applyRunningTransition( + previousRunning: Set, + running: Set, + selectedSessionId: String?, + unread: Set, +): Set { + val completed = previousRunning.filterTo(mutableSetOf()) { it !in running && it != selectedSessionId } + val newlyRunning = running.filter { it !in previousRunning } + if (completed.isEmpty() && newlyRunning.isEmpty()) return unread + val next = unread.toMutableSet() + newlyRunning.forEach(next::remove) + completed.forEach(next::add) + return next +} + +fun pruneUnread(unread: Set, existingIds: Collection): Set { + val existing = existingIds.toSet() + return unread.filterTo(linkedSetOf()) { it in existing } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/UpdateNotice.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/UpdateNotice.kt new file mode 100644 index 000000000..59a5870da --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/UpdateNotice.kt @@ -0,0 +1,151 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.AppUpdateInfo +import io.github.kahme247.ompweb.net.OmpUpdateInfo + +/** Which of the two server-managed updates a notice is about. */ +enum class UpdateKind { + /** omp-web itself (`/api/app-update`, `NpmUpdateStatus`). */ + APP, + + /** The installed `omp` binary (`/api/omp-update`). */ + OMP, +} + +/** + * The web's update announcement (`AppShell.tsx`: a toast whose id carries the + * version) as a banner state: one pending update, the version it dismisses by, + * and the version that was just installed. + * + * Both flags come from the web's localStorage keys: `omp-web:dismissed-*` and + * `omp-web:completed-app-update`. + */ +data class UpdateNoticeState( + val kind: UpdateKind, + val currentVersion: String?, + val availableVersion: String, + /** The version this kind was dismissed for; a newer version shows again. */ + val dismissedVersion: String? = null, + /** `omp-web:completed-app-update`: that install finished, so it is no news. */ + val completedVersion: String? = null, +) { + /** A dismissed or just-installed version stays hidden; anything newer shows. */ + val visible: Boolean + get() = dismissedVersion != availableVersion && completedVersion != availableVersion +} + +/** + * How long a successful start/resume notice check is reused. Both routes + * memoize their answer for an hour (`lib/npm-update.ts: CHECK_TTL_MS`, + * `lib/omp/updates.ts: OMP_UPDATE_CHECK_TTL_MS`: the first runs `npm view`, the + * second `omp update --check`, each at most once per TTL), so asking more often + * than that cannot find newer news. The window matches the server's TTL. + */ +const val UPDATE_NOTICE_CHECK_INTERVAL_MILLIS: Long = 60L * 60L * 1000L + +/** + * How long after a check that reached *no* endpoint the next attempt waits. + * A failed check published nothing, so the hour would only keep a release + * hidden: an offline start, a VPN that is not up yet or `omp update --check` + * timing out buys a short backoff instead of the whole window. Long enough + * that repeated foreground returns do not hammer both routes. + */ +const val UPDATE_NOTICE_FAILURE_BACKOFF_MILLIS: Long = 2L * 60L * 1000L + +/** + * Whether the shell should ask for update notices now. [lastSuccessAtMillis] + * is when a check last got an answer; the interval only counts from there, so + * a failed check never consumes the window. [lastFailureAtMillis] is when a + * check last ran without any answer and opens the short + * [UPDATE_NOTICE_FAILURE_BACKOFF_MILLIS] retry delay. Clock values in the + * future (a clock that ran backwards) read as "not due", so a time change + * cannot trigger a burst either. + */ +fun isUpdateNoticeCheckDue( + lastSuccessAtMillis: Long?, + lastFailureAtMillis: Long?, + nowMillis: Long, +): Boolean { + if (lastFailureAtMillis != null && + nowMillis - lastFailureAtMillis < UPDATE_NOTICE_FAILURE_BACKOFF_MILLIS + ) { + return false + } + if (lastSuccessAtMillis == null) return true + return nowMillis - lastSuccessAtMillis >= UPDATE_NOTICE_CHECK_INTERVAL_MILLIS +} + +/** + * Picks the one notice the shell announces. A visible omp-web update wins over + * an omp update: it is the app the user is looking at, and installing it + * restarts the server the other update runs on. A dismissed (or already + * installed) version stays hidden and does not swallow the other update — the + * web shows its two toasts independently. The caller re-checks on every start + * and resume, so re-showing a dismissed version would nag. + */ +fun selectUpdateNotice( + app: AppUpdateInfo?, + omp: OmpUpdateInfo?, + dismissedAppVersion: String?, + dismissedOmpVersion: String?, + completedAppVersion: String?, +): UpdateNoticeState? { + val appNotice = appNotice(app, dismissedAppVersion, completedAppVersion) + // A dismissed (or just installed) app version is no news, but it must not + // swallow a pending omp update: the web shows both toasts independently. + if (appNotice != null && appNotice.visible) return appNotice + return ompNotice(omp, dismissedOmpVersion) +} + +private fun appNotice( + app: AppUpdateInfo?, + dismissedAppVersion: String?, + completedAppVersion: String?, +): UpdateNoticeState? { + if (app == null || !app.updateAvailable || app.updatesDisabled) return null + val version = app.availableVersion ?: return null + return UpdateNoticeState( + kind = UpdateKind.APP, + currentVersion = app.currentVersion, + availableVersion = version, + dismissedVersion = dismissedAppVersion, + completedVersion = completedAppVersion, + ) +} + +private fun ompNotice(omp: OmpUpdateInfo?, dismissedOmpVersion: String?): UpdateNoticeState? { + if (omp == null || !omp.updateAvailable || omp.updatesDisabled) return null + val version = omp.availableVersion ?: return null + return UpdateNoticeState( + kind = UpdateKind.OMP, + currentVersion = omp.currentVersion, + availableVersion = version, + dismissedVersion = dismissedOmpVersion, + ) +} + +/** Server-side self-update stage (`SelfUpdateStatus.stage`). */ +enum class UpdatePhase { + IDLE, + PREPARING, + RESTARTING, + INSTALLING, + FINALIZING, + COMPLETED, + FAILED, +} + +/** + * Maps a `SelfUpdateStatus` to the phase the card shows. A status without a + * stage already passed preparation (web: `status.stage ?? "stopping"`). + */ +fun updatePhaseFor(stage: String?): UpdatePhase = when (stage) { + "preparing" -> UpdatePhase.PREPARING + "stopping" -> UpdatePhase.RESTARTING + "installing" -> UpdatePhase.INSTALLING + "restarting", "finalizing" -> UpdatePhase.FINALIZING + else -> UpdatePhase.RESTARTING +} + +/** The attempt reached its terminal state; `state` is `succeeded`/`failed`. */ +fun isTerminalUpdateState(state: String?): Boolean = state == "succeeded" || state == "failed" diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/WordPrediction.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/WordPrediction.kt new file mode 100644 index 000000000..a245930f4 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/WordPrediction.kt @@ -0,0 +1,119 @@ +package io.github.kahme247.ompweb.domain + +/** + * Pure draft/ghost-text arithmetic for composer word prediction, ported from + * `lib/word-prediction.ts` and `hooks/useWordPrediction.ts` (omp's + * `predict_word` RPC). The engine answers with the suffix to paint after the + * word at the cursor; everything here decides where that suffix still applies. + */ + +/** Ghost-text word completion preference (`omp-web:word-completion`). */ +enum class WordCompletionMode(val wireName: String) { + /** Only with a hardware keyboard or a fine pointer — see [wordCompletionActive]. */ + AUTO("auto"), + ON("on"), + OFF("off"); + + companion object { + val DEFAULT = AUTO + + fun fromWire(value: String?): WordCompletionMode = + entries.firstOrNull { it.wireName == value } ?: DEFAULT + } +} + +/** A ghost-text suffix and the draft state (text + caret) it was predicted for. */ +data class WordGhost(val text: String, val caret: Int, val suffix: String) + +/** + * The result of carrying a ghost to a new draft state: the ghost that still + * applies (if any) and whether the user typed past the suggestion. + */ +data class GhostAdvance(val ghost: WordGhost?, val typedPast: Boolean) + +/** Where an accepted ghost lands when inserted into the draft. */ +data class GhostInsertion(val text: String, val caret: Int, val insert: String) + +/** Wait this long after the last keystroke before asking omp; a burst of typing sends one request. */ +const val WORD_PREDICTION_DEBOUNCE_MS: Long = 60 + +/** omp's prose gate answers nothing past this draft length, so don't ship bigger drafts per keystroke. */ +const val WORD_PREDICTION_MAX_DRAFT_LENGTH: Int = 20_000 + +/** + * After an omp without a command (`predict_word`, `btw`), check again this + * much later (it may have been updated and restarted). + */ +const val WORD_PREDICTION_UNSUPPORTED_RETRY_MS: Long = 60_000 + +/** + * `auto` enables ghost text only with a hardware keyboard (`Configuration.keyboard + * != KEYBOARD_NOKEYS`) or a connected mouse — the Android equivalent of the + * web's `(pointer: fine)` check, which cannot detect an on-screen keyboard. + */ +fun wordCompletionActive( + mode: WordCompletionMode, + hasHardwareKeyboard: Boolean, + hasFinePointer: Boolean, +): Boolean = when (mode) { + WordCompletionMode.ON -> true + WordCompletionMode.OFF -> false + WordCompletionMode.AUTO -> hasHardwareKeyboard || hasFinePointer +} + +/** Ghost text only paints where the rest of the line is empty, so it never overlaps typed text. */ +fun atLineEnd(text: String, cursor: Int): Boolean { + val next = text.getOrNull(cursor)?.toString() ?: "" + return next.isEmpty() || next == "\n" +} + +/** JS `String.prototype.slice` bounds (negative indexes and out-of-range ends clamp, end < start is empty). */ +private fun slice(text: String, start: Int, end: Int): String { + val from = start.coerceIn(0, text.length) + val to = end.coerceIn(0, text.length) + return if (to <= from) "" else text.substring(from, to) +} + +/** + * Carry [ghost] to a new draft state. Typing that matches the ghost keeps its + * remainder (so it doesn't flicker while the engine re-answers); typing that + * diverges reports `typedPast` so the engine can learn the rejection; any + * other edit or caret move just drops it. + */ +fun advanceGhost(ghost: WordGhost, text: String, cursor: Int): GhostAdvance { + if (text == ghost.text && cursor == ghost.caret) return GhostAdvance(ghost, typedPast = false) + val head = slice(ghost.text, 0, ghost.caret) + val tail = slice(ghost.text, ghost.caret, ghost.text.length) + val typed = slice(text, ghost.caret, cursor) + if (cursor <= ghost.caret || text != head + typed + tail) return GhostAdvance(null, typedPast = false) + if (ghost.suffix.lowercase().startsWith(typed.lowercase())) { + val suffix = ghost.suffix.drop(typed.length) + return GhostAdvance( + ghost = if (suffix.isNotEmpty()) WordGhost(text, cursor, suffix) else null, + typedPast = false, + ) + } + return GhostAdvance(null, typedPast = true) +} + +/** Whitespace or punctuation that already separates the word, so accepting adds no space. */ +private val WORD_TERMINATED = Regex("^[\\s.,;:!?\"\\])}]") + +/** + * Insert the ghost at its caret, plus the trailing space omp's editor adds + * unless whitespace/punctuation follows. [GhostInsertion.insert] is the exact + * string a composer has to splice at the caret. + */ +fun acceptGhost(ghost: WordGhost): GhostInsertion { + val after = slice(ghost.text, ghost.caret, ghost.text.length) + val insert = ghost.suffix + if (WORD_TERMINATED.containsMatchIn(after)) "" else " " + return GhostInsertion( + text = slice(ghost.text, 0, ghost.caret) + insert + after, + caret = ghost.caret + insert.length, + insert = insert, + ) +} + +/** An omp without `predict_word` answers "Unknown command"; the predictor pauses asking it. */ +fun isUnsupportedPredictionError(message: String?): Boolean = + message?.contains("Unknown command", ignoreCase = true) == true diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/domain/Worktrees.kt b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Worktrees.kt new file mode 100644 index 000000000..71588d4ea --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/domain/Worktrees.kt @@ -0,0 +1,192 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.ApiError +import io.github.kahme247.ompweb.net.WorktreeEntry +import io.github.kahme247.ompweb.net.WorktreeList +import io.github.kahme247.ompweb.net.userMessage + +/** + * Client-side rules of the Worktrees tab (`components/WorktreesPanel.tsx`, + * `lib/worktree.ts`, `app/api/worktrees/route.ts`) and of the Phase-4 + * new-session worktree select — one cache, one set of rules (AGENTS.md + * "Worktrees and project grouping"). + * + * The server owns git. What the client decides: whether the tab applies to + * the active cwd, which worktree is current, where a created worktree lands, + * which branches may be sent at all, and what a failed call means on screen + * (dirty ⇒ ask before force). + */ + +/** Directory suffix AGENTS.md documents for created worktrees. */ +const val WORKTREE_DIR_SUFFIX = "-worktrees" + +/** Stable error `code`s of `app/api/worktrees/route.ts` (`worktreeErrorCode`). */ +object WorktreeCode { + const val BRANCH_REQUIRED = "branch_required" + const val INVALID_BRANCH_NAME = "invalid_branch_name" + const val DIRECTORY_EXISTS = "worktree_directory_exists" + const val NOT_A_WORKTREE = "not_a_worktree" + const val CANNOT_REMOVE_MAIN = "cannot_remove_main_worktree" + const val DIRTY = "worktree_dirty" + const val ACCESS_DENIED = "access_denied" + const val DIRECTORY_NOT_FOUND = "directory_not_found" +} + +/** + * What a failed worktree call means for the pane. + * + * [Dirty] is the 409 `worktree_dirty` answer: removing a worktree with + * uncommitted files is refused once, and the UI asks before retrying with + * `force` (AGENTS.md invariant: force confirmation, main worktree never + * removable). + */ +sealed interface WorktreeFailure { + data object Dirty : WorktreeFailure + data object BranchRequired : WorktreeFailure + data object InvalidBranchName : WorktreeFailure + data object DirectoryExists : WorktreeFailure + data object NotAWorktree : WorktreeFailure + data object CannotRemoveMain : WorktreeFailure + data object AccessDenied : WorktreeFailure + data object DirectoryNotFound : WorktreeFailure + + /** Anything else; [message] is display-ready. */ + data class Other(val message: String) : WorktreeFailure +} + +/** Maps an [ApiError] (or any throwable) onto the pane's reaction. */ +fun worktreeFailure(error: Throwable): WorktreeFailure = when (error) { + is ApiError.ServerResponse -> when (error.code) { + WorktreeCode.DIRTY -> WorktreeFailure.Dirty + WorktreeCode.BRANCH_REQUIRED -> WorktreeFailure.BranchRequired + WorktreeCode.INVALID_BRANCH_NAME -> WorktreeFailure.InvalidBranchName + WorktreeCode.DIRECTORY_EXISTS -> WorktreeFailure.DirectoryExists + WorktreeCode.NOT_A_WORKTREE -> WorktreeFailure.NotAWorktree + WorktreeCode.CANNOT_REMOVE_MAIN -> WorktreeFailure.CannotRemoveMain + WorktreeCode.ACCESS_DENIED -> WorktreeFailure.AccessDenied + WorktreeCode.DIRECTORY_NOT_FOUND -> WorktreeFailure.DirectoryNotFound + // A 409 without the code is still git refusing a dirty worktree. + else -> if (error.status == 409) WorktreeFailure.Dirty else WorktreeFailure.Other(error.userMessage()) + } + is ApiError.Forbidden -> WorktreeFailure.AccessDenied + else -> WorktreeFailure.Other(error.userMessage()) +} + +/** Characters git's own ref rules reject (`lib/worktree.ts` `addWorktree`). */ +private val INVALID_BRANCH_CHARACTERS = Regex("[\\s\\u0000-\\u001f\\u007f~^:?*\\[\\]\\\\]") + +/** Characters that cannot appear in the directory name of a branch. */ +private val BRANCH_DIR_SEPARATORS = Regex("[/\\\\:*?\"<>|\\s]+") + +/** `sanitizeBranchForDir`: `feature/login x` → `feature-login-x`. */ +fun sanitizeBranchForDir(branch: String): String = + BRANCH_DIR_SEPARATORS.replace(branch, "-").trim('-') + +/** + * Rejects a branch before the request when git certainly would + * (`branch_required` / `invalid_branch_name`), so the pane can keep the input + * open and say why. `null` means "send it and let git decide". + */ +fun validateWorktreeBranch(branch: String): WorktreeFailure? { + val trimmed = branch.trim() + if (trimmed.isEmpty()) return WorktreeFailure.BranchRequired + // A bare positional like `--force` would be read as flags by `git worktree add -b`. + if (trimmed.startsWith("-")) return WorktreeFailure.InvalidBranchName + if (INVALID_BRANCH_CHARACTERS.containsMatchIn(trimmed)) return WorktreeFailure.InvalidBranchName + if (trimmed.contains("..") || trimmed.endsWith(".lock")) return WorktreeFailure.InvalidBranchName + if (trimmed.startsWith(".") || trimmed.endsWith(".")) return WorktreeFailure.InvalidBranchName + if (sanitizeBranchForDir(trimmed).isEmpty()) return WorktreeFailure.InvalidBranchName + return null +} + +/** `-worktrees/`; `null` for an unusable branch. */ +fun worktreeTargetPath(repoRoot: String, branch: String): String? { + val root = repoRoot.trimEnd('/', '\\') + val directory = sanitizeBranchForDir(branch.trim()) + if (root.isEmpty() || directory.isEmpty()) return null + val separator = if (root.contains('\\')) "\\" else "/" + return "$root$WORKTREE_DIR_SUFFIX$separator$directory" +} + +/** + * AGENTS.md / web `showWorktreeSwitcher`: only a Git top-level (main checkout + * or a linked worktree) gets a worktree list. A repo subdirectory keeps its + * own workspace identity and must not inherit the repository's worktrees. + */ +fun isWorktreeContextAvailable(list: WorktreeList): Boolean = + list.isGit && list.isTopLevel && list.projectRoot.isNotBlank() + +/** + * The worktree the given cwd belongs to, or the main checkout. Paths compare + * case-folded on Windows forms (`comparableProjectPath`), because the cwd may + * be spelled with another drive-letter or directory case than git reports. + */ +fun currentWorktreePath(worktrees: List, cwd: String?): String? { + if (cwd.isNullOrBlank()) return worktrees.firstOrNull { it.isMain }?.path + val key = comparableProjectPath(cwd) + return worktrees.firstOrNull { comparableProjectPath(it.path) == key }?.path + ?: worktrees.firstOrNull { it.isMain }?.path +} + +/** The main worktree is not removable (AGENTS.md); the pane also hides the action. */ +fun canRemoveWorktree(worktree: WorktreeEntry): Boolean = !worktree.isMain + +/** Optimistic `POST` result: append the created worktree unless it is listed already. */ +fun withCreatedWorktree(worktrees: List, created: WorktreeEntry): List { + val key = comparableProjectPath(created.path) + if (worktrees.any { comparableProjectPath(it.path) == key }) return worktrees + return worktrees + created +} + +/** Optimistic `DELETE` result. */ +fun withoutWorktree(worktrees: List, path: String): List { + val key = comparableProjectPath(path) + return worktrees.filter { comparableProjectPath(it.path) != key } +} + +/** + * Where the effective cwd moves when [removedPath] was the current worktree: + * back to the main checkout, never onto the deleted directory. + */ +fun worktreeCwdAfterRemove(projectRoot: String, removedPath: String, currentPath: String?): String? = + if (currentPath != null && comparableProjectPath(currentPath) == comparableProjectPath(removedPath)) { + projectRoot + } else { + null + } + +/** + * Web `AppShell.sessionWorktreeRemoved`: the open session's folder belongs to + * [repoProjectRoot]'s worktree list, which no longer contains it — the + * checkout was removed, so the explorer must not keep it as its workspace. + */ +fun sessionWorktreeRemoved( + sessionCwd: String?, + sessionProjectRoot: String?, + repoProjectRoot: String?, + worktrees: List, +): Boolean { + if (sessionCwd.isNullOrBlank() || repoProjectRoot.isNullOrBlank()) return false + val sessionProject = sessionProjectRoot?.takeIf { it.isNotBlank() } ?: sessionCwd + if (comparableProjectPath(sessionProject) != comparableProjectPath(repoProjectRoot)) return false + val cwd = comparableProjectPath(sessionCwd) + return worktrees.none { entry -> + val root = comparableProjectPath(entry.path) + cwd == root || cwd.startsWith("$root/") + } +} + +/** + * The explorer's workspace: the open session's folder first (unless its + * worktree was removed), then the sidebar's effective cwd, then the + * new-session target — web `AppShell.explorerCwd`. + */ +fun explorerWorkspace( + sessionCwd: String?, + sessionWorktreeRemoved: Boolean, + sidebarCwd: String?, + newSessionCwd: String?, +): String? = + sessionCwd?.takeIf { !sessionWorktreeRemoved && it.isNotBlank() } + ?: sidebarCwd?.takeIf { it.isNotBlank() } + ?: newSessionCwd?.takeIf { it.isNotBlank() } diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/events/Backoff.kt b/android/app/src/main/java/io/github/kahme247/ompweb/events/Backoff.kt new file mode 100644 index 000000000..acbaaed1e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/events/Backoff.kt @@ -0,0 +1,24 @@ +package io.github.kahme247.ompweb.events + +import kotlin.random.Random + +/** + * Exponential backoff with jitter for event-stream reconnects: 1 s, 2 s, 4 s + * … capped at 30 s, with ±20 % jitter so a fleet of clients that lost a server + * at the same moment does not reconnect in lockstep. + */ +class Backoff( + private val baseMillis: Long = 1_000, + private val maxMillis: Long = 30_000, + private val jitterRatio: Double = 0.2, + private val random: Random = Random.Default, +) { + fun delayMillis(attempt: Int): Long { + var capped = baseMillis + repeat(attempt.coerceIn(0, 30)) { + capped = (capped * 2).coerceAtMost(maxMillis) + } + val factor = 1.0 + (random.nextDouble() * 2 - 1) * jitterRatio + return (capped * factor).toLong().coerceIn(1, maxMillis) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/events/EventChannel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/events/EventChannel.kt new file mode 100644 index 000000000..ae397d722 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/events/EventChannel.kt @@ -0,0 +1,73 @@ +package io.github.kahme247.ompweb.events + +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +/** + * The channels of `/api/events` the app subscribes to, mirroring + * `EventStreamChannel` in `lib/event-stream-protocol.ts`. + */ +sealed interface EventChannel { + + /** Stable identity of the channel; two subscriptions with the same key share one server subscription. */ + val key: String + + fun toJson(): JsonObject + + /** Running-session ids plus session-list refresh hints. */ + data object Running : EventChannel { + override val key: String = "running" + + override fun toJson(): JsonObject = buildJsonObject { put("kind", "running") } + } + + /** One session's live frames. */ + data class Session(val sessionId: String) : EventChannel { + override val key: String = "session:$sessionId" + + override fun toJson(): JsonObject = buildJsonObject { + put("kind", "session") + put("sessionId", sessionId) + } + } + + /** + * Host-tool calls of sessions no tab watches, so those turns never block + * (protocol §2.3 `host-tools`). Answered per call with `host_tool_result`. + */ + data object HostTools : EventChannel { + override val key: String = "host-tools" + + override fun toJson(): JsonObject = buildJsonObject { put("kind", "host-tools") } + } + + /** + * One file's changes (web `openFileChannel`): frames `connected`, + * `change` and `error`. [sessionId] authorizes a file outside the + * workspace through that session's roots. + */ + data class File(val path: String, val sessionId: String?) : EventChannel { + override val key: String = "file:$path|${sessionId.orEmpty()}" + + override fun toJson(): JsonObject = buildJsonObject { + put("kind", "file") + put("path", path) + if (sessionId == null) put("sessionId", JsonNull) else put("sessionId", sessionId) + } + } + + /** + * Server-routed notifications for one device identity (`clientId`), which + * the notification service keeps open in the background. + */ + data class Notifications(val clientId: String) : EventChannel { + override val key: String = "notifications:$clientId" + + override fun toJson(): JsonObject = buildJsonObject { + put("kind", "notifications") + put("clientId", clientId) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/events/EventMux.kt b/android/app/src/main/java/io/github/kahme247/ompweb/events/EventMux.kt new file mode 100644 index 000000000..d19ff7a43 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/events/EventMux.kt @@ -0,0 +1,521 @@ +package io.github.kahme247.ompweb.events + +import io.github.kahme247.ompweb.net.ApiError +import io.github.kahme247.ompweb.net.EventPostResult +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +/** Whether the mux currently has a working stream. */ +enum class ConnectionState { Connected, Reconnecting, Offline } + +/** A subscription that the server refused (409) or dropped; delivered to its collectors. */ +class EventSubscriptionException( + val status: Int?, + message: String, +) : Exception(message) + +/** + * The part of [EventMux] a view model needs, as a seam for JVM tests. + * + * Production always uses [EventMux]; `ChatViewModelTest` supplies a fake flow. + */ +interface SessionEventSource { + val state: StateFlow + + fun subscribe(channel: EventChannel): Flow +} + +/** + * One SSE connection per process to `/api/events`, with ref-counted + * subscriptions. + * + * Subscriptions carry the stable id the server echoes as the SSE `event:` + * name. After `hello`, every active subscription is POSTed; after a reconnect + * the same ids are POSTed again on the new connection. An `end` control frame + * means the server closed only that subscription (its watcher died): the + * flow stays open and the subscription is POSTed again after + * [RESUBSCRIBE_DELAY_MILLIS], as the protocol requires. Only a refusing answer + * (409/403/404 with the channel's own code) ends that subscription's flow + * with an error, never the connection. While no foreground service holds the + * stream, it is foreground-only: five seconds after the app goes to the + * background the connection is dropped, and it comes back on return. The + * notification service calls [setPersistent] to keep exactly this one + * connection alive in the background. + */ +class EventMux( + private val transport: EventTransport, + private val scope: CoroutineScope, + private val backoff: Backoff = Backoff(), + /** + * Monotonic time base for the liveness/rate-limit guards. The app injects + * `SystemClock.elapsedRealtime()` (it keeps counting through deep sleep); the + * portable default keeps the class free of Android APIs so the JVM tests can + * run a live mux. + */ + private val clock: () -> Long = { System.nanoTime() / 1_000_000 }, +) : SessionEventSource { + + private class ActiveSubscription( + val channel: EventChannel, + val id: String, + val sinks: MutableSet> = LinkedHashSet(), + ) + + private val lock = Any() + private val active = LinkedHashMap() + private val acknowledged = mutableSetOf() + private val subscribeMutex = Mutex() + private var nextId = 1 + + private var connectionJob: Job? = null + private var backgroundJob: Job? = null + private var connectionId: String? = null + + /** + * Which [connectionLoop] owns the connection identity. `stopConnection()` + * and every `connectNow()` bump it, so a loop that was replaced while it + * blocked in a non-cancellable `open()` can never reset the identity of + * the connection that replaced it. + */ + private var loopGeneration = 0L + + /** Set by the notification service: backgrounding the app must not drop the stream. */ + private var persistent = false + + /** When the last connection attempt started; rate-limits network-triggered retries. */ + private var lastRetryAt: Long? = null + + /** When the last live frame arrived on the current connection (null = none yet). */ + @Volatile + private var lastFrameAt: Long? = null + + /** + * Bumped whenever the connection identity changes. A subscribe POST that + * started under an older generation must never mark its subscription as + * acknowledged on the newer connection. + */ + private var connectionEpoch = 0L + private var stream: EventStream? = null + + private val _state = MutableStateFlow(ConnectionState.Reconnecting) + override val state: StateFlow = _state.asStateFlow() + + /** + * Live frames of [channel]. Two collectors of the same channel share one + * server subscription; the subscription is dropped with its last collector. + * The flow stays open across an `end` frame (the mux resubscribes) and + * throws [EventSubscriptionException] when the server refuses the + * subscription. + */ + override fun subscribe(channel: EventChannel): Flow = flow { + val sink = Channel( + capacity = 128, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + register(channel, sink) + try { + for (frame in sink) emit(frame) + } finally { + unregister(channel.key, sink) + } + } + + /** Called when the app returns to the foreground. */ + fun onForeground() { + synchronized(lock) { + backgroundJob?.cancel() + backgroundJob = null + } + if (hasSubscriptions()) connectNow() + } + + /** Called when the app leaves the foreground; disconnects after a grace period. */ + fun onBackground() { + if (synchronized(lock) { persistent }) return + if (!hasSubscriptions()) return + synchronized(lock) { + backgroundJob?.cancel() + backgroundJob = scope.launch { + delay(BACKGROUND_GRACE_MILLIS) + stopConnection() + } + } + } + + /** + * The notification foreground service keeps the stream open while the app + * is backgrounded. The service and the UI still share this one connection; + * this only suspends the background disconnect. + */ + fun setPersistent(active: Boolean) { + synchronized(lock) { + persistent = active + if (active) { + backgroundJob?.cancel() + backgroundJob = null + } + } + if (active && hasSubscriptions()) connectNow() + } + + /** Drops the current connection and reconnects immediately (the banner's Retry). */ + fun retryNow() { + stopConnection() + if (hasSubscriptions()) connectNow() + } + + /** + * A default network became available (Wi-Fi after mobile data, Tailscale + * roaming, airplane mode off). Half-open SSE sockets survive such a change + * without an error, so the stream has to be reconnected instead of waiting + * for the read timeout. + * + * Two guards keep this from becoming a reconnect storm: the call does + * nothing without a subscription, and it fires at most once per + * [MIN_NETWORK_RETRY_INTERVAL_MILLIS] (a network switch delivers several + * `onAvailable` callbacks for the same event). A reconnect also never + * happens on top of a live connection that just delivered a frame. + */ + fun onNetworkAvailable() { + if (!hasSubscriptions()) return + val now = clock() + // A frame that arrived moments ago proves the socket survived the + // network change; reconnecting then would only add churn. Control + // frames (hello/open/end) count as liveness too, so a connection that + // just completed its handshake is not torn down on every `onAvailable`. + val frameAt = lastFrameAt + if (frameAt != null && now - frameAt < NETWORK_LIVENESS_MILLIS) return + synchronized(lock) { + val retryAt = lastRetryAt + if (retryAt != null && now - retryAt < MIN_NETWORK_RETRY_INTERVAL_MILLIS) return + lastRetryAt = now + } + retryNow() + } + + private fun register(channel: EventChannel, sink: Channel) { + var needsConnection = false + var pumpConnection: String? = null + synchronized(lock) { + val subscription = active[channel.key] + ?: ActiveSubscription(channel = channel, id = "s${nextId++}").also { + active[channel.key] = it + } + subscription.sinks.add(sink) + if (connectionJob?.isActive != true) { + needsConnection = true + } else { + pumpConnection = connectionId + } + } + if (needsConnection) { + connectNow() + } else if (pumpConnection != null) { + pumpSubscriptions() + } + } + + private fun unregister(key: String, sink: Channel) { + var removedId: String? = null + val currentConnectionId: String? + synchronized(lock) { + val subscription = active[key] ?: return + subscription.sinks.remove(sink) + if (subscription.sinks.isEmpty()) { + active.remove(key) + acknowledged.remove(subscription.id) + if (connectionId != null) removedId = subscription.id + } + currentConnectionId = connectionId + } + removedId?.let { id -> + val target = currentConnectionId ?: return@let + scope.launch { + runCatching { transport.unsubscribe(target, id) } + } + } + if (!hasSubscriptions()) stopConnection() + } + + private fun connectNow() { + synchronized(lock) { + if (connectionJob?.isActive == true) return + val owner = ++loopGeneration + connectionJob = scope.launch { + connectionLoop(owner) + } + } + } + + private fun stopConnection() { + val job = synchronized(lock) { + // Disown the running loop: it may still be blocked in a + // non-cancellable `open()`, and it must not clear the identity of + // the connection that replaces it. + loopGeneration++ + connectionEpoch++ + connectionId = null + stream = null + acknowledged.clear() + lastFrameAt = null + connectionJob.also { connectionJob = null } + } + job?.cancel() + _state.value = ConnectionState.Offline + } + + private fun hasSubscriptions(): Boolean = synchronized(lock) { active.isNotEmpty() } + + private fun ownsConnection(owner: Long): Boolean = synchronized(lock) { loopGeneration == owner } + + private suspend fun connectionLoop(owner: Long) { + var attempt = 0 + while (true) { + if (!hasSubscriptions()) break + if (!ownsConnection(owner)) return + _state.value = ConnectionState.Reconnecting + var opened: EventStream? = null + try { + opened = transport.open() + // `open()` can block in a non-cancellable handshake for the + // whole connect timeout. If this loop was replaced meanwhile, + // the stream belongs to nobody: close it and leave the new + // loop's identity alone. + val owns = synchronized(lock) { + val mine = loopGeneration == owner + if (mine) stream = opened + mine + } + if (!owns) return + opened.frames.collect { frame -> + if (frame.event == MUX_CONTROL_EVENT) { + when (val control = parseControl(frame.data)) { + is MuxControl.Hello -> { + synchronized(lock) { + connectionEpoch++ + connectionId = control.connectionId + acknowledged.clear() + } + noteFrame() + attempt = 0 + _state.value = ConnectionState.Connected + pumpSubscriptions() + } + is MuxControl.End -> { + noteFrame() + endSubscription(control.sub) + } + MuxControl.Open -> noteFrame() + null -> Unit + } + } else if (frame.event != null) { + deliver(frame.event, frame.data) + } + } + // Server closed the stream; fall through to a reconnect. + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: ApiError.Unauthorized) { + if (ownsConnection(owner)) _state.value = ConnectionState.Offline + return + } catch (_: Throwable) { + // Network failure or an unusable stream: reconnect below. + } finally { + opened?.close() + synchronized(lock) { + if (stream === opened) stream = null + // Only the loop that still owns the connection may reset + // its identity: a replaced loop returning late used to + // clear the new connection's id and leave a "Connected" + // stream that never subscribed again. + if (loopGeneration == owner) { + connectionEpoch++ + connectionId = null + acknowledged.clear() + } + } + } + if (!hasSubscriptions()) break + if (!ownsConnection(owner)) return + _state.value = ConnectionState.Reconnecting + delay(backoff.delayMillis(attempt++)) + } + if (ownsConnection(owner)) _state.value = ConnectionState.Offline + } + + /** A frame of any kind proves the socket is alive (SSE heartbeats are comments and never reach here). */ + private fun noteFrame() { + lastFrameAt = clock() + } + + private fun pumpSubscriptions() { + val connection = synchronized(lock) { + val current = connectionId ?: return + current to connectionEpoch + } + val (currentConnectionId, epoch) = connection + scope.launch { + try { + subscribeMutex.withLock { postMissingSubscriptions(currentConnectionId, epoch) } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: StaleConnection) { + // `unknown_connection`: the server no longer knows this stream. + dropStreamIfCurrent(currentConnectionId, epoch) + } catch (_: ApiError.Unauthorized) { + _state.value = ConnectionState.Offline + } catch (_: Throwable) { + // A transient POST failure (network, 413, 5xx) must not hot-loop: + // drop the stream and let the connection loop use its backoff. + dropStreamIfCurrent(currentConnectionId, epoch) + } + } + } + + private suspend fun postMissingSubscriptions(connectionId: String, epoch: Long) { + while (true) { + val pending = synchronized(lock) { + if (!isCurrent(connectionId, epoch)) return + active.values.filter { it.id !in acknowledged } + } + if (pending.isEmpty()) return + for (subscription in pending) { + val result = transport.subscribe(connectionId, subscription.id, subscription.channel) + if (!synchronized(lock) { isCurrent(connectionId, epoch) }) return + when (result) { + EventPostResult.Ok -> synchronized(lock) { + // The ack belongs to this connection only; a POST that + // returns after the stream changed must not hide the + // subscription on the new one. + if (isCurrent(connectionId, epoch)) acknowledged.add(subscription.id) + } + EventPostResult.ConnectionUnknown -> throw StaleConnection() + is EventPostResult.Rejected -> terminate( + subscription, + EventSubscriptionException( + result.status, + result.message.ifBlank { "Subscription refused" }, + ), + ) + } + } + } + } + + private fun isCurrent(connectionId: String, epoch: Long): Boolean = + this.connectionId == connectionId && this.connectionEpoch == epoch + + private fun dropStreamIfCurrent(connectionId: String, epoch: Long) { + val current = synchronized(lock) { + if (!isCurrent(connectionId, epoch)) return + stream + } + current?.close() + } + + /** + * An `end` control frame closed only this subscription on the server. The + * collectors stay attached and the same id is subscribed again after + * [RESUBSCRIBE_DELAY_MILLIS]. + */ + private fun endSubscription(subId: String) { + val subscription = synchronized(lock) { + active.values.firstOrNull { it.id == subId } + } ?: return + synchronized(lock) { acknowledged.remove(subscription.id) } + scope.launch { + delay(RESUBSCRIBE_DELAY_MILLIS) + synchronized(lock) { + if (active[subscription.channel.key] !== subscription) return@launch + acknowledged.remove(subscription.id) + } + pumpSubscriptions() + } + } + + private fun terminate(subscription: ActiveSubscription, cause: Throwable?) { + synchronized(lock) { + if (active[subscription.channel.key] === subscription) { + active.remove(subscription.channel.key) + } + acknowledged.remove(subscription.id) + } + for (sink in subscription.sinks.toList()) { + if (cause == null) sink.close() else sink.close(cause) + } + subscription.sinks.clear() + if (!hasSubscriptions()) stopConnection() + } + + private fun deliver(subId: String, data: String) { + val subscription = synchronized(lock) { active.values.firstOrNull { it.id == subId } } ?: return + val json = try { + JSON.parseToJsonElement(data).jsonObject + } catch (_: Exception) { + return + } + lastFrameAt = clock() + for (sink in subscription.sinks.toList()) { + sink.trySend(json) + } + } + + private fun parseControl(data: String): MuxControl? = try { + val root = JSON.parseToJsonElement(data).jsonObject + when (root["type"]?.jsonPrimitive?.contentOrNull) { + "hello" -> root["connectionId"]?.jsonPrimitive?.contentOrNull?.let { MuxControl.Hello(it) } + "open" -> MuxControl.Open + "end" -> root["sub"]?.jsonPrimitive?.contentOrNull?.let { MuxControl.End(it) } + else -> null + } + } catch (_: Exception) { + null + } + + private sealed interface MuxControl { + data class Hello(val connectionId: String) : MuxControl + data object Open : MuxControl + data class End(val sub: String) : MuxControl + } + + private class StaleConnection : Exception() + + companion object { + /** SSE event name of connection/subscription control frames. */ + const val MUX_CONTROL_EVENT = "mux" + + /** Time the app may stay in the background before the stream is dropped. */ + const val BACKGROUND_GRACE_MILLIS = 5_000L + + /** The protocol's pause before a subscription the server ended is opened again. */ + const val RESUBSCRIBE_DELAY_MILLIS = 3_000L + + /** + * Minimum distance between two network-triggered reconnects: one + * network switch delivers several `onAvailable` callbacks. + */ + const val MIN_NETWORK_RETRY_INTERVAL_MILLIS = 2_000L + + /** A frame younger than this proves the socket survived a network change. */ + const val NETWORK_LIVENESS_MILLIS = 5_000L + + private val JSON = Json { ignoreUnknownKeys = true } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/events/EventTransport.kt b/android/app/src/main/java/io/github/kahme247/ompweb/events/EventTransport.kt new file mode 100644 index 000000000..1965cfbc3 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/events/EventTransport.kt @@ -0,0 +1,25 @@ +package io.github.kahme247.ompweb.events + +import io.github.kahme247.ompweb.net.EventPostResult +import kotlinx.coroutines.flow.Flow + +/** + * The wire side of the event mux, behind an interface so the mux's + * reconnect/resubscribe logic can be tested without a network. + */ +interface EventTransport { + /** Opens the multiplexed stream. The first frame the server sends is `hello`. */ + suspend fun open(): EventStream + + suspend fun subscribe(connectionId: String, id: String, channel: EventChannel): EventPostResult + + suspend fun unsubscribe(connectionId: String, id: String) +} + +/** One open `/api/events` connection. */ +interface EventStream { + val frames: Flow + + /** Closes the connection; the frames flow then completes. */ + fun close() +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/events/HttpEventTransport.kt b/android/app/src/main/java/io/github/kahme247/ompweb/events/HttpEventTransport.kt new file mode 100644 index 000000000..36684369a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/events/HttpEventTransport.kt @@ -0,0 +1,138 @@ +package io.github.kahme247.ompweb.events + +import io.github.kahme247.ompweb.net.ApiClient +import io.github.kahme247.ompweb.net.ApiError +import io.github.kahme247.ompweb.net.EventPostResult +import io.github.kahme247.ompweb.net.ServerAddress +import java.io.IOException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import okhttp3.Call +import okhttp3.Response + +/** + * [EventTransport] over OkHttp's streaming read of `/api/events`. + * + * [heartbeatSeconds] is the keep-alive interval the whole app asks for (S7; + * 15–110 s). The app asks for the top of that range: in two 30-minute doze + * windows on the emulator the 110 s + * build stayed connected for the whole window with no wakelock, 427 ms + 76 ms + * of CPU and 62.7 KB down / 48.7 KB up, against 1 129 ms + 168 ms and 904 KB / + * 106 KB at 60 s. 110 s also sends the fewest keep-alive frames of any allowed + * interval (about 16 per 30 minutes). + * The call's read timeout is derived from it (heartbeat + 30 s, i.e. 140 s), so + * an on-time heartbeat can never race the idle timeout, and a silently dead + * socket is still noticed within that bound. + */ +class HttpEventTransport( + private val api: ApiClient, + private val scope: CoroutineScope, + private val heartbeatSeconds: Int = DEFAULT_HEARTBEAT_SECONDS, +) : EventTransport { + + override suspend fun open(): EventStream = withContext(Dispatchers.IO) { + val call = api.openEventStream(heartbeatSeconds) + var stream: HttpEventStream? = null + try { + val response = try { + call.execute() + } catch (error: IOException) { + throw ApiError.Network(error) + } + if (response.code !in 200..299) { + val body = try { + response.body.string().take(2048) + } catch (_: IOException) { + "" + } + response.close() + val url = response.request.url + throw api.errorFor(response.code, body, ServerAddress(url.scheme, url.host, url.port)) + } + stream = HttpEventStream(call, response, scope) + stream + } finally { + // Cancelling while execute() blocked never reaches a suspension + // point inside the block; close the stream here so the call and + // its reader cannot outlive the caller. + if (!isActive) { + stream?.close() ?: call.cancel() + } + } + } + + override suspend fun subscribe(connectionId: String, id: String, channel: EventChannel): EventPostResult = + api.postEvent( + buildJsonObject { + put("connectionId", connectionId) + put( + "subscribe", + buildJsonObject { + put("id", id) + put("channel", channel.toJson()) + }, + ) + }, + ) + + override suspend fun unsubscribe(connectionId: String, id: String) { + api.postEvent( + buildJsonObject { + put("connectionId", connectionId) + put("unsubscribe", id) + }, + ) + } + + companion object { + /** S7 heartbeat the app asks for; the server accepts 15–110 s. */ + const val DEFAULT_HEARTBEAT_SECONDS = 110 + } +} + +/** + * Reads the SSE body on its own coroutine and hands frames over a channel, so + * closing the connection (backgrounding the app, signing out) cancels the + * blocking read immediately instead of waiting for the next heartbeat. + */ +private class HttpEventStream( + private val call: Call, + private val response: Response, + scope: CoroutineScope, +) : EventStream { + + private val channel = Channel(Channel.BUFFERED) + + override val frames: Flow = channel.receiveAsFlow() + + private val reader = scope.launch(Dispatchers.IO) { + try { + val parser = SseParser() + val source = response.body.source() + while (isActive) { + val line = source.readUtf8Line() ?: break + val frame = parser.consume(line) ?: continue + if (channel.trySend(frame).isFailure) break + } + } catch (_: IOException) { + // The call was cancelled or the connection dropped; EventMux reconnects. + } finally { + response.close() + channel.close() + } + } + + override fun close() { + call.cancel() + reader.cancel() + channel.close() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/events/SseParser.kt b/android/app/src/main/java/io/github/kahme247/ompweb/events/SseParser.kt new file mode 100644 index 000000000..a30ea3818 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/events/SseParser.kt @@ -0,0 +1,51 @@ +package io.github.kahme247.ompweb.events + +/** One dispatched Server-Sent Event. */ +data class SseFrame( + /** The `event:` field; the mux uses `mux` for control frames and `s` for subscriptions. */ + val event: String?, + val data: String, +) + +/** + * Incremental SSE parser: feed one line at a time, get a frame back when a + * blank line completes one. + * + * Only the fields ompweb uses are kept (`event`, `data`); `id` and `retry` + * are ignored because the mux reconnects with its own backoff and never + * resumes from `Last-Event-ID`. Multi-line `data:` fields are joined with `\n` + * as the SSE specification requires. + */ +class SseParser { + + private val data = StringBuilder() + private var event: String? = null + private var hasData = false + + fun consume(line: String): SseFrame? { + if (line.isEmpty()) return dispatch() + if (line.startsWith(":")) return null + val separator = line.indexOf(':') + val field = if (separator < 0) line else line.substring(0, separator) + var value = if (separator < 0) "" else line.substring(separator + 1) + if (value.startsWith(" ")) value = value.substring(1) + when (field) { + "event" -> event = value + "data" -> { + if (hasData) data.append('\n') + data.append(value) + hasData = true + } + } + return null + } + + private fun dispatch(): SseFrame? { + if (!hasData && event == null) return null + val frame = SseFrame(event = event, data = data.toString()) + data.setLength(0) + event = null + hasData = false + return frame + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/AgentsApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/AgentsApi.kt new file mode 100644 index 000000000..92e5de73f --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/AgentsApi.kt @@ -0,0 +1,230 @@ +package io.github.kahme247.ompweb.net + +import java.net.URLEncoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * Agent roster routes (`app/api/agents`), as an interface so `AgentsViewModel` + * is JVM-testable. A 403 means the workspace is not allowed for file access — + * a normal answer the UI turns into a warning, never a crash. + */ +interface AgentsApi { + + /** `GET /api/agents?cwd=` — every agent omp finds, plus scan diagnostics. */ + suspend fun list(cwd: String?): AgentsDocument + + /** `POST` (create) or `PUT` (update) `/api/agents` with the full agent payload. */ + suspend fun save( + cwd: String?, + scope: String, + name: String, + previousName: String?, + agent: JsonObject, + update: Boolean, + ): AgentSaveResult + + /** `DELETE /api/agents {cwd?, scope, name}`. */ + suspend fun delete(cwd: String?, scope: String, name: String) + + /** `POST /api/agents {action:"unpack", cwd?, scope}` — copy omp's bundled templates in. */ + suspend fun unpack(cwd: String?, scope: String): AgentUnpackResult +} + +/** One discovered agent file. Mirrors `AgentInfo` of `lib/omp/agents-service.ts`. */ +data class AgentInfo( + val name: String, + val description: String = "", + /** `provider/model` role selectors (`@designer`, `@smol`) when set. */ + val model: List = emptyList(), + val tools: List = emptyList(), + /** `spawns: "*"` lists no names and sets [spawnsAll]. */ + val spawns: List = emptyList(), + val spawnsAll: Boolean = false, + val thinkingLevel: String? = null, + /** `bundled`, `user` or `project`. */ + val source: String = "user", + /** `bundled`/`user`/`project`; used as the write scope for user/project. */ + val scope: String = "user", + val filePath: String = "", + val valid: Boolean = true, + val enabled: Boolean = true, + val body: String? = null, + /** The untouched frontmatter, sent back as `existingFrontmatter` so unknown keys survive. */ + val rawFrontmatter: JsonObject? = null, +) { + val isBundled: Boolean get() = scope == "bundled" || source == "bundled" +} + +data class AgentDiagnostic( + val type: String = "warning", + val message: String = "", + val path: String? = null, +) + +data class AgentsDocument( + val agents: List = emptyList(), + val diagnostics: List = emptyList(), +) + +data class AgentSaveResult( + val agent: AgentInfo? = null, + val path: String? = null, +) + +data class AgentUnpackResult( + val total: Int = 0, + val written: Int = 0, + val skipped: Int = 0, +) + +/** Production [AgentsApi] over the app's single authenticated client. */ +fun ApiClient.agentsApi(): AgentsApi = HttpAgentsApi(this) + +private val agentsJson = Json { ignoreUnknownKeys = true; explicitNulls = false } +private val AGENTS_JSON_MEDIA = "application/json".toMediaType() + +private fun JsonObject.string(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull + +private fun JsonObject.int(key: String): Int? = + (this[key] as? JsonPrimitive)?.contentOrNull?.toIntOrNull() + +private fun JsonObject.strings(key: String): List? { + val array = this[key] as? JsonArray ?: return null + return array.mapNotNull { (it as? JsonPrimitive)?.takeIf { primitive -> primitive.isString }?.contentOrNull } +} + +private fun parseAgentsObject(text: String): JsonObject? = try { + agentsJson.parseToJsonElement(text) as? JsonObject +} catch (_: Exception) { + null +} + +private fun parseAgent(element: JsonElement): AgentInfo? { + val root = element as? JsonObject ?: return null + val name = root.string("name")?.takeIf { it.isNotBlank() } ?: return null + val spawnsRaw = root["spawns"] + val spawnsAll = (spawnsRaw as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull == "*" + return AgentInfo( + name = name, + description = root.string("description").orEmpty(), + model = root.strings("model").orEmpty(), + tools = root.strings("tools").orEmpty(), + spawns = if (spawnsAll) emptyList() else root.strings("spawns").orEmpty(), + spawnsAll = spawnsAll, + thinkingLevel = root.string("thinkingLevel")?.takeIf { it.isNotBlank() }, + source = root.string("source") ?: "user", + scope = root.string("scope") ?: "user", + filePath = root.string("filePath").orEmpty(), + valid = (root["valid"] as? JsonPrimitive)?.contentOrNull != "false", + enabled = (root["enabled"] as? JsonPrimitive)?.contentOrNull != "false", + body = root.string("body"), + rawFrontmatter = root["rawFrontmatter"] as? JsonObject, + ) +} + +private class HttpAgentsApi(private val api: ApiClient) : AgentsApi { + + override suspend fun list(cwd: String?): AgentsDocument { + val suffix = if (cwd.isNullOrBlank()) "" else "?cwd=${encodeQuery(cwd)}" + val root = call(api.requestBuilder("$AGENTS_PATH$suffix").get().build()) + return AgentsDocument( + agents = (root["agents"] as? JsonArray).orEmpty().mapNotNull { parseAgent(it) }, + diagnostics = (root["diagnostics"] as? JsonArray).orEmpty().mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + AgentDiagnostic( + type = item.string("type") ?: "warning", + message = item.string("message").orEmpty(), + path = item.string("path"), + ) + }, + ) + } + + override suspend fun save( + cwd: String?, + scope: String, + name: String, + previousName: String?, + agent: JsonObject, + update: Boolean, + ): AgentSaveResult { + val payload = buildJsonObject { + if (!cwd.isNullOrBlank()) put("cwd", cwd) + put("scope", scope) + put("name", name) + if (!previousName.isNullOrBlank()) put("previousName", previousName) + put("agent", agent) + } + val request = jsonRequest(AGENTS_PATH, if (update) "PUT" else "POST", payload.toString()) + val root = call(request) + return AgentSaveResult( + agent = root["agent"]?.let { parseAgent(it) }, + path = root.string("path"), + ) + } + + override suspend fun delete(cwd: String?, scope: String, name: String) { + val payload = buildJsonObject { + if (!cwd.isNullOrBlank()) put("cwd", cwd) + put("scope", scope) + put("name", name) + } + call(jsonRequest(AGENTS_PATH, "DELETE", payload.toString())) + } + + override suspend fun unpack(cwd: String?, scope: String): AgentUnpackResult { + val payload = buildJsonObject { + put("action", "unpack") + if (!cwd.isNullOrBlank()) put("cwd", cwd) + put("scope", scope) + } + val root = call(jsonRequest(AGENTS_PATH, "POST", payload.toString())) + return AgentUnpackResult( + total = root.int("total") ?: 0, + written = root.int("written") ?: 0, + skipped = root.int("skipped") ?: 0, + ) + } + + // --- transport --------------------------------------------------------- + + private suspend fun call(request: Request): JsonObject { + val response = api.exchange(request) + val text = response.text() + if (response.status !in 200..299) { + throw api.errorFor( + response.status, + text, + ServerAddress(request.url.scheme, request.url.host, request.url.port), + ) + } + return parseAgentsObject(text) ?: throw ApiError.Http(response.status, text.take(2048)) + } + + private fun jsonRequest(path: String, method: String, payload: String): Request { + val body = payload.toRequestBody(AGENTS_JSON_MEDIA) + val builder = api.requestBuilder(path) + return when (method) { + "PUT" -> builder.put(body).build() + "DELETE" -> builder.delete(body).build() + else -> builder.post(body).build() + } + } + + private companion object { + const val AGENTS_PATH = "api/agents" + } +} + +private fun encodeQuery(value: String): String = URLEncoder.encode(value, "UTF-8") diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/AndroidReleaseApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/AndroidReleaseApi.kt new file mode 100644 index 000000000..981460a4d --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/AndroidReleaseApi.kt @@ -0,0 +1,110 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.AndroidRelease +import io.github.kahme247.ompweb.domain.AndroidReleaseAsset +import io.github.kahme247.ompweb.domain.androidVersionFromTag +import io.github.kahme247.ompweb.domain.isAndroidReleaseAsset +import io.github.kahme247.ompweb.domain.isAndroidReleasePage +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull +import okhttp3.OkHttpClient +import okhttp3.Request + +/** + * The public GitHub releases list of the ompweb repository. The app itself is + * distributed as an APK there (tags `android-v*`), so its update check + * is independent of the connected server. + * + * This request must not carry the session cookie and must never be pointed at + * the configured server: [HttpAndroidReleaseSource] takes an unauthenticated + * client and hard-codes the repository. + */ +interface AndroidReleaseSource { + /** Every release the repository lists, newest first; drafts included (filtered on read). */ + suspend fun releases(): List +} + +/** GitHub's REST API over a client without the app's session cookie jar. */ +class HttpAndroidReleaseSource( + private val client: OkHttpClient, + private val owner: String = DEFAULT_OWNER, + private val repo: String = DEFAULT_REPO, + private val userAgent: String = DEFAULT_USER_AGENT, +) : AndroidReleaseSource { + + override suspend fun releases(): List = withContext(Dispatchers.IO) { + val request = Request.Builder() + .url("https://api.github.com/repos/$owner/$repo/releases?per_page=100") + .header("Accept", "application/vnd.github+json") + .header("X-GitHub-Api-Version", "2022-11-28") + .header("User-Agent", userAgent) + .build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + // Offline, rate-limited (403/429) or a GitHub outage: the caller + // keeps its cache and stays silent, so there is nothing to retry here. + throw IOException("GitHub releases answered HTTP ${response.code}") + } + parseAndroidReleases(response.body?.string().orEmpty()) + } + } + + private companion object { + const val DEFAULT_OWNER = "kahme247" + const val DEFAULT_REPO = "ompweb" + const val DEFAULT_USER_AGENT = "ompweb-android" + } +} + +private val releaseJson = Json { ignoreUnknownKeys = true } + +/** + * Parses `GET /releases`. Entries with another tag prefix (`v0.5.2`, `omp-v…`) + * are dropped; a release without an `android-v*` tag is not for this app. + * Only a page under this repository's GitHub releases survives, because the + * banner opens it. Attached files are kept only when they point at this + * repository's release downloads; the APK itself is optional (an older + * release may have none), so the notice can still offer its page. + */ +fun parseAndroidReleases(body: String): List { + val root = runCatching { releaseJson.parseToJsonElement(body) }.getOrNull() as? JsonArray + ?: return emptyList() + return root.mapNotNull { element -> + val entry = element as? JsonObject ?: return@mapNotNull null + val tag = entry["tag_name"]?.jsonPrimitive?.contentOrNull ?: return@mapNotNull null + val version = androidVersionFromTag(tag) ?: return@mapNotNull null + val url = entry["html_url"]?.jsonPrimitive?.contentOrNull + ?.takeIf(::isAndroidReleasePage) + ?: return@mapNotNull null + AndroidRelease( + version = version, + htmlUrl = url, + prerelease = entry["prerelease"]?.jsonPrimitive?.booleanOrNull == true, + draft = entry["draft"]?.jsonPrimitive?.booleanOrNull == true, + assets = parseAndroidReleaseAssets(entry["assets"] as? JsonArray), + ) + } +} + +private fun parseAndroidReleaseAssets(entries: JsonArray?): List = + entries.orEmpty().mapNotNull { element -> + val asset = element as? JsonObject ?: return@mapNotNull null + val name = asset["name"]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() } + ?: return@mapNotNull null + val downloadUrl = asset["browser_download_url"]?.jsonPrimitive?.contentOrNull + ?.takeIf(::isAndroidReleaseAsset) + ?: return@mapNotNull null + AndroidReleaseAsset( + name = name, + downloadUrl = downloadUrl, + size = asset["size"]?.jsonPrimitive?.longOrNull ?: 0L, + ) + } diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/AndroidUpdateApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/AndroidUpdateApi.kt new file mode 100644 index 000000000..31b213b5c --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/AndroidUpdateApi.kt @@ -0,0 +1,135 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.AndroidUpdateCandidate +import io.github.kahme247.ompweb.domain.AndroidUpdateFeedResult +import io.github.kahme247.ompweb.domain.MAX_ANDROID_UPDATE_FEED_BYTES +import io.github.kahme247.ompweb.domain.newestAndroidRelease +import io.github.kahme247.ompweb.domain.parseAndroidUpdateFeed +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.ResponseBody +import okio.Buffer + +/** + * Where the app update is announced: GitHub Releases (default) or the user's + * custom feed. One interface, one controller, no second update path. + */ +interface AndroidUpdateSource { + /** + * The update [feedUrl] announces over [installedVersionName] / + * [installedVersionCode], or null when the installed build is current. + * A `null` [feedUrl] selects GitHub Releases. + * + * Failures (offline, HTTP error, an unusable feed) throw; the controller + * keeps its previous answer and shows "could not check". + */ + suspend fun latest( + feedUrl: String?, + installedVersionName: String, + installedVersionCode: Long, + ): AndroidUpdateCandidate? +} + +/** + * Reads both sources over one client that carries no cookie jar and no + * authorization header: nothing about the connected ompweb server leaves the + * device for an update check. The client enforces the plain-HTTP policy on + * every hop ([HttpPolicyInterceptor]). + * + * GitHub's release asset URLs redirect to a storage host, which is why the + * client allows cross-origin redirects; the redirect target is still checked + * against the scheme policy. + */ +class HttpAndroidUpdateSource( + private val github: AndroidReleaseSource, + private val client: OkHttpClient, + private val userAgent: String = DEFAULT_USER_AGENT, +) : AndroidUpdateSource { + + override suspend fun latest( + feedUrl: String?, + installedVersionName: String, + installedVersionCode: Long, + ): AndroidUpdateCandidate? = + if (feedUrl == null) { + githubLatest(installedVersionName) + } else { + customLatest(feedUrl, installedVersionCode) + } + + private suspend fun githubLatest(installedVersionName: String): AndroidUpdateCandidate? { + val release = newestAndroidRelease(github.releases(), installedVersionName) ?: return null + val apkName = "ompweb-${release.version}.apk" + val apk = release.assets.firstOrNull { it.name == apkName } + val checksum = release.assets.firstOrNull { it.name == "$apkName.sha256" } + return AndroidUpdateCandidate( + versionName = release.version, + versionCode = null, + pageUrl = release.htmlUrl, + apkUrl = apk?.downloadUrl, + apkSize = apk?.size?.takeIf { it > 0 }, + sha256Url = checksum?.downloadUrl, + ) + } + + private suspend fun customLatest(feedUrl: String, installedVersionCode: Long): AndroidUpdateCandidate? { + val body = fetchText(feedUrl, MAX_ANDROID_UPDATE_FEED_BYTES, "application/json") + val feed = when (val parsed = parseAndroidUpdateFeed(body, feedUrl)) { + is AndroidUpdateFeedResult.Parsed -> parsed.feed + is AndroidUpdateFeedResult.Invalid -> throw IOException(parsed.reason) + } + // Android decides installability by versionCode; a feed that does not + // raise it is not an update, whatever its versionName says. + if (feed.versionCode <= installedVersionCode) return null + return AndroidUpdateCandidate( + versionName = feed.versionName, + versionCode = feed.versionCode, + pageUrl = null, + apkUrl = feed.apkUrl, + apkSize = feed.size, + sha256 = feed.sha256, + notes = feed.notes, + ) + } + + private suspend fun fetchText(url: String, maxBytes: Long, accept: String?): String = + withContext(Dispatchers.IO) { + val request = Request.Builder() + .url(url) + .header("User-Agent", userAgent) + .apply { if (accept != null) header("Accept", accept) } + .build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("The update source answered HTTP ${response.code}") + } + response.body.readLimited(maxBytes) + } + } + + private companion object { + const val DEFAULT_USER_AGENT = "ompweb-android" + } +} + +/** + * Reads at most [maxBytes] from the body; a larger answer is refused instead of + * being buffered. A feed is a handful of fields, not an APK. + */ +internal fun ResponseBody.readLimited(maxBytes: Long): String { + val source = source() + val buffer = Buffer() + var total = 0L + while (true) { + val read = source.read(buffer, READ_CHUNK) + if (read == -1L) break + total += read + if (total > maxBytes) throw IOException("The update answer is too large.") + } + return buffer.readUtf8() +} + +private const val READ_CHUNK = 8L * 1024L diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/AndroidUpdateDownloader.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/AndroidUpdateDownloader.kt new file mode 100644 index 000000000..8dae24325 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/AndroidUpdateDownloader.kt @@ -0,0 +1,187 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.AndroidUpdateCandidate +import io.github.kahme247.ompweb.domain.MAX_ANDROID_UPDATE_BYTES +import io.github.kahme247.ompweb.domain.MAX_ANDROID_UPDATE_CHECKSUM_BYTES +import io.github.kahme247.ompweb.domain.isAndroidUpdateSizeAllowed +import io.github.kahme247.ompweb.domain.parseAndroidUpdateSha256Asset +import java.io.File +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** The downloaded APK and the digest the source announced for it. */ +data class AndroidUpdateDownload( + val file: File, + val expectedSha256: String, + val size: Long, +) + +/** Downloads an announced APK into the app's cache. */ +interface AndroidUpdateDownloader { + /** + * Downloads [candidate] and returns the file with the announced digest. + * [onProgress] runs with bytes received and the announced total. The + * caller deletes the file when the install finishes or fails. + * + * Cancelling the calling coroutine cancels the HTTP call and deletes the + * partial file. + */ + suspend fun download( + candidate: AndroidUpdateCandidate, + onProgress: (received: Long, total: Long) -> Unit, + ): AndroidUpdateDownload +} + +/** + * OkHttp download with a hard size cap, progress and cancellation. + * + * The URL and its digest come from the update source, never from the + * connected server. The checksum is either the feed's own field or the + * release's `.sha256` asset, fetched over the same cookie-free client. + */ +class HttpAndroidUpdateDownloader( + private val client: OkHttpClient, + private val cacheDir: File, + private val userAgent: String = "ompweb-android", +) : AndroidUpdateDownloader { + + override suspend fun download( + candidate: AndroidUpdateCandidate, + onProgress: (received: Long, total: Long) -> Unit, + ): AndroidUpdateDownload { + val apkUrl = candidate.apkUrl?.takeIf { it.isNotBlank() } + ?: throw IOException("This release has no APK to install.") + val expectedSize = candidate.apkSize + if (!isAndroidUpdateSizeAllowed(expectedSize)) { + throw IOException("The announced APK size is missing or too large.") + } + val announcedSize = expectedSize!! + val expectedSha256 = candidate.sha256?.let { sha -> + sha.takeIf { it.isNotBlank() } ?: throw IOException("The announced SHA-256 is empty.") + } ?: fetchChecksum(candidate.sha256Url) + ?: throw IOException("This release has no SHA-256 to verify the download.") + + val directory = cacheDir + if (!directory.isDirectory && !directory.mkdirs()) { + throw IOException("The update cache directory could not be created.") + } + // A previous attempt's file (process death during a download) must not + // be mistaken for this one. + directory.listFiles()?.forEach { existing -> existing.delete() } + val target = File(directory, "ompweb-update.apk") + try { + downloadToFile(apkUrl, target, announcedSize, onProgress) + if (target.length() != announcedSize) { + throw IOException("The download is incomplete.") + } + return AndroidUpdateDownload(target, expectedSha256, target.length()) + } catch (error: Throwable) { + target.delete() + throw error + } + } + + private suspend fun fetchChecksum(checksumUrl: String?): String? { + val url = checksumUrl?.takeIf { it.isNotBlank() } ?: return null + val text = withContext(Dispatchers.IO) { + val request = Request.Builder() + .url(url) + .header("User-Agent", userAgent) + .build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("The update's checksum file answered HTTP ${response.code}") + } + response.body.readLimited(MAX_ANDROID_UPDATE_CHECKSUM_BYTES) + } + } + return parseAndroidUpdateSha256Asset(text) + } + + private suspend fun downloadToFile( + url: String, + target: File, + expectedSize: Long, + onProgress: (received: Long, total: Long) -> Unit, + ) { + val request = Request.Builder() + .url(url) + .header("User-Agent", userAgent) + .build() + val call = client.newCall(request) + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { call.cancel() } + call.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + if (continuation.isCancelled) return + continuation.resumeWithException( + IOException("The update download failed: ${e.message}", e), + ) + } + + override fun onResponse(call: Call, response: Response) { + response.use { answer -> + try { + if (!answer.isSuccessful) { + throw IOException("The update download answered HTTP ${answer.code}") + } + val body = answer.body + val contentLength = body.contentLength() + if (contentLength >= 0 && contentLength != expectedSize) { + throw IOException("The download's size does not match the announced size.") + } + var total = 0L + var reported = 0L + target.outputStream().use { output -> + body.byteStream().use { input -> + val buffer = ByteArray(DOWNLOAD_CHUNK) + while (true) { + if (continuation.isCancelled) { + throw IOException("The download was canceled.") + } + val read = input.read(buffer) + if (read < 0) break + total += read + if (total > expectedSize || total > MAX_ANDROID_UPDATE_BYTES) { + throw IOException("The download is larger than announced.") + } + output.write(buffer, 0, read) + if (total - reported >= PROGRESS_STEP || total == expectedSize) { + reported = total + onProgress(total, expectedSize) + } + } + } + } + if (total != expectedSize) { + throw IOException("The download ended before the full file arrived.") + } + if (continuation.isCancelled) { + throw IOException("The download was canceled.") + } + onProgress(total, expectedSize) + continuation.resume(Unit) + } catch (error: Throwable) { + target.delete() + if (!continuation.isCancelled) continuation.resumeWithException(error) + } + } + } + }) + } + } + + private companion object { + const val DOWNLOAD_CHUNK = 64 * 1024 + const val PROGRESS_STEP = 64L * 1024L + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiCall.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiCall.kt new file mode 100644 index 000000000..e3fd280f0 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiCall.kt @@ -0,0 +1,21 @@ +package io.github.kahme247.ompweb.net + +import java.io.IOException +import kotlinx.coroutines.CancellationException + +/** + * Runs one API call and reports every failure as an [ApiError]: nothing but + * cancellation may escape a view-model coroutine (an uncaught IOException + * would crash the app instead of showing a notice). + */ +suspend fun captureApi(block: suspend () -> T): Result = try { + Result.success(block()) +} catch (cancelled: CancellationException) { + throw cancelled +} catch (error: ApiError) { + Result.failure(error) +} catch (error: IOException) { + Result.failure(ApiError.Network(error)) +} catch (error: Exception) { + Result.failure(ApiError.Network(IOException(error))) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiClient.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiClient.kt new file mode 100644 index 000000000..3991e3c47 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiClient.kt @@ -0,0 +1,1557 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.ComposerImage +import io.github.kahme247.ompweb.domain.SkillDiagnosticsSnapshot +import io.github.kahme247.ompweb.domain.SlashCommandItem +import io.github.kahme247.ompweb.domain.parseSkillDiagnosticsSnapshot +import io.github.kahme247.ompweb.domain.parseSlashCommands +import java.io.EOFException +import java.io.IOException +import java.net.URLEncoder +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import okhttp3.Call +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +/** One NDJSON frame from `POST /api/projects/clone`. */ +sealed interface CloneFrame { + data class Output(val text: String) : CloneFrame + + data class Done(val path: String) : CloneFrame + + data class Cancelled(val path: String) : CloneFrame + + data class Error(val message: String, val code: String?) : CloneFrame +} + +/** + * One raw HTTP exchange for callers that need multipart uploads or binary + * bodies ([io.github.kahme247.ompweb.net.SttApi]). + */ +class RawExchange(val status: Int, val body: ByteArray, val contentType: String?) { + fun text(): String = body.toString(Charsets.UTF_8) +} + +/** The self-contained HTML export of one session, ready for SAF/share. */ +data class ExportedSession(val fileName: String, val body: ByteArray) + +/** The session list of one origin plus the ETag it was delivered with. */ +private data class CachedSessions(val etag: String, val list: SessionList) + +/** + * One `GET /api/sessions` answer: the list **and** the validator that belongs + * to that very response. Callers that write the list to disk persist both from + * this pair, never the list from one answer with the ETag of another. + */ +data class SessionListAnswer(val list: SessionList, val etag: String?) + +/** What `POST /api/web-auth/session` told us. */ +enum class LoginOutcome { + Success, + + /** 401: the password is wrong. */ + WrongPassword, + + /** 404: the server has no password protection; no cookie is needed. */ + PasswordDisabled, + + /** 413: the request body was too large for the server's cap. */ + TooLarge, +} + +/** The answer to one `/api/events` subscribe/unsubscribe POST. */ +sealed interface EventPostResult { + data object Ok : EventPostResult + + /** 404 `unknown_connection`: the server restarted; reconnect and resubscribe. */ + data object ConnectionUnknown : EventPostResult + + /** The subscription itself was refused (409 for a wrapper-less session, 400 for a bad channel). */ + data class Rejected(val status: Int, val code: String?, val message: String) : EventPostResult +} + +/** + * The app's HTTP entry to one ompweb server. + * + * Deliberately sends no `Origin` and no `Sec-Fetch-Site`: ompweb's origin gate + * answers those with 403, because a native client is not a browser. Every + * request carries the session cookie of exactly the configured origin (see + * `OriginBoundCookieJar`), and every 401 is reported to [onUnauthorized] so + * the app can return to the password screen from anywhere. + */ +class ApiClient( + private val client: OkHttpClient, + private val origin: () -> ServerAddress?, + private val onUnauthorized: (ServerAddress) -> Unit = {}, +) : ChatApi, ComposerControlsApi, SkillDiagnosticsApi, SessionSearchApi { + + private val json = Json { + ignoreUnknownKeys = true + explicitNulls = false + } + + /** + * One daemon thread per running clone stream. Kept off [Dispatchers.IO] + * because the read cannot be interrupted: the thread ends when the call + * is cancelled. + */ + private val cloneExecutor = Executors.newCachedThreadPool { runnable -> + Thread(runnable, "clone-stream").apply { isDaemon = true } + } + + /** + * Derived clients for [openEventStream], one per requested heartbeat. + * Built lazily and reused: every builder copy allocates a new dispatcher + * wrapper and connection pool view. + */ + private val eventStreamClients = ConcurrentHashMap() + + /** + * The client of [agentState] — the same policy plus a hard per-call + * timeout. The submit gate waits ten seconds for that answer, so a server + * that never answers must cost the same ten seconds instead of the + * 60 s read timeout (a trickling body would otherwise hold the call, and + * the composer, indefinitely). Built once: the copy shares pool, + * dispatcher and cookie jar. + */ + private val liveStateClient: OkHttpClient by lazy { + client.newBuilder() + .callTimeout(LIVE_STATE_READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + .build() + } + + /** + * Last session list per origin together with its ETag, so a poll that the + * server answers with 304 keeps the list the UI already shows (A23, web + * `SessionSidebar`). Keyed by origin: a server switch must never reuse the + * previous installation's list. + */ + private val sessionsCache = ConcurrentHashMap() + + /** + * `GET /api/sessions/{id}` requests currently on the wire, keyed by origin + * and id. Opening a session asks two view models for the same body within + * milliseconds of each other (`SessionsViewModel.loadDetail` for the + * header, `ChatViewModel.loadMeta` for the branch tree); sharing the + * request is what keeps the transcript from crossing the wire twice. + * + * A finished answer stays here for [DETAIL_SHARE_WINDOW_MILLIS] so a + * request that starts right after the first one still joins it instead of + * opening a second transfer. After that every caller asks the server + * again — this is request sharing, not a content cache. + */ + private val detailFlights = HashMap() + private val detailLock = Any() + + /** + * Seeds the ETag cache with a list read from disk, so the first request of + * a cold start can be a conditional one and a `304` keeps exactly that + * list instead of failing for a missing entry. + */ + fun primeSessionList(list: SessionList, etag: String?) { + val cacheKey = sessionsCacheKey() ?: return + if (etag.isNullOrEmpty()) return + synchronized(sessionsCache) { sessionsCache[cacheKey] = CachedSessions(etag, list) } + } + + /** + * Drops every answer this client keeps in memory: the list/ETag per origin + * and the shared detail answers (each carries a whole transcript). Called + * when the signed-in user's data is dropped, so nothing of that + * installation survives on the heap. + */ + fun clearMemoryCaches() { + synchronized(sessionsCache) { sessionsCache.clear() } + synchronized(detailLock) { detailFlights.clear() } + } + + private fun sessionsCacheKey(): String? { + val requestUrl = url("api/sessions") + return "${requestUrl.scheme}://${requestUrl.host}:${requestUrl.port}" + } + + suspend fun sessions(): SessionList = sessionListAnswer().list + + /** + * The list together with the validator of **this** answer. A caller that + * persists the body must use this pair: [sessionsCache] may already hold a + * newer answer by the time the write runs, and a body paired with another + * response's ETag makes the server answer `304` for a list it never sent. + */ + suspend fun sessionListAnswer(): SessionListAnswer = withContext(Dispatchers.IO) { + val requestUrl = url("api/sessions") + val cacheKey = "${requestUrl.scheme}://${requestUrl.host}:${requestUrl.port}" + val cached = synchronized(sessionsCache) { sessionsCache[cacheKey] } + val request = Request.Builder() + .url(requestUrl) + .header("Accept", "application/json") + .apply { if (cached != null) header("If-None-Match", cached.etag) } + .get() + .build() + val response = perform(request) + if (response.status == 304) { + // Without an entry the 304 is a server bug; surfacing it as an + // error is better than pretending the list is empty. + val kept = cached ?: throw errorFor(304, response.body, response.origin) + return@withContext SessionListAnswer(kept.list, kept.etag) + } + val body = requireSuccess(response) + val list = decode(body) { json.decodeFromString(SessionList.serializer(), body) } + val etag = response.etag + synchronized(sessionsCache) { + if (etag == null) sessionsCache.remove(cacheKey) else sessionsCache[cacheKey] = CachedSessions(etag, list) + } + SessionListAnswer(list, etag) + } + + // --- Projects ----------------------------------------------------------- + + /** `GET /api/app-info`: the server's name/version handshake (intro version line). */ + suspend fun appInfo(): AppInfo = withContext(Dispatchers.IO) { + val body = requireSuccess(perform(get("api/app-info"))) + decode(body) { json.decodeFromString(AppInfo.serializer(), body) } + } + + suspend fun projects(): ProjectList = withContext(Dispatchers.IO) { + val body = requireSuccess(perform(get("api/projects"))) + decode(body) { json.decodeFromString(ProjectList.serializer(), body) } + } + + /** + * `POST /api/projects {cwd, launchConfig?}`. The server validates the + * directory and resolves Git worktrees to their main project root + * (`app/api/projects/route.ts` POST). + */ + suspend fun addProject(cwd: String, launchConfig: ProjectLaunchConfig? = null): ManagedProject = + withContext(Dispatchers.IO) { + val payload = buildJsonObject { + put("cwd", cwd) + if (launchConfig != null) put("launchConfig", launchConfig.toJson()) + } + val response = perform(postJson("api/projects", payload.toString())) + if (response.status !in 200..299) throw requestError(response) + val body = decode(response.body) { json.decodeFromString(ProjectResponse.serializer(), response.body) } + body.project ?: throw ApiError.Http(response.status, response.body) + } + + suspend fun updateProjectAlias(cwd: String, alias: String) = withContext(Dispatchers.IO) { + patchProject(buildJsonObject { put("cwd", cwd); put("alias", alias) }) + } + + /** `launchConfig = null` clears the stored configuration. */ + suspend fun updateProjectLaunchConfig(cwd: String, launchConfig: ProjectLaunchConfig?) = withContext(Dispatchers.IO) { + patchProject( + buildJsonObject { + put("cwd", cwd) + put("launchConfig", launchConfig?.toJson() ?: JsonNull) + }, + ) + } + + /** + * Persists one whole-list order as a single atomic batched PATCH — the + * server applies every entry in one registry save, so a drag reorder can + * never interleave with another writer (`app/api/projects/route.ts` PATCH). + */ + suspend fun reorderProjects(paths: List) = withContext(Dispatchers.IO) { + val updates = buildJsonArray { + paths.forEachIndexed { index, path -> + add( + buildJsonObject { + put("cwd", path) + put("sortOrder", JsonPrimitive(index)) + }, + ) + } + } + patchProject(buildJsonObject { put("updates", updates) }) + } + + suspend fun deleteProject(cwd: String) = withContext(Dispatchers.IO) { + val payload = buildJsonObject { put("cwd", cwd) } + val response = perform(postJson("api/projects", payload.toString(), method = "DELETE")) + if (response.status !in 200..299) throw requestError(response) + } + + private fun patchProject(payload: JsonObject) { + val response = perform(postJson("api/projects", payload.toString(), method = "PATCH")) + if (response.status !in 200..299) throw requestError(response) + } + + // --- Sessions ----------------------------------------------------------- + + override suspend fun sessionDetail(id: String): SessionDetail = withContext(Dispatchers.IO) { + sharedSessionDetail(id) + } + + /** + * The same request without the sharing window. An explicit reload must see + * what the server has *now*: a session that was just deleted answers 404 + * here, instead of repeating the answer another view model received two + * seconds ago. + */ + suspend fun sessionDetailFresh(id: String): SessionDetail = withContext(Dispatchers.IO) { + invalidateSessionDetail(id) + fetchSessionDetail(id) + } + + /** + * A mutation changed what `GET /api/sessions/{id}` answers (rename, + * auto-name, delete, archive). The shared answer of the last request must + * never be handed to the next caller: an open right after a delete would + * otherwise resurrect the session from the two-second sharing window. + */ + fun invalidateSessionDetail(id: String) { + // Without a configured origin there is no key and nothing to forget. + if (origin() == null) return + synchronized(detailLock) { detailFlights.remove(detailKey(id)) } + } + + private fun detailKey(id: String): String { + val origin = url("api/sessions/${encodePathSegment(id)}") + return "${origin.scheme}://${origin.host}:${origin.port}|$id" + } + + /** + * One in-flight `GET /api/sessions/{id}` per origin and id. The first + * caller fetches; every caller that arrives while it runs (or within + * [DETAIL_SHARE_WINDOW_MILLIS] after it) gets the same answer. + */ + private suspend fun sharedSessionDetail(id: String): SessionDetail { + val key = detailKey(id) + val flight: DetailFlight + val owner: Boolean + synchronized(detailLock) { + val now = System.currentTimeMillis() + // Bounded: an expired answer carries a whole transcript, so the + // table never keeps more than the requests of the last window. + detailFlights.entries.removeAll { !it.value.isActive() && now - it.value.completedAt > DETAIL_SHARE_WINDOW_MILLIS } + val running = detailFlights[key] + if (running != null && (running.isActive() || running.isFresh(now))) { + flight = running + owner = false + } else { + flight = DetailFlight(CompletableDeferred()) + detailFlights[key] = flight + owner = true + } + } + if (!owner) return flight.result.await() + return try { + val detail = fetchSessionDetail(id) + flight.completedAt = System.currentTimeMillis() + flight.result.complete(detail) + detail + } catch (error: Throwable) { + synchronized(detailLock) { detailFlights.remove(key, flight) } + // The owner's own cancellation is its business: every joiner is an + // unrelated caller that still wants the body. Completing their + // shared answer with the owner's CancellationException would cancel + // their coroutine instead of letting them show/retry a failure. + flight.result.completeExceptionally( + if (error is CancellationException) IOException("Request cancelled") else error, + ) + throw error + } + } + + private class DetailFlight(val result: CompletableDeferred) { + @Volatile + var completedAt: Long = 0 + + fun isActive(): Boolean = !result.isCompleted + + fun isFresh(now: Long = System.currentTimeMillis()): Boolean = + completedAt > 0 && now - completedAt <= DETAIL_SHARE_WINDOW_MILLIS + } + + private suspend fun fetchSessionDetail(id: String): SessionDetail { + val body = requireSuccess(perform(get("api/sessions/${encodePathSegment(id)}"))) + return decode(body) { json.decodeFromString(SessionDetail.serializer(), body) } + } + + /** `PATCH /api/sessions/{id} {name}` — the server persists the title. */ + suspend fun renameSession(id: String, name: String) = withContext(Dispatchers.IO) { + val payload = buildJsonObject { put("name", name) } + val response = perform(postJson("api/sessions/${encodePathSegment(id)}", payload.toString(), method = "PATCH")) + if (response.status !in 200..299) throw requestError(response) + } + + suspend fun deleteSession(id: String) = withContext(Dispatchers.IO) { + val response = perform(postJson("api/sessions/${encodePathSegment(id)}", "{}", method = "DELETE")) + if (response.status !in 200..299) throw requestError(response) + } + + /** `POST /api/sessions/{id}/archive` — refuses parents with 409. */ + suspend fun archiveSession(id: String) = withContext(Dispatchers.IO) { + val response = perform(postJson("api/sessions/${encodePathSegment(id)}/archive", "{}")) + if (response.status !in 200..299) throw requestError(response) + } + + suspend fun autoNameSession(id: String): AutoNameResponse = withContext(Dispatchers.IO) { + val response = perform(postJson("api/sessions/${encodePathSegment(id)}/auto-name", "{}")) + if (response.status !in 200..299) throw requestError(response) + decode(response.body) { json.decodeFromString(AutoNameResponse.serializer(), response.body) } + } + + /** `POST /api/sessions/import {fileName, content}` — a native omp JSONL. */ + suspend fun importSession(fileName: String, content: String) = withContext(Dispatchers.IO) { + val payload = buildJsonObject { + put("fileName", fileName) + put("content", content) + } + val response = perform(postJson("api/sessions/import", payload.toString())) + if (response.status !in 200..299) throw requestError(response) + } + + /** + * `GET /api/sessions/messages/search?q=&limit=&cursor=` — read-only + * full-text search over the session files. The server bounds every page + * (time, bytes, result count); [cursor] asks for the next slice. + * + * The older `/api/sessions/search` is **not** this route: servers from + * 0.6.0 on use it for the sidebar ranking and answer NDJSON, so the app + * only ever calls the `messages/` path (guarded by + * `capabilities.messageSearch`). + */ + override suspend fun searchSessions( + query: String, + limit: Int, + cursor: String?, + ): SessionSearchPage = withContext(Dispatchers.IO) { + val params = buildString { + append("q=").append(encodeQuery(query)) + append("&limit=").append(limit.coerceIn(1, 100)) + if (!cursor.isNullOrBlank()) append("&cursor=").append(encodeQuery(cursor)) + } + val response = perform(get("api/sessions/messages/search?$params")) + // The route answers errors as `{error, code}` (a busy server sends 429 + // `search_busy`), so the UI can localize them instead of printing + // "HTTP 429". + if (response.status !in 200..299) throw requestError(response) + val body = response.body + decode(body) { json.decodeFromString(SessionSearchPage.serializer(), body) } + } + + /** The self-contained HTML of `GET /api/sessions/{id}/export`. */ + suspend fun exportSession(id: String): ExportedSession = withContext(Dispatchers.IO) { + val request = Request.Builder() + .url(url("api/sessions/${encodePathSegment(id)}/export")) + .header("Accept", "text/html") + .get() + .build() + val response = try { + client.newCall(request).execute() + } catch (error: IOException) { + throw ApiError.Network(error) + } + val origin = request.url.let { ServerAddress(it.scheme, it.host, it.port) } + response.use { + if (it.code !in 200..299) throw requestError(RawResponse(it.code, readCappedBody(it), origin)) + val name = fileNameFromContentDisposition(it.header("Content-Disposition")) + ?: "omp-session-${id.take(12)}.html" + // Read with the same cap as every other route: `bytes()` would + // allocate an unbounded (chunked) body before the size check. + ExportedSession(name, readCappedBytes(it, MAX_EXPORT_BYTES)) + } + } + + /** `POST /api/agent/new` with `type:"ensure_session"` returns the real id. */ + suspend fun createSession( + cwd: String, + toolNames: List?, + provider: String?, + modelId: String?, + thinkingLevel: String?, + advisor: Boolean, + ): String = withContext(Dispatchers.IO) { + val payload = buildJsonObject { + put("cwd", cwd) + put("type", "ensure_session") + if (toolNames != null) put("toolNames", buildJsonArray { toolNames.forEach { add(JsonPrimitive(it)) } }) + if (provider != null && modelId != null) { + put("provider", provider) + put("modelId", modelId) + } + if (!thinkingLevel.isNullOrBlank() && thinkingLevel != "auto") put("thinkingLevel", thinkingLevel) + if (advisor) put("advisor", true) + } + val response = perform(postJson("api/agent/new", payload.toString())) + if (response.status !in 200..299) throw requestError(response) + val body = decode(response.body) { json.decodeFromString(AgentNewResponse.serializer(), response.body) } + body.sessionId.takeIf { it.isNotBlank() } ?: throw ApiError.Http(response.status, response.body) + } + + /** `POST /api/agent/{id} {type:"prompt"}` with images and streaming behavior. */ + override suspend fun prompt( + sessionId: String, + message: String, + images: List, + streamingBehavior: String?, + force: Boolean, + ): JsonElement? = sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "prompt") + put("message", message) + if (images.isNotEmpty()) put("images", imagesJson(images)) + if (!streamingBehavior.isNullOrBlank()) put("streamingBehavior", streamingBehavior) + }, + force = force, + ) + + /** `{type:"steer", message, images?}` — into the running turn. */ + override suspend fun steer( + sessionId: String, + message: String, + images: List, + ): JsonElement? = sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "steer") + put("message", message) + if (images.isNotEmpty()) put("images", imagesJson(images)) + }, + ) + + /** `{type:"follow_up", message, images?}` — queued behind the running turn. */ + override suspend fun followUp( + sessionId: String, + message: String, + images: List, + ): JsonElement? = sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "follow_up") + put("message", message) + if (images.isNotEmpty()) put("images", imagesJson(images)) + }, + ) + + override suspend fun bash(sessionId: String, command: String, excludeFromContext: Boolean): JsonElement? = + sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "bash") + put("command", command) + put("excludeFromContext", excludeFromContext) + }, + ) + + override suspend fun abortBash(sessionId: String): JsonElement? = + sendAgentCommand(sessionId, buildJsonObject { put("type", "abort_bash") }) + + override suspend fun abortAndRestoreQueue(sessionId: String): QueueRestore = withContext(Dispatchers.IO) { + val data = sendAgentCommand(sessionId, buildJsonObject { put("type", "abort_and_restore_queue") }) + val root = data as? JsonObject + QueueRestore( + steering = queuedEntries(root?.get("steering")), + followUp = queuedEntries(root?.get("followUp")), + ) + } + + override suspend fun removeQueuedMessage( + sessionId: String, + message: String, + queue: String, + ): QueuedMessageRemoval = withContext(Dispatchers.IO) { + val data = sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "remove_queued_message") + put("message", message) + put("queue", queue) + }, + ) + val root = data as? JsonObject + QueuedMessageRemoval( + removed = (root?.get("removed") as? JsonPrimitive)?.booleanOrNull == true, + images = imagesFromRpc(root?.get("images")), + ) + } + + override suspend fun promoteQueuedMessage(sessionId: String, message: String): Boolean = withContext(Dispatchers.IO) { + val data = sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "promote_queued_message") + put("message", message) + }, + ) + val root = data as? JsonObject + (root?.get("promoted") as? JsonPrimitive)?.booleanOrNull == true + } + + override suspend fun commands(sessionId: String): List = withContext(Dispatchers.IO) { + val data = sendAgentCommand(sessionId, buildJsonObject { put("type", "get_commands") }) + parseSlashCommands((data as? JsonObject)?.get("commands")) + } + + override suspend fun skillDiagnostics(sessionId: String): SkillDiagnosticsSnapshot? = + parseSkillDiagnosticsSnapshot( + sendAgentCommand(sessionId, buildJsonObject { put("type", "get_skill_diagnostics") }), + ) + + override suspend fun setSkillStartupDiagnostics( + sessionId: String, + enabled: Boolean, + ): SkillDiagnosticsSnapshot? = parseSkillDiagnosticsSnapshot( + sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "set_skill_startup_diagnostics") + put("enabled", enabled) + }, + ), + ) + + override suspend fun compact(sessionId: String, customInstructions: String?): JsonElement? = sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "compact") + if (!customInstructions.isNullOrBlank()) put("customInstructions", customInstructions) + }, + ) + + override suspend fun reload(sessionId: String): JsonElement? = + sendAgentCommand(sessionId, buildJsonObject { put("type", "reload") }) + + override suspend fun setSessionName(sessionId: String, name: String): JsonElement? = sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "set_session_name") + put("name", name) + }, + ) + + override suspend fun lastAssistantText(sessionId: String): String? = withContext(Dispatchers.IO) { + val data = sendAgentCommand(sessionId, buildJsonObject { put("type", "get_last_assistant_text") }) + ((data as? JsonObject)?.get("text") as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotEmpty() } + } + + override suspend fun skills(cwd: String): SkillsResponse = withContext(Dispatchers.IO) { + val body = requireSuccess(perform(get("api/skills?cwd=${encodeQuery(cwd)}"))) + decode(body) { json.decodeFromString(SkillsResponse.serializer(), body) } + } + + // --- Chat (Phase 5) ----------------------------------------------------- + + /** + * `GET /api/agent/{id}` — `{running, alive, state}` for reconciliation. + * + * The chat's submit gate waits at most ten seconds for this answer, so the + * read is bounded and cancellable on its own: a blocking `execute()` only + * ends at a socket timeout (60 s read here, unbounded for a trickling + * body), and a coroutine timeout cannot interrupt it. [liveStateClient] + * caps a server that never answers, and cancelling the waiting coroutine + * closes the call, so the submit's bound is a real bound. + */ + override suspend fun agentState(sessionId: String): JsonObject = withContext(Dispatchers.IO) { + val body = requireSuccess( + performCancellable(liveStateClient, get("api/agent/${encodePathSegment(sessionId)}")), + ) + decode(body) { json.parseToJsonElement(body).jsonObject } + } + + /** + * `GET /api/sessions` — the ids the server runs a turn for right now + * (`runningSessionIds`, the session list's own registry). Used only as the + * fallback authority for a 503 that does not report the run state, so the + * shared (ETag-revalidated) list read is reused instead of a new call. + */ + override suspend fun runningSessionIds(): List = sessions().runningSessionIds + + /** `GET /api/github-repo?cwd=` — `owner/repo` for the checkout, or null. */ + override suspend fun githubRepo(cwd: String): String? = withContext(Dispatchers.IO) { + val body = requireSuccess(perform(get("api/github-repo?cwd=${encodeQuery(cwd)}"))) + decode(body) { + json.parseToJsonElement(body).jsonObject["repo"]?.jsonPrimitive?.contentOrNull + } + } + + /** [agentCommand] with the session's advisor flag off. */ + private suspend fun sendAgentCommand( + sessionId: String, + command: JsonObject, + force: Boolean = false, + ): JsonElement? = + agentCommand(sessionId, command, advisor = false, force = force) + + /** + * `POST /api/agent/{id}` — every omp RPC command (`prompt`, `abort`, + * `fork`, `get_state`, …). The route wraps the result in + * `{success, data}`; `data` is null for fire-and-forget commands. + * `?advisor=1` marks commands of an advisor-enabled session, `?force=1` + * skips the gate against a session another omp process owns. + */ + override suspend fun agentCommand( + sessionId: String, + command: JsonObject, + advisor: Boolean, + force: Boolean, + ): JsonElement? = + withContext(Dispatchers.IO) { + val query = buildList { + if (advisor) add("advisor=1") + if (force) add("force=1") + }.joinToString("&") + val path = "api/agent/${encodePathSegment(sessionId)}" + if (query.isEmpty()) "" else "?$query" + val response = perform(postJson(path, command.toString())) + if (response.status !in 200..299) throw requestError(response) + val root = decode(response.body) { json.parseToJsonElement(response.body).jsonObject } + val failed = root["success"]?.jsonPrimitive?.booleanOrNull == false || root["error"] != null + if (failed) { + val code = root["code"]?.jsonPrimitive?.contentOrNull + val message = root["error"]?.jsonPrimitive?.contentOrNull ?: "The command failed" + throw ApiError.ServerResponse(response.status, code, message) + } + root["data"] + } + + /** + * `GET /api/sessions/{id}/context?sync=1` — one display-history page plus + * the live snapshot. + */ + override suspend fun contextSync( + sessionId: String, + cursor: String?, + leafId: String?, + includePreCompaction: Boolean, + deferThinking: Boolean, + deferMedia: Boolean, + ): JsonObject = withContext(Dispatchers.IO) { + val query = buildList { + add("sync=1") + if (cursor != null) add("cursor=${encodeQuery(cursor)}") + if (leafId != null) add("leafId=${encodeQuery(leafId)}") + add("includePreCompaction=${if (includePreCompaction) 1 else 0}") + if (deferThinking) add("deferThinking=1") + if (deferMedia) add("deferMedia=1") + }.joinToString("&") + val body = requireSuccess(perform(get("api/sessions/${encodePathSegment(sessionId)}/context?$query"))) + decode(body) { json.parseToJsonElement(body).jsonObject } + } + + /** + * `GET /api/sessions/{id}/context` — the whole display history (web + * `loadContext`, one request per open). Older rows are revealed lazily by + * the transcript window, not by additional history requests. + */ + override suspend fun contextFull( + sessionId: String, + leafId: String?, + includePreCompaction: Boolean, + deferThinking: Boolean, + deferMedia: Boolean, + ): JsonObject = withContext(Dispatchers.IO) { + val body = contextFullRaw(sessionId, leafId, includePreCompaction, deferThinking, deferMedia) + decode(body) { json.parseToJsonElement(body).jsonObject } + } + + /** + * The same response as [contextFull], as the exact body the server sent. + * The chat writes that body to the offline cache unchanged, so a cached + * open parses byte-for-byte what a live open would have parsed. + */ + override suspend fun contextFullRaw( + sessionId: String, + leafId: String?, + includePreCompaction: Boolean, + deferThinking: Boolean, + deferMedia: Boolean, + ): String = withContext(Dispatchers.IO) { + val query = buildList { + if (leafId != null) add("leafId=${encodeQuery(leafId)}") + if (includePreCompaction) add("includePreCompaction=1") + if (deferThinking) add("deferThinking=1") + if (deferMedia) add("deferMedia=1") + }.joinToString("&") + val suffix = if (query.isEmpty()) "" else "?$query" + requireSuccess(perform(get("api/sessions/${encodePathSegment(sessionId)}/context$suffix"))) + } + + /** `GET .../thinking?blockIndex=` for a deferred thinking block. */ + override suspend fun thinking(sessionId: String, entryId: String, blockIndex: Int): String = + withContext(Dispatchers.IO) { + val path = "api/sessions/${encodePathSegment(sessionId)}/entries/${encodePathSegment(entryId)}/thinking" + + "?blockIndex=$blockIndex" + val body = requireSuccess(perform(get(path))) + decode(body) { json.decodeFromString(ThinkingResponse.serializer(), body) }.thinking + } + + /** `POST /api/agent/{id} {type:"fork", entryId}`. */ + override suspend fun forkSession(sessionId: String, entryId: String): ForkOutcome = + withContext(Dispatchers.IO) { + val data = sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "fork") + put("entryId", entryId) + }, + ) + if (data == null || data is JsonNull) { + throw ApiError.ServerResponse(200, null, "The session could not be forked") + } + val text = data.toString() + decode(text) { json.decodeFromString(ForkOutcome.serializer(), text) } + } + + /** `POST /api/agent/{id} {type:"abort"}`. */ + override suspend fun abort(sessionId: String): JsonElement? = sendAgentCommand( + sessionId, + buildJsonObject { put("type", "abort") }, + ) + + /** `POST /api/agent/{id} {type:"abort_retry"}` — cancel the retry backoff. */ + override suspend fun abortRetry(sessionId: String): JsonElement? = sendAgentCommand( + sessionId, + buildJsonObject { put("type", "abort_retry") }, + ) + + /** A referenced bash-output file of the current session, if still available. */ + override suspend fun bashOutput(sessionId: String, path: String): BashOutputResult = withContext(Dispatchers.IO) { + val request = get("api/agent/${encodePathSegment(sessionId)}/bash-output?path=${encodeQuery(path)}") + val response = perform(request) + when (response.status) { + in 200..299 -> { + val root = decode(response.body) { json.parseToJsonElement(response.body).jsonObject } + val output = root["data"]?.let { data -> + (data as? JsonObject)?.get("output")?.jsonPrimitive?.contentOrNull + } + BashOutputResult.Text(output ?: "") + } + 413 -> { + val root = try { + json.parseToJsonElement(response.body).jsonObject + } catch (_: Exception) { + null + } + val data = root?.get("data") as? JsonObject + BashOutputResult.TooLarge( + size = data?.get("size")?.jsonPrimitive?.contentOrNull?.toLongOrNull() ?: 0L, + maxBytes = data?.get("maxBytes")?.jsonPrimitive?.contentOrNull?.toLongOrNull() ?: 0L, + ) + } + else -> throw requestError(response) + } + } + + /** The same bash output as a text download, for SAF "save full output". */ + override suspend fun bashOutputBytes(sessionId: String, path: String): ByteArray = withContext(Dispatchers.IO) { + val request = Request.Builder() + .url(url("api/agent/${encodePathSegment(sessionId)}/bash-output?path=${encodeQuery(path)}&download=1")) + .header("Accept", "text/plain, */*") + .get() + .build() + val response = try { + client.newCall(request).execute() + } catch (error: IOException) { + throw ApiError.Network(error) + } + val origin = request.url.let { ServerAddress(it.scheme, it.host, it.port) } + response.use { + if (it.code !in 200..299) throw requestError(RawResponse(it.code, readCappedBody(it), origin)) + readCappedBytes(it, MAX_BASH_OUTPUT_BYTES) + } + } + + /** `GET /api/sessions/{id}/subagents/{subagentId}?mode=completion`. */ + override suspend fun subagentCompletion(sessionId: String, subagentId: String): SubagentCompletion = + withContext(Dispatchers.IO) { + val path = "api/sessions/${encodePathSegment(sessionId)}/subagents/${encodePathSegment(subagentId)}" + + "?mode=completion" + val body = requireSuccess(perform(get(path))) + decode(body) { json.decodeFromString(SubagentCompletion.serializer(), body) } + } + + /** `{type:"get_subagents"}` — the live registry roster. */ + override suspend fun subagents(sessionId: String): JsonElement? = sendAgentCommand( + sessionId, + buildJsonObject { put("type", "get_subagents") }, + ) + + /** `GET /api/sessions/{id}/subagents` — the on-disk roster. */ + override suspend fun subagentHistory(sessionId: String): JsonElement? = withContext(Dispatchers.IO) { + val path = "api/sessions/${encodePathSegment(sessionId)}/subagents" + val response = perform(get(path)) + // A 404 means "no on-disk history yet"; the caller treats it as empty. + if (response.status == 404) return@withContext null + val body = requireSuccess(response) + decode(body) { json.parseToJsonElement(body) } + } + + /** `{type:"get_subagent_messages", …}` — one page from the live registry. */ + override suspend fun subagentTranscript( + sessionId: String, + subagentId: String, + sessionFile: String?, + fromByte: Long, + ): JsonElement? = sendAgentCommand( + sessionId, + buildJsonObject { + put("type", "get_subagent_messages") + put("subagentId", subagentId) + if (sessionFile != null) put("sessionFile", sessionFile) + put("fromByte", fromByte) + }, + ) + + /** `GET .../subagents/{id}?fromByte=` — the disk fallback for a page. */ + override suspend fun subagentTranscriptDisk(sessionId: String, subagentId: String, fromByte: Long): JsonElement? = + withContext(Dispatchers.IO) { + val path = "api/sessions/${encodePathSegment(sessionId)}/subagents/${encodePathSegment(subagentId)}" + + "?fromByte=$fromByte" + val response = perform(get(path)) + if (response.status == 404) return@withContext null + val body = requireSuccess(response) + decode(body) { json.parseToJsonElement(body) } + } + + /** `POST /api/cwd/validate` — a 400 answers with a `{error, code}` the UI localizes. */ + override suspend fun validateCwd(cwd: String): CwdValidation = withContext(Dispatchers.IO) { + val payload = buildJsonObject { put("cwd", cwd) } + val response = perform(postJson("api/cwd/validate", payload.toString())) + if (response.status in 200..299) { + val root = decode(response.body) { json.parseToJsonElement(response.body).jsonObject } + CwdValidation.Valid( + cwd = root["cwd"]?.jsonPrimitive?.contentOrNull ?: cwd, + projectRoot = root["projectRoot"]?.jsonPrimitive?.contentOrNull, + ) + } else { + if (response.status == 401) onUnauthorized(response.origin) + val parsed = parseErrorBody(response.body) + CwdValidation.Invalid(parsed.code, parsed.message) + } + } + + /** + * `GET /api/files/{segments}?type=read` — text types answer JSON + * (`{content, language, size}`), binaries stream their bytes. `FilesApi` + * is the only caller: the file panel reads every byte through it. + */ + suspend fun fileRead(segments: List, sessionId: String?): FileReadResult = withContext(Dispatchers.IO) { + val encoded = segments.joinToString("/") { encodePathSegment(it) } + val query = if (sessionId.isNullOrBlank()) { + "api/files/$encoded?type=read" + } else { + "api/files/$encoded?type=read&sessionId=${encodePathSegment(sessionId)}" + } + val request = Request.Builder() + .url(url(query)) + .header("Accept", "application/json, */*") + .get() + .build() + val response = try { + client.newCall(request).execute() + } catch (error: IOException) { + throw ApiError.Network(error) + } + val origin = request.url.let { ServerAddress(it.scheme, it.host, it.port) } + response.use { + if (it.code !in 200..299) throw requestError(RawResponse(it.code, readCappedBody(it), origin)) + val mimeType = it.header("Content-Type").orEmpty() + val bytes = readCappedBytes(it, MAX_FILE_BYTES) + if (mimeType.startsWith("application/json")) { + val text = bytes.toString(Charsets.UTF_8) + val file = decode(text) { json.decodeFromString(FileText.serializer(), text) } + FileReadResult.Text(file.content, file.language, file.size) + } else { + FileReadResult.Binary(bytes, mimeType.substringBefore(';')) + } + } + } + + // --- Archive browser ---------------------------------------------------- + + suspend fun archivedSessions(): ArchiveList = withContext(Dispatchers.IO) { + val body = requireSuccess(perform(get("api/sessions/archive"))) + decode(body) { json.decodeFromString(ArchiveList.serializer(), body) } + } + + suspend fun restoreArchive(key: String): RestoreArchiveResponse = withContext(Dispatchers.IO) { + val payload = buildJsonObject { put("key", key) } + val response = perform(postJson("api/sessions/archive", payload.toString())) + if (response.status !in 200..299) throw requestError(response) + decode(response.body) { json.decodeFromString(RestoreArchiveResponse.serializer(), response.body) } + } + + // --- Directories, worktrees, models ------------------------------------- + + /** + * `GET /api/cwd/browse?path=…`. A rejected path answers `{error, code}` + * (404 `directory_not_found`, 400 `not_a_directory`); [requestError] keeps + * that wording, so the picker shows the server's sentence instead of a + * bare "HTTP 404" (the web picker shows the same text). + */ + suspend fun browseDirectory(path: String?): BrowseResponse = withContext(Dispatchers.IO) { + val suffix = if (path.isNullOrBlank()) "" else "?path=${encodeQuery(path)}" + val response = perform(get("api/cwd/browse$suffix")) + if (response.status !in 200..299) throw requestError(response) + decode(response.body) { json.decodeFromString(BrowseResponse.serializer(), response.body) } + } + + suspend fun defaultCwd(): String = withContext(Dispatchers.IO) { + val response = perform(postJson("api/default-cwd", "{}")) + if (response.status !in 200..299) throw requestError(response) + decode(response.body) { json.decodeFromString(DefaultCwd.serializer(), response.body) }.cwd + } + + suspend fun homeDirectory(): String = withContext(Dispatchers.IO) { + val body = requireSuccess(perform(get("api/home"))) + decode(body) { json.decodeFromString(HomeDirectory.serializer(), body) }.home + } + + suspend fun worktrees(cwd: String): WorktreeList = withContext(Dispatchers.IO) { + val body = requireSuccess(perform(get("api/worktrees?cwd=${encodeQuery(cwd)}"))) + decode(body) { json.decodeFromString(WorktreeList.serializer(), body) } + } + + override suspend fun models(cwd: String?): ModelsResponse = withContext(Dispatchers.IO) { + val suffix = if (cwd.isNullOrBlank()) "" else "?cwd=${encodeQuery(cwd)}" + val body = requireSuccess(perform(get("api/models$suffix"))) + decode(body) { json.decodeFromString(ModelsResponse.serializer(), body) } + } + + // --- Clone -------------------------------------------------------------- + + /** + * `POST /api/projects/clone {id,parent,url}` streams NDJSON frames + * (`output`, then exactly one of `done`/`cancelled`/`error`). The caller + * owns [onOutput] and receives the terminal frame. Cancelling the calling + * coroutine drops the stream, which makes the server cancel and clean up. + */ + suspend fun cloneRepo( + id: String, + parent: String, + url: String, + onOpened: () -> Unit = {}, + onCall: (Call) -> Unit = {}, + onOutput: (String) -> Unit, + ): CloneFrame { + val payload = buildJsonObject { + put("id", id) + put("parent", parent) + put("url", url) + } + val request = postJson("api/projects/clone", payload.toString()) + val call = client.newCall(request) + // The caller can close the blocking read directly; waiting for the + // coroutine job would keep a cancelled clone alive until the server + // ends the stream. + onCall(call) + // OkHttp's read cannot be interrupted, so the stream runs on a plain + // thread and cancellation closes the call instead of awaiting a + // server-side end that may never come. + return suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { call.cancel() } + cloneExecutor.execute { + val result = runCatching { readCloneStream(call, request, onOpened, onOutput) } + if (continuation.isActive) { + val mapped: Result = result.fold( + onSuccess = { Result.success(it) }, + onFailure = { error -> Result.failure(error.toCloneError()) }, + ) + continuation.resumeWith(mapped) + } + } + } + } + + private fun readCloneStream( + call: Call, + request: Request, + onOpened: () -> Unit, + onOutput: (String) -> Unit, + ): CloneFrame { + val response = try { + call.execute() + } catch (error: IOException) { + throw ApiError.Network(error) + } + val origin = request.url.let { ServerAddress(it.scheme, it.host, it.port) } + response.use { + if (it.code !in 200..299) throw requestError(RawResponse(it.code, readCappedBody(it), origin)) + onOpened() + val source = it.body.source() + var streamedChars = 0L + while (true) { + val line = try { + // A hostile or broken server can stream one endless + // line; the strict read refuses to buffer it. + source.readUtf8LineStrict(MAX_CLONE_LINE_BYTES) + } catch (_: EOFException) { + // `exhausted()` is true only for a regular stream end: + // a line over the cap leaves bytes buffered. + if (source.exhausted()) break + call.cancel() + return CloneFrame.Error( + "The clone output exceeded the display limit.", + "clone_output_too_large", + ) + } catch (error: IOException) { + throw ApiError.Network(error) + } + streamedChars += line.length + if (streamedChars > MAX_CLONE_STREAM_CHARS) { + call.cancel() + return CloneFrame.Error( + "The clone output exceeded the display limit.", + "clone_output_too_large", + ) + } + if (line.isBlank()) continue + val frame = parseCloneFrame(line) ?: continue + if (frame is CloneFrame.Output) { + onOutput(frame.text) + } else { + return frame + } + } + return CloneFrame.Error("The clone stream ended without a result", "clone_interrupted") + } + } + + private fun Throwable.toCloneError(): ApiError = when (this) { + is ApiError -> this + is IOException -> ApiError.Network(this) + else -> ApiError.Network(IOException(this)) + } + + /** + * `DELETE /api/projects/clone {id}`. `false` (404) means the server has + * not registered the clone; every other failure is thrown so the caller + * can drop the stream like the web does on a rejected fetch. + */ + suspend fun cancelClone(id: String): Boolean = withContext(Dispatchers.IO) { + val payload = buildJsonObject { put("id", id) } + val response = perform(postJson("api/projects/clone", payload.toString(), method = "DELETE")) + when { + response.status == 404 -> false + response.status !in 200..299 -> throw requestError(response) + else -> true + } + } + + private fun parseCloneFrame(line: String): CloneFrame? = try { + val root = json.parseToJsonElement(line).jsonObject + when (root["type"]?.jsonPrimitive?.contentOrNull) { + "output" -> CloneFrame.Output(root["text"]?.jsonPrimitive?.contentOrNull ?: "") + "done" -> CloneFrame.Done(root["path"]?.jsonPrimitive?.contentOrNull ?: "") + "cancelled" -> CloneFrame.Cancelled(root["path"]?.jsonPrimitive?.contentOrNull ?: "") + "error" -> CloneFrame.Error( + root["error"]?.jsonPrimitive?.contentOrNull ?: "Clone failed", + root["code"]?.jsonPrimitive?.contentOrNull, + ) + else -> null + } + } catch (_: Exception) { + null + } + + private fun ProjectLaunchConfig.toJson(): JsonObject = buildJsonObject { + profile?.takeIf { it.isNotBlank() }?.let { put("profile", it) } + if (advisor == true) put("advisor", true) + if (!extraArgs.isNullOrEmpty()) put("extraArgs", buildJsonArray { extraArgs.forEach { add(JsonPrimitive(it)) } }) + } + + suspend fun login(password: String): LoginOutcome = withContext(Dispatchers.IO) { + val payload = json.encodeToString(LoginRequest.serializer(), LoginRequest(password)) + val response = perform(postJson("api/web-auth/session", payload)) + when (response.status) { + 401 -> LoginOutcome.WrongPassword + 404 -> LoginOutcome.PasswordDisabled + 413 -> LoginOutcome.TooLarge + in 200..299 -> LoginOutcome.Success + else -> throw errorFor(response.status, response.body, response.origin) + } + } + + suspend fun postEvent(payload: JsonObject): EventPostResult = withContext(Dispatchers.IO) { + val response = perform(postJson("api/events", payload.toString())) + when (response.status) { + in 200..299 -> EventPostResult.Ok + // Only an unknown connection means "reconnect"; a 404 with any + // other code is the channel's own failure (for example + // `file_not_found`) and must not reconnect forever. + 404 -> { + val error = parseErrorBody(response.body) + if (error.code == UNKNOWN_CONNECTION) { + EventPostResult.ConnectionUnknown + } else { + EventPostResult.Rejected(response.status, error.code, error.message) + } + } + // The protocol treats 400/403/409 as a fatal subscription error. + 400, 403, 409 -> { + val error = parseErrorBody(response.body) + EventPostResult.Rejected(response.status, error.code, error.message) + } + else -> throw errorFor(response.status, response.body, response.origin) + } + } + + /** + * Opens the multiplexed SSE stream. The caller owns the [Call] and must + * cancel it to close the connection; the response is read incrementally. + * + * `GET /api/events?heartbeat=`: the body is idle between two + * heartbeats, so the shared 60 s read timeout would fire long before the + * app's default 110 s heartbeat and drop a healthy stream. This call + * therefore uses a derived client (same pool, dispatcher, cookie jar and + * interceptors) whose idle timeout is the heartbeat plus + * [STREAM_READ_TIMEOUT_SLACK_SECONDS] (110 s + 30 s = 140 s by default); + * [heartbeatSeconds] is the server's 15–110 s range. + */ + fun openEventStream(heartbeatSeconds: Int): Call { + val target = url("api/events") + .newBuilder() + .addQueryParameter("heartbeat", heartbeatSeconds.toString()) + .build() + val request = Request.Builder() + .url(target) + .header("Accept", "text/event-stream") + .header("Cache-Control", "no-cache") + .get() + .build() + return eventStreamClient(heartbeatSeconds).newCall(request) + } + + /** One derived client per requested heartbeat; built once, then reused. */ + private fun eventStreamClient(heartbeatSeconds: Int): OkHttpClient = + eventStreamClients.getOrPut(heartbeatSeconds) { + client.newBuilder() + .readTimeout(heartbeatSeconds.toLong() + STREAM_READ_TIMEOUT_SLACK_SECONDS, TimeUnit.SECONDS) + .build() + } + + /** A request builder bound to the configured origin (same URL and cookie rules as every call). */ + fun requestBuilder(path: String, accept: String = "application/json"): Request.Builder = + Request.Builder().url(url(path)).header("Accept", accept) + + /** + * Executes [request]; an IOException becomes [ApiError.Network] and the + * body is capped like every other route, so callers never see a raw + * response or an unbounded read. + */ + suspend fun exchange(request: Request, limit: Long = MAX_BODY_BYTES): RawExchange = withContext(Dispatchers.IO) { + val response = try { + client.newCall(request).execute() + } catch (error: IOException) { + throw ApiError.Network(error) + } + response.use { RawExchange(it.code, readCappedBytes(it, limit), it.header("Content-Type")) } + } + + /** + * Opens [request] for an incremental read (SSE/NDJSON). The caller owns the + * [Call] and cancels it to stop; unlike [exchange] nothing is buffered, so + * a long-lived stream stays readable. Used by the OAuth login stream, whose + * blocking read must be interruptible from the calling coroutine. + */ + fun openStream(request: Request): Call = client.newCall(request) + + /** + * Maps a non-2xx status the same way every call does; [origin] is the + * origin of the request that failed, so a late 401 from a server the user + * already left cannot sign out the new one. + */ + fun errorFor(status: Int, body: String, origin: ServerAddress): ApiError { + when (status) { + 401 -> { + onUnauthorized(origin) + return ApiError.Unauthorized(body) + } + 403 -> return ApiError.Forbidden(body) + } + // A busy omp answers `session_slow` (503 on the polls, 400 on + // `POST /api/agent`); upstream's `get_state` timeout answers + // `session_state_timeout` — 400 on the agent route, 500 on the state + // route. The session keeps running in every case, so the code decides, + // not the status: neither may surface as a server setup problem or a + // plain HTTP error. + val parsed = parseErrorBody(body) + if (isSessionSlowCode(parsed.code)) { + return ApiError.SessionSlow(parsed.message, parsed.maybeStarted, parsed.abortUnconfirmed, parsed.running) + } + if (status == 503) return ApiError.ServerMisconfigured(body) + return ApiError.Http(status, body) + } + + private fun get(path: String): Request = Request.Builder() + .url(url(path)) + .header("Accept", "application/json") + .get() + .build() + + private fun postJson(path: String, payload: String, method: String = "POST"): Request { + val builder = Request.Builder() + .url(url(path)) + .header("Accept", "application/json") + val body = payload.toRequestBody(JSON_MEDIA_TYPE) + return when (method) { + "DELETE" -> builder.delete(body).build() + "PATCH" -> builder.patch(body).build() + else -> builder.post(body).build() + } + } + + /** + * Non-2xx handling for the routes that answer with `{error, code}` JSON: + * auth failures keep their global meaning, everything else becomes a + * [ApiError.ServerResponse] the UI can localize. + */ + private fun requestError(response: RawResponse): ApiError { + when (response.status) { + 401 -> onUnauthorized(response.origin) + 403 -> return ApiError.Forbidden(response.body) + } + val parsed = parseErrorBody(response.body) + // `session_slow` is "busy, retry later" on every route, not just the + // 503 polls: POST /api/agent answers 400 with the same code when a + // prompt ack times out, and `maybeStarted` says the message may already + // be running; `abortUnconfirmed` says an abort was not confirmed. + // `session_state_timeout` (upstream's `get_state` timeout) carries the + // same meaning, so it must not surface as a hard error either. + if (isSessionSlowCode(parsed.code)) { + return ApiError.SessionSlow(parsed.message, parsed.maybeStarted, parsed.abortUnconfirmed, parsed.running) + } + // The foreign-owner gate: the session is not broken, it just belongs to + // another omp process. The chat needs the owner report for its banner. + if (parsed.code == SESSION_RUNNING_ELSEWHERE) return ApiError.SessionRunningElsewhere(parsed.elsewhere) + if (response.status == 503) return ApiError.ServerMisconfigured(response.body) + return ApiError.ServerResponse(response.status, parsed.code, parsed.message) + } + + private fun encodePathSegment(value: String): String = + URLEncoder.encode(value, "UTF-8").replace("+", "%20") + + private fun encodeQuery(value: String): String = URLEncoder.encode(value, "UTF-8") + + private fun fileNameFromContentDisposition(header: String?): String? { + if (header == null) return null + val match = Regex("""filename\s*=\s*"?([^";]+)"?""").find(header) ?: return null + val raw = match.groupValues[1].substringAfterLast('/').substringAfterLast('\\').trim() + return raw.takeIf { it.isNotEmpty() } + } + + private fun url(path: String): okhttp3.HttpUrl { + val address = origin() ?: throw ApiError.NotConfigured + return address.apiUrl(path).toHttpUrl() + } + + /** + * A 2xx body that does not match the documented shape (a stale server, a + * proxy error page) is an [ApiError.Http], so callers never see a raw + * serialization exception and the UI can show "server answered with an error". + */ + private inline fun decode(body: String, block: () -> T): T = try { + block() + } catch (error: SerializationException) { + throw ApiError.Http(200, body.take(2048)) + } + + private fun requireSuccess(response: RawResponse): String { + if (response.status in 200..299) return response.body + throw errorFor(response.status, response.body, response.origin) + } + + private fun perform(request: Request): RawResponse = execute(client.newCall(request), request) + + /** + * [perform] for a call the waiting coroutine must be able to give up on. + * OkHttp's blocking `execute()` ignores coroutine cancellation, so the + * cancellation closes the call; `withTimeout`/`cancel()` then release the + * caller at once instead of at the next socket timeout. [http] carries + * the per-call bound (`callTimeout`). + */ + private suspend fun performCancellable(http: OkHttpClient, request: Request): RawResponse = + suspendCancellableCoroutine { continuation -> + val call = http.newCall(request) + continuation.invokeOnCancellation { call.cancel() } + val outcome = runCatching { execute(call, request) } + // A cancelled continuation ignores the result; the call is closed + // by the hook above and `execute` returns right away. + if (continuation.isActive) continuation.resumeWith(outcome) + } + + /** One bounded request→response round trip; an IOException becomes [ApiError.Network]. */ + private fun execute(call: Call, request: Request): RawResponse { + val response = try { + call.execute() + } catch (error: IOException) { + throw ApiError.Network(error) + } + val origin = request.url.let { ServerAddress(it.scheme, it.host, it.port) } + response.use { + return RawResponse(it.code, readCappedBody(it), origin, it.header("ETag")) + } + } + + private fun readCappedBody(response: Response): String = readCappedBytes(response, MAX_BODY_BYTES).toString(Charsets.UTF_8) + + /** + * Reads a response body with a hard ceiling. A connection that breaks + * after the headers becomes an [ApiError.Network] instead of an + * IOException escaping into a view-model coroutine. `internal` so the + * auth stream and native settings API cap their own response reads the + * same way (chunked bodies have no `Content-Length` to check). + */ + internal fun readCappedBytes(response: Response, limit: Long): ByteArray { + val body = response.body + if (body.contentLength() > limit) { + throw ApiError.Http(response.code, "The response body exceeded $limit bytes.") + } + val stream = body.byteStream() + val buffer = ByteArray(16 * 1024) + val output = java.io.ByteArrayOutputStream() + var total = 0L + while (true) { + val read = try { + stream.read(buffer) + } catch (error: IOException) { + throw ApiError.Network(error) + } + if (read == -1) break + total += read + if (total > limit) { + throw ApiError.Http(response.code, "The response body exceeded $limit bytes.") + } + output.write(buffer, 0, read) + } + return output.toByteArray() + } + + private data class RawResponse( + val status: Int, + val body: String, + val origin: ServerAddress, + /** The `ETag` of this answer; the session list revalidates with it. */ + val etag: String? = null, + ) + + private data class ErrorBody( + val code: String?, + val message: String, + val maybeStarted: Boolean, + val abortUnconfirmed: Boolean, + /** The owner report of a `session_running_elsewhere` answer. */ + val elsewhere: ElsewhereSession? = null, + /** The server's run state on a `session_slow` answer (`null` = unreported). */ + val running: Boolean? = null, + ) + + private fun parseErrorBody(body: String): ErrorBody = try { + val root = json.parseToJsonElement(body).jsonObject + ErrorBody( + code = root["code"]?.jsonPrimitive?.contentOrNull, + message = root["error"]?.jsonPrimitive?.contentOrNull ?: body, + maybeStarted = root["maybeStarted"]?.jsonPrimitive?.booleanOrNull == true, + abortUnconfirmed = root["abortUnconfirmed"]?.jsonPrimitive?.booleanOrNull == true, + elsewhere = elsewhereFrom(root["elsewhere"]), + running = root["running"]?.jsonPrimitive?.booleanOrNull, + ) + } catch (_: Exception) { + ErrorBody(code = null, message = body, maybeStarted = false, abortUnconfirmed = false) + } + + private fun imagesJson(images: List): JsonArray = buildJsonArray { + images.forEach { image -> + add( + buildJsonObject { + put("type", "image") + put("data", image.data) + put("mimeType", image.mimeType) + }, + ) + } + } + + private fun queuedEntries(element: JsonElement?): List { + val array = element as? JsonArray ?: return emptyList() + return array.mapNotNull { item -> + val entry = item as? JsonObject ?: return@mapNotNull null + val text = (entry["text"] as? JsonPrimitive)?.contentOrNull.orEmpty() + val images = imagesFromRpc(entry["images"]) + if (text.isEmpty() && images.isEmpty()) null else QueuedMessagePayload(text, images) + } + } + + @Serializable + private data class LoginRequest(val password: String) + + internal companion object { + internal const val MAX_BODY_BYTES = 8L * 1024 * 1024 + + /** + * How long a finished `GET /api/sessions/{id}` answer is shared with a + * second caller (the header load and the chat's metadata load never + * overlap exactly). Deliberately short: this shares one request burst, + * it is not a transcript cache. + */ + internal const val DETAIL_SHARE_WINDOW_MILLIS = 2_000L + const val MAX_EXPORT_BYTES = 32L * 1024 * 1024 + const val MAX_FILE_BYTES = 16L * 1024 * 1024 + const val MAX_BASH_OUTPUT_BYTES = 8L * 1024 * 1024 + const val MAX_CLONE_LINE_BYTES = 256L * 1024 + const val MAX_CLONE_STREAM_CHARS = 8L * 1024 * 1024 + + /** + * Read-timeout slack over the SSE heartbeat: a heartbeat that arrives + * on time must never race the idle timeout. 110 s heartbeat → 140 s. + */ + const val STREAM_READ_TIMEOUT_SLACK_SECONDS = 30 + + /** + * Hard per-call bound of the live-state read (`GET /api/agent/{id}`). + * It is the chat's submit bound: no answer within this window means + * "unknown state", and the draft stays in the composer. + */ + internal const val LIVE_STATE_READ_TIMEOUT_MILLIS = 10_000L + const val UNKNOWN_CONNECTION = "unknown_connection" + /** 503 code for "omp is busy, the session keeps running". */ + const val SESSION_SLOW = "session_slow" + + /** + * The `get_state` timeout of servers before the rename: same meaning + * as [SESSION_SLOW] (the child is kept, ask again later), so it is + * mapped to the same [ApiError.SessionSlow]. Servers from the rebase + * normalize it to `session_slow`; this stays for older answers and + * for the route paths that still use it. + */ + const val SESSION_STATE_TIMEOUT = "session_state_timeout" + + /** Both codes mean "omp did not answer in time; the session keeps running". */ + internal fun isSessionSlowCode(code: String?): Boolean = + code == SESSION_SLOW || code == SESSION_STATE_TIMEOUT + + /** 409 code for "another omp process owns this session". */ + const val SESSION_RUNNING_ELSEWHERE = "session_running_elsewhere" + val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiError.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiError.kt new file mode 100644 index 000000000..2a17ee410 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiError.kt @@ -0,0 +1,99 @@ +package io.github.kahme247.ompweb.net + +import java.io.IOException + +/** + * Every failure an API call can produce, in the terms the UI needs. + * + * [Unauthorized] is reported globally (the app returns to the password screen) + * as well as to the caller; [ServerMisconfigured] is a server-side setup + * problem that retrying cannot fix. + */ +sealed class ApiError(message: String, cause: Throwable? = null) : Exception(message, cause) { + + /** 401: the session cookie is missing, expired or invalid. */ + class Unauthorized(val body: String) : ApiError("Not signed in") + + /** 503: ompweb is running, but its password configuration is broken. */ + class ServerMisconfigured(val body: String) : ApiError("Server is misconfigured") + + /** + * `session_slow`: omp is busy (it blocks its event loop for tens of + * seconds on large sessions) and did not answer in time. The session and + * its run keep going — callers retry later, they never treat this as the + * run ending or as a fatal error. On a prompt ack timeout + * [maybeStarted] is true: the frame may already be running, so the send + * stays locked until a frame or a successful state snapshot arrives. + * On an abort timeout [abortUnconfirmed] is true: neither the stop nor the + * run state is known, so the UI must not claim the run ended. + */ + class SessionSlow( + val serverMessage: String?, + val maybeStarted: Boolean = false, + val abortUnconfirmed: Boolean = false, + /** + * The run state from the 503 body: `false` means no turn is in flight — + * the child is alive but only blocked (a fresh spawn loading a large + * session answers `get_state` too late) — so the poll must settle the + * run instead of keeping it marked. `null` = the server does not report + * it (older build); the caller then bounds the unknown window. + */ + val running: Boolean? = null, + ) : ApiError(serverMessage ?: "The agent is busy") + + /** + * 409 `session_running_elsewhere`: another omp process owns this session, + * so the server refused to spawn a writer. The chat is not broken and the + * text was not sent — the composer keeps the draft, the screen shows the + * foreign-owner banner and only an explicit "send anyway (copy)" retries + * with `?force=1`. [elsewhere] is the owner report from the body (null if + * the body was unreadable). + */ + class SessionRunningElsewhere(val elsewhere: ElsewhereSession?) : + ApiError("Another omp process is running this session") + + /** 403: the origin gate or a proxy refused the request. */ + class Forbidden(val body: String) : ApiError("Request refused") + + /** No server origin is configured (the user is on "change server"). */ + data object NotConfigured : ApiError("No server origin is configured") + + /** No answer: DNS, connection, TLS or a timeout. */ + class Network(cause: IOException) : ApiError("Cannot reach the server", cause) + + /** + * A non-2xx answer whose JSON body carries a stable `code`/`error` pair, + * as the project, archive, clone and session routes send it. The UI can + * localize these; the status stays available for branching. + */ + class ServerResponse( + val status: Int, + val code: String?, + val serverMessage: String?, + ) : ApiError(serverMessage ?: "HTTP $status") + + /** Any other non-2xx status. */ + class Http(val code: Int, val body: String) : ApiError("HTTP $code") +} + +/** + * The one user-facing wording for API failures. Every screen shows these + * strings; localized variants would go here instead of being copied per view + * model (the copies had already started to drift). + */ +fun ApiError.userMessage(): String = when (this) { + is ApiError.ServerResponse -> serverMessage ?: code ?: "HTTP $status" + is ApiError.Http -> "HTTP $code" + is ApiError.Network -> "Cannot reach the server" + is ApiError.Forbidden -> "Not allowed" + is ApiError.ServerMisconfigured -> "The server needs configuration" + is ApiError.SessionSlow -> + serverMessage ?: "The agent is busy and did not answer in time. It is still running — try again in a moment." + is ApiError.SessionRunningElsewhere -> + "Another omp process is running this chat. Sending would create a copy — your message was not sent." + is ApiError.Unauthorized -> "The session expired; sign in again" + is ApiError.NotConfigured -> "No server configured" +} + +/** [ApiError.userMessage] for any throwable; non-API failures fall back to their message. */ +fun Throwable.userMessage(): String = (this as? ApiError)?.userMessage() ?: message ?: "Request failed" diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiModels.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiModels.kt new file mode 100644 index 000000000..013452e5d --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ApiModels.kt @@ -0,0 +1,409 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.ComposerImage +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull + +/** `GET /api/app-info`: the capability handshake a native client requires. */ +@Serializable +data class AppInfo( + val name: String = "", + val version: String = "", + val api: Int = 0, + val capabilities: Map = emptyMap(), +) { + /** + * `capabilities.messageSearch`: the server serves + * `GET /api/sessions/messages/search`. A server that predates that route + * omits the flag and answers the route with 404, so the app hides its + * message search. The flag is deliberately not called `sessionSearch`: + * `/api/sessions/search` belongs to the server's sidebar ranking, which + * answers a different (NDJSON) shape. + */ + val supportsMessageSearch: Boolean get() = capabilities["messageSearch"] == true +} + +/** + * One row of `GET /api/sessions` (server shape: `lib/types.ts` `SessionInfo`). + * Unknown fields are ignored on purpose. + */ +@Serializable +data class SessionSummary( + val id: String, + val path: String = "", + val cwd: String = "", + val name: String? = null, + val created: String = "", + val modified: String = "", + val messageCount: Int = 0, + val firstMessage: String = "", + val parentSessionId: String? = null, + val projectRoot: String? = null, + val projectKey: String? = null, + val worktreeBranch: String? = null, +) + +/** `POST /api/cwd/validate {cwd}` — the normalized checkout, or the route's error code. */ +sealed interface CwdValidation { + data class Valid(val cwd: String, val projectRoot: String?) : CwdValidation + + /** `path_required`, `directory_not_found`, `not_a_directory`, … */ + data class Invalid(val code: String?, val message: String?) : CwdValidation +} + +/** One live omp child that the server still lists for the running badges. */ +@Serializable +data class RunningSession( + val id: String, + val cwd: String = "", +) + +/** + * Another omp process owns a session (server `SessionElsewhere`): a second + * omp-web instance, a terminal `omp --resume`, or — weakest — a session file + * that was written recently and never closed. omp lets only one process write + * a session, so a prompt from this app would move the chat to a copy; the + * server answers that send with `409 session_running_elsewhere` unless the + * user explicitly asks for the copy (`?force=1`). + * + * [kind] is `omp-web`/`terminal`, [source] `instance-list`/`process`/ + * `transcript` (strongest first), [running] means the owner is mid-turn and + * [active] that it wrote within the server's activity window. + * [lastActivityAt] is present only when the server read the session file, so + * absence is normal (not "idle"). The owner's pid and cwd are never sent: no + * client uses them and they leak the other instance/process. + * + * [id] exists only on the entries of `elsewhereSessions` (the list and the + * `running` frame, one per session). The owner report of a single session + * (`GET /api/agent/{id}`, the `409` body) is the server's `SessionElsewhere` + * and carries no id: the chat already knows which session it asked about, so + * a decode that insisted on an id would drop every live report. + */ +@Serializable +data class ElsewhereSession( + val id: String = "", + val kind: String = "", + val source: String = "", + val running: Boolean = false, + val active: Boolean = false, + val lastActivityAt: Long? = null, +) + +/** One omp child that exited unexpectedly (`ExitedRpcSession`). */ +@Serializable +data class ExitedSession( + val id: String, + val cwd: String = "", + val at: Long = 0, + val code: Int? = null, + val signal: String? = null, + val detail: String = "", +) + +@Serializable +data class SessionList( + val sessions: List = emptyList(), + val runningSessionIds: List = emptyList(), + val runningSessions: List = emptyList(), + val exitedSessions: List = emptyList(), + /** + * Sessions another omp process owns. Deliberately apart from + * `runningSessionIds`: those mean "this server runs a turn", which the + * unread and notification rules depend on. An older server omits the + * field, so it defaults to empty. + */ + val elsewhereSessions: List = emptyList(), +) + +private val elsewhereJson = kotlinx.serialization.json.Json { ignoreUnknownKeys = true } + +/** + * One `elsewhere` payload (`GET /api/agent/{id}`, the `409` body), or null for + * absent/`null`/malformed. A foreign-owner report must never fail the + * surrounding answer, so the decode is tolerant. + */ +fun elsewhereFrom(element: JsonElement?): ElsewhereSession? { + val obj = element as? JsonObject ?: return null + return try { + elsewhereJson.decodeFromJsonElement(ElsewhereSession.serializer(), obj) + } catch (_: Exception) { + null + } +} + +/** + * The workspace-level omp launch configuration (`lib/types.ts` + * `ProjectLaunchConfig`). + */ +@Serializable +data class ProjectLaunchConfig( + val profile: String? = null, + val advisor: Boolean? = null, + val extraArgs: List? = null, +) { + fun isEmpty(): Boolean = profile.isNullOrBlank() && advisor != true && extraArgs.isNullOrEmpty() +} + +/** One row of `GET /api/projects` (`ManagedProject`). */ +@Serializable +data class ManagedProject( + val path: String, + val addedAt: String? = null, + val alias: String? = null, + val sortOrder: Int? = null, + val launchConfig: ProjectLaunchConfig? = null, +) + +@Serializable +data class ProjectList(val projects: List = emptyList()) + +@Serializable +data class ProjectResponse(val project: ManagedProject? = null) + +/** `GET /api/sessions/{id}` — only the fields the session header needs. */ +@Serializable +data class SessionInfo( + val path: String = "", + val id: String, + val cwd: String = "", + val name: String? = null, + val created: String = "", + val modified: String = "", + val messageCount: Int = 0, + val firstMessage: String = "", + val parentSessionId: String? = null, + val projectRoot: String? = null, + val projectKey: String? = null, + val worktreeBranch: String? = null, +) + +@Serializable +data class SessionModel( + val provider: String = "", + @SerialName("modelId") val modelId: String = "", +) + +/** + * The context part of `GET /api/sessions/{id}`. Messages stay as raw JSON + * elements: Phase 5 owns transcript rendering, the session header only needs + * the model and the message count. + */ +@Serializable +data class SessionContext( + val thinkingLevel: String = "auto", + val model: SessionModel? = null, + val messages: List = emptyList(), +) + +@Serializable +data class SessionDetail( + val sessionId: String = "", + val filePath: String = "", + val info: SessionInfo? = null, + val leafId: String? = null, + val context: SessionContext? = null, + /** Projected branch tree (`{entry,children[],…}`); parsed by the chat UI. */ + val tree: List = emptyList(), +) + +/** `GET /api/sessions/{id}/entries/{entryId}/thinking?blockIndex=` */ +@Serializable +data class ThinkingResponse(val thinking: String = "") + +/** `GET /api/sessions/{id}/subagents/{subagentId}?mode=completion` */ +@Serializable +data class SubagentCompletion( + val completion: String? = null, + val truncated: Boolean = false, +) + +/** `POST /api/agent/{id} {type:"fork", entryId}` — the new child session. */ +@Serializable +data class ForkOutcome( + val cancelled: Boolean = false, + val newSessionId: String? = null, + val text: String? = null, +) + +/** The text answer of `GET /api/files/[...path]?type=read`. */ +@Serializable +data class FileText( + val content: String = "", + val language: String? = null, + val size: Long = 0, +) + +/** A file read: JSON for text, raw bytes for images/audio/documents. */ +sealed interface FileReadResult { + data class Text(val content: String, val language: String?, val size: Long) : FileReadResult + + data class Binary(val bytes: ByteArray, val mimeType: String) : FileReadResult +} + +/** The referenced bash output, or the server's refusal because it is too large. */ +sealed interface BashOutputResult { + data class Text(val output: String) : BashOutputResult + + data class TooLarge(val size: Long, val maxBytes: Long) : BashOutputResult +} + +/** One row of `GET /api/sessions/archive` (`ArchivedSessionInfo`). */ +@Serializable +data class ArchiveEntry( + val key: String, + val id: String, + val cwd: String = "", + val name: String? = null, + val created: String = "", + val archivedAt: String = "", + val messageCount: Int = 0, + val firstMessage: String = "", + val size: Long = 0, + val status: String = "", +) + +@Serializable +data class ArchiveList(val archives: List = emptyList()) + +@Serializable +data class RestoreArchiveResponse(val ok: Boolean = false, val sessionId: String? = null) + +@Serializable +data class DirectoryEntry(val name: String, val path: String) + +@Serializable +data class BrowseResponse( + val path: String = "", + val parentPath: String? = null, + val directories: List = emptyList(), + val drives: List? = null, +) + +@Serializable +data class DefaultCwd(val cwd: String = "") + +@Serializable +data class HomeDirectory(val home: String = "") + +@Serializable +data class WorktreeEntry( + val path: String, + val branch: String? = null, + val isMain: Boolean = false, +) + +@Serializable +data class WorktreeList( + val projectRoot: String = "", + val projectKey: String? = null, + val isGit: Boolean = false, + val isTopLevel: Boolean = false, + val currentWorktreePath: String? = null, + val worktrees: List = emptyList(), +) + +@Serializable +data class ModelEntry( + val id: String, + val name: String = "", + val provider: String = "", + val thinkingLevels: List = emptyList(), + val supportsFastMode: Boolean = false, + val supportsSlowMode: Boolean = false, + val contextWindow: Int? = null, + val maxTokens: Int? = null, +) + +@Serializable +data class DefaultModel(val provider: String = "", val modelId: String = "") + +/** `GET /api/models?cwd=` — the live model catalog used by the new session picker. */ +@Serializable +data class ModelsResponse( + val models: Map = emptyMap(), + val modelList: List = emptyList(), + val defaultModel: DefaultModel? = null, + val thinkingLevels: Map> = emptyMap(), + /** Providers whose catalog entries carry a pre-spawn Slow tier (anthropic). */ + val anthropicSlowMode: Boolean = false, + /** Logged-in providers omp reports (`/api/models`); the registry panel lists them. */ + val connectedProviders: List = emptyList(), +) + +/** One logged-in provider as `/api/models` reports it (no credentials). */ +@Serializable +data class ConnectedProvider( + val id: String = "", + val name: String = "", + val disabled: Boolean = false, +) + +@Serializable +data class AutoNameResponse( + val title: String = "", + val generated: Boolean = false, +) + +@Serializable +data class AgentNewResponse( + val success: Boolean = false, + val sessionId: String = "", +) + +/** One entry of omp's `abort_and_restore_queue` reply. */ +data class QueuedMessagePayload(val text: String, val images: List) + +/** `{steering:[...], followUp:[...]}` withdrawn by `abort_and_restore_queue`. */ +data class QueueRestore( + val steering: List = emptyList(), + val followUp: List = emptyList(), +) { + val entries: List get() = steering + followUp + val isEmpty: Boolean get() = steering.isEmpty() && followUp.isEmpty() +} + +/** `remove_queued_message` reply: `{removed, images?}`. */ +data class QueuedMessageRemoval(val removed: Boolean, val images: List = emptyList()) + +@Serializable +data class SkillInfo( + val name: String = "", + val disableModelInvocation: Boolean = false, +) + +/** `GET /api/skills?cwd=` — the skill roster plus scanner diagnostics. */ +@Serializable +data class SkillsResponse( + val skills: List = emptyList(), + val diagnostics: List = emptyList(), +) + +/** + * The agent route answers a prompt/steer with + * `{queued:true, queue:"followUp", reason:"shellRunning"}` when a `!!` shell + * command owns the session: the text went into omp's queue, no turn started. + */ +fun isQueuedWhileShellRunning(reply: JsonElement?): Boolean { + val root = reply as? JsonObject ?: return false + val queued = (root["queued"] as? JsonPrimitive)?.booleanOrNull == true + val reason = (root["reason"] as? JsonPrimitive)?.contentOrNull + return queued && reason == "shellRunning" +} + +/** Read the `{data, mimeType}` images of one RPC payload (invalid entries dropped). */ +fun imagesFromRpc(element: JsonElement?): List { + val array = element as? JsonArray ?: return emptyList() + return array.mapNotNull { item -> + val entry = item as? JsonObject ?: return@mapNotNull null + // omp's queue snapshot may wrap images as `{type:"image", data, mimeType}`. + val data = (entry["data"] as? JsonPrimitive)?.contentOrNull ?: return@mapNotNull null + val mimeType = (entry["mimeType"] as? JsonPrimitive)?.contentOrNull ?: return@mapNotNull null + ComposerImage(data = data, mimeType = mimeType) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/AuthApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/AuthApi.kt new file mode 100644 index 000000000..109cb1049 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/AuthApi.kt @@ -0,0 +1,282 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.OAuthFrame +import io.github.kahme247.ompweb.domain.parseOAuthFrame +import io.github.kahme247.ompweb.events.SseParser +import java.io.IOException +import java.net.URLEncoder +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * Auth routes of the Providers tab (`app/api/auth`): + * provider rosters, stored accounts, API-key status and the OAuth login stream. + * An interface so `ProvidersModelsViewModel` is JVM-testable. + */ +interface AuthApi { + + /** `GET /api/auth/providers` — providers that support a browser/OAuth login. */ + suspend fun oauthProviders(): List + + /** `GET /api/auth/all-providers` — every provider omp knows, with model counts. */ + suspend fun apiKeyProviders(): List + + /** `GET /api/auth/api-key/{provider}` — configured state; never the key itself. */ + suspend fun apiKeyStatus(provider: String): ApiKeyStatus + + /** + * The server's explanation of why keys cannot be edited here + * (`POST /api/auth/api-key/{provider}` always answers 501 without writing). + * Returns `null` when even that probe fails. + */ + suspend fun apiKeyGuidance(provider: String): String? + + /** `GET /api/auth/accounts/{provider}` — stored credentials omp holds. */ + suspend fun accounts(provider: String): ProviderAccounts + + /** `DELETE /api/auth/accounts/{provider}?credentialId=`. */ + suspend fun disconnectAccount(provider: String, credentialId: Long) + + /** + * `GET /api/auth/login/{provider}` as an SSE stream. Calls [onFrame] for + * every parsed frame until a terminal frame, EOF, or cancellation; the + * server disposes its child when the call is cancelled. + */ + suspend fun login(provider: String, onFrame: (OAuthFrame) -> Unit) + + /** `POST /api/auth/login/{provider} {token, code}` with the pasted code/URL or a selected option id. */ + suspend fun submitLogin(provider: String, token: String, value: String) +} + +data class OAuthProvider( + val id: String, + val name: String = "", + val loggedIn: Boolean = false, +) + +data class ApiKeyProvider( + val id: String, + val displayName: String = "", + val configured: Boolean = false, + val modelCount: Int = 0, +) + +data class ApiKeyStatus( + val provider: String = "", + val displayName: String = "", + val configured: Boolean = false, + val models: Int = 0, +) + +/** One stored credential; [label]/[detail] are omp's, never a raw key. */ +data class ProviderAccount( + val credentialId: Long, + val label: String, + val detail: String, + /** `api_key` or `oauth`. */ + val type: String, +) + +data class ProviderAccounts( + val supported: Boolean = true, + val accounts: List = emptyList(), +) + +/** Production [AuthApi] over the app's single authenticated client. */ +fun ApiClient.authApi(): AuthApi = HttpAuthApi(this) + +private val authJson = Json { ignoreUnknownKeys = true } +private val AUTH_JSON_MEDIA = "application/json".toMediaType() + +private fun JsonObject.string(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull + +private fun JsonObject.int(key: String): Int? = (this[key] as? JsonPrimitive)?.intOrNull + +private fun parseAuthObject(text: String): JsonObject? = try { + authJson.parseToJsonElement(text) as? JsonObject +} catch (_: Exception) { + null +} + +private class HttpAuthApi(private val api: ApiClient) : AuthApi { + + override suspend fun oauthProviders(): List { + val root = call(api.requestBuilder(PROVIDERS_PATH).get().build()) + return (root["providers"] as? JsonArray).orEmpty().mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + val id = item.string("id")?.takeIf { it.isNotBlank() } ?: return@mapNotNull null + OAuthProvider( + id = id, + name = item.string("name")?.takeIf { it.isNotBlank() } ?: id, + loggedIn = (item["loggedIn"] as? JsonPrimitive)?.contentOrNull == "true", + ) + } + } + + override suspend fun apiKeyProviders(): List { + val root = call(api.requestBuilder(ALL_PROVIDERS_PATH).get().build()) + return (root["providers"] as? JsonArray).orEmpty().mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + val id = item.string("id")?.takeIf { it.isNotBlank() } ?: return@mapNotNull null + ApiKeyProvider( + id = id, + displayName = item.string("displayName")?.takeIf { it.isNotBlank() } ?: id, + configured = (item["configured"] as? JsonPrimitive)?.contentOrNull == "true", + modelCount = item.int("modelCount") ?: 0, + ) + } + } + + override suspend fun apiKeyStatus(provider: String): ApiKeyStatus { + val root = call(api.requestBuilder(apiKeyPath(provider)).get().build()) + return ApiKeyStatus( + provider = root.string("provider") ?: provider, + displayName = root.string("displayName")?.takeIf { it.isNotBlank() } ?: provider, + configured = (root["configured"] as? JsonPrimitive)?.contentOrNull == "true", + models = root.int("models") ?: 0, + ) + } + + override suspend fun apiKeyGuidance(provider: String): String? = try { + val request = api.requestBuilder(apiKeyPath(provider)) + .post("{}".toRequestBody(AUTH_JSON_MEDIA)) + .build() + val response = api.exchange(request) + val text = response.text() + parseAuthObject(text)?.string("error")?.takeIf { it.isNotBlank() } + ?: text.take(400).takeIf { it.isNotBlank() } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // The probe is best-effort; the panel falls back to its generic hint. + null + } + + override suspend fun accounts(provider: String): ProviderAccounts { + val root = call(api.requestBuilder(accountsPath(provider)).get().build()) + val accounts = (root["accounts"] as? JsonArray).orEmpty().mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + val id = item["credentialId"]?.let { (it as? JsonPrimitive)?.contentOrNull?.toLongOrNull() } + ?: return@mapNotNull null + ProviderAccount( + credentialId = id, + label = item.string("label").orEmpty(), + detail = item.string("detail").orEmpty(), + type = item.string("type").orEmpty(), + ) + } + return ProviderAccounts( + supported = (root["supported"] as? JsonPrimitive)?.contentOrNull != "false", + accounts = accounts, + ) + } + + override suspend fun disconnectAccount(provider: String, credentialId: Long) { + val request = api.requestBuilder("${accountsPath(provider)}?credentialId=$credentialId") + .delete() + .build() + call(request) + } + + override suspend fun login(provider: String, onFrame: (OAuthFrame) -> Unit) = withContext(Dispatchers.IO) { + val request = api.requestBuilder(loginPath(provider), accept = "text/event-stream") + .header("Cache-Control", "no-cache") + .get() + .build() + val call = api.openStream(request) + // OkHttp reads cannot be interrupted, so the coroutine's completion + // (cancel of the view model job) closes the socket instead. + val completion = currentJob()?.invokeOnCompletion { call.cancel() } + try { + val response = try { + call.execute() + } catch (error: IOException) { + if (!isActive) return@withContext + throw ApiError.Network(error) + } + response.use { reply -> + if (reply.code !in 200..299) { + // Same hard cap as every other route: the error page of a + // proxy or rogue server has no trusted length (P11Sec 1). + val text = try { + api.readCappedBytes(reply, ApiClient.MAX_BODY_BYTES).toString(Charsets.UTF_8) + } catch (error: ApiError.Network) { + if (!isActive) return@withContext + throw error + } + throw api.errorFor(reply.code, text, originOf(request)) + } + val source = reply.body.source() + val parser = SseParser() + while (isActive) { + val line = try { + source.readUtf8Line() + } catch (error: IOException) { + if (!isActive) return@withContext + throw ApiError.Network(error) + } ?: break + val frame = parser.consume(line) ?: continue + val parsed = parseOAuthFrame(frame.data) ?: continue + onFrame(parsed) + if (parsed is OAuthFrame.Success || parsed is OAuthFrame.Failed || parsed is OAuthFrame.Cancelled) break + } + } + } finally { + completion?.dispose() + call.cancel() + } + } + + override suspend fun submitLogin(provider: String, token: String, value: String) { + val payload = buildJsonObject { + put("token", token) + put("code", value) + } + val request = api.requestBuilder(loginPath(provider)) + .post(payload.toString().toRequestBody(AUTH_JSON_MEDIA)) + .build() + call(request) + } + + // --- transport --------------------------------------------------------- + + private suspend fun call(request: Request): JsonObject { + val response = api.exchange(request) + val text = response.text() + if (response.status !in 200..299) throw api.errorFor(response.status, text, originOf(request)) + return parseAuthObject(text) ?: throw ApiError.Http(response.status, text.take(2048)) + } + + private fun originOf(request: Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + private fun loginPath(provider: String): String = "api/auth/login/${encodeSegment(provider)}" + + private fun apiKeyPath(provider: String): String = "api/auth/api-key/${encodeSegment(provider)}" + + private fun accountsPath(provider: String): String = "api/auth/accounts/${encodeSegment(provider)}" + + private companion object { + const val PROVIDERS_PATH = "api/auth/providers" + const val ALL_PROVIDERS_PATH = "api/auth/all-providers" + } +} + +private fun encodeSegment(value: String): String = URLEncoder.encode(value, "UTF-8").replace("+", "%20") + +private suspend fun currentJob(): Job? = kotlin.coroutines.coroutineContext[Job] diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/BtwApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/BtwApi.kt new file mode 100644 index 000000000..4c37dbdff --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/BtwApi.kt @@ -0,0 +1,28 @@ +package io.github.kahme247.ompweb.net + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +/** + * The omp RPC calls side questions need (`btw`, `btw_cancel`, + * `get_btw_history`, `get_state`), as an interface so the BTW view model runs + * in JVM tests without a server. + * + * It is deliberately one method: all four are `POST /api/agent/{id}` commands + * and the client treats them identically. Production uses [btwApi]. + */ +interface BtwApi { + /** `POST /api/agent/{sessionId}` with [command]; returns the reply's `data`. */ + suspend fun send(sessionId: String, command: JsonObject): JsonElement? +} + +/** + * The production [BtwApi]: omp's RPC route with the session's advisor flag off, + * exactly like the web's `sendAgentCommand` (`hooks/useBtw.ts`). + */ +fun ApiClient.btwApi(): BtwApi = AgentBtwApi(this) + +private class AgentBtwApi(private val client: ApiClient) : BtwApi { + override suspend fun send(sessionId: String, command: JsonObject): JsonElement? = + client.agentCommand(sessionId, command, advisor = false) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ChatApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ChatApi.kt new file mode 100644 index 000000000..e28522381 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ChatApi.kt @@ -0,0 +1,159 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.ComposerImage +import io.github.kahme247.ompweb.domain.SlashCommandItem +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +/** + * The [ApiClient] calls the chat view model makes, as an interface so the view + * model runs in JVM tests without a server. + * + * Signatures are exactly the `ApiClient` ones. `ChatViewModelTest` supplies a + * fake; production wires [ApiClient], which implements this interface. + */ +interface ChatApi { + + /** + * `POST /api/agent/{id} {type:"prompt"}` with optional images and + * streaming behavior. [force] sends `?force=1`: the server's gate against + * a session another omp process owns is skipped, on purpose and only after + * the user confirmed that the send goes into a copy. + */ + suspend fun prompt( + sessionId: String, + message: String, + images: List = emptyList(), + streamingBehavior: String? = null, + force: Boolean = false, + ): JsonElement? + + /** `GET /api/agent/{id}` — `{running, alive, state}` for reconciliation. */ + suspend fun agentState(sessionId: String): JsonObject + + /** + * `GET /api/sessions` — the ids this server currently runs a turn for. + * The last-resort authority for a 503 that does not report the run state + * (older server); it is the same registry the session list shows. + */ + suspend fun runningSessionIds(): List + + suspend fun abort(sessionId: String): JsonElement? + + /** `{type:"abort_retry"}` — stop the automatic retry banner's wait. */ + suspend fun abortRetry(sessionId: String): JsonElement? + + /** `{type:"steer"}` — deliver [message] into the running turn. */ + suspend fun steer(sessionId: String, message: String, images: List = emptyList()): JsonElement? + + /** `{type:"follow_up"}` — queue [message] behind the running turn. */ + suspend fun followUp(sessionId: String, message: String, images: List = emptyList()): JsonElement? + + /** `{type:"bash", command, excludeFromContext}` (composer `!` / `!!`). */ + suspend fun bash(sessionId: String, command: String, excludeFromContext: Boolean): JsonElement? + + /** `{type:"abort_bash"}` — stop a `!!` shell command. */ + suspend fun abortBash(sessionId: String): JsonElement? + + /** `{type:"abort_and_restore_queue"}` — take pending messages back and abort. */ + suspend fun abortAndRestoreQueue(sessionId: String): QueueRestore + + /** `{type:"remove_queued_message", message, queue}`. */ + suspend fun removeQueuedMessage(sessionId: String, message: String, queue: String): QueuedMessageRemoval + + /** `{type:"promote_queued_message", message}` — move a follow-up into steering. */ + suspend fun promoteQueuedMessage(sessionId: String, message: String): Boolean + + /** `{type:"get_commands"}` — omp's slash command roster. */ + suspend fun commands(sessionId: String): List + + /** `{type:"compact"}` with optional custom instructions. */ + suspend fun compact(sessionId: String, customInstructions: String? = null): JsonElement? + + /** `{type:"reload"}` — re-read extensions, skills and prompts. */ + suspend fun reload(sessionId: String): JsonElement? + + /** `{type:"set_session_name"}`. */ + suspend fun setSessionName(sessionId: String, name: String): JsonElement? + + /** `{type:"get_last_assistant_text"}` — text for `/copy`. */ + suspend fun lastAssistantText(sessionId: String): String? + + /** `GET /api/skills?cwd=` — skill names and their model-invocation state. */ + suspend fun skills(cwd: String): SkillsResponse + + /** `GET /api/sessions/{id}/context?sync=1` — one display-history page plus the live snapshot. */ + suspend fun contextSync( + sessionId: String, + cursor: String? = null, + leafId: String? = null, + includePreCompaction: Boolean = false, + deferThinking: Boolean = true, + deferMedia: Boolean = true, + ): JsonObject + + /** `GET /api/sessions/{id}/context` — the whole display history in one response (web `loadContext`). */ + suspend fun contextFull( + sessionId: String, + leafId: String? = null, + includePreCompaction: Boolean = false, + deferThinking: Boolean = true, + deferMedia: Boolean = true, + ): JsonObject + + /** + * The same response as [contextFull], as the body the server sent. The + * chat writes it to the offline cache and parses a cached open from it, + * so a cached transcript is exactly what the live request would have + * produced. + * + * The default re-renders the parsed object, which is what a test fake + * needs; [ApiClient] overrides it to hand back the received bytes. + */ + suspend fun contextFullRaw( + sessionId: String, + leafId: String? = null, + includePreCompaction: Boolean = false, + deferThinking: Boolean = true, + deferMedia: Boolean = true, + ): String = contextFull(sessionId, leafId, includePreCompaction, deferThinking, deferMedia).toString() + + suspend fun sessionDetail(id: String): SessionDetail + + /** `GET /api/github-repo?cwd=` — `owner/repo` for the checkout, or null. */ + suspend fun githubRepo(cwd: String): String? + + suspend fun forkSession(sessionId: String, entryId: String): ForkOutcome + + /** `GET .../entries/{entryId}/thinking?blockIndex=` for a deferred thinking block. */ + suspend fun thinking(sessionId: String, entryId: String, blockIndex: Int): String + + suspend fun bashOutput(sessionId: String, path: String): BashOutputResult + + suspend fun bashOutputBytes(sessionId: String, path: String): ByteArray + + suspend fun subagentCompletion(sessionId: String, subagentId: String): SubagentCompletion + + /** `{type:"get_subagents"}` — the live registry roster (`{subagents:[…]}`). */ + suspend fun subagents(sessionId: String): JsonElement? + + /** `GET /api/sessions/{id}/subagents` — the on-disk roster, without a live process. */ + suspend fun subagentHistory(sessionId: String): JsonElement? + + /** + * `{type:"get_subagent_messages", subagentId, sessionFile, fromByte}` — one + * transcript page from the live registry (same shape as the disk route). + */ + suspend fun subagentTranscript( + sessionId: String, + subagentId: String, + sessionFile: String?, + fromByte: Long, + ): JsonElement? + + /** `GET .../subagents/{subagentId}?fromByte=` — the disk fallback for a page. */ + suspend fun subagentTranscriptDisk(sessionId: String, subagentId: String, fromByte: Long): JsonElement? + + /** `POST /api/cwd/validate` — used when restoring a remembered workspace. */ + suspend fun validateCwd(cwd: String): CwdValidation +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ComposerControlsApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ComposerControlsApi.kt new file mode 100644 index 000000000..f59e630f6 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ComposerControlsApi.kt @@ -0,0 +1,160 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.ContextUsageInfo +import io.github.kahme247.ompweb.domain.SessionStats +import io.github.kahme247.ompweb.domain.SessionTokenTotals +import io.github.kahme247.ompweb.domain.ThinkingModelMeta +import io.github.kahme247.ompweb.domain.UsageLimit +import io.github.kahme247.ompweb.domain.parseContextUsage +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull + +/** + * The server calls the composer controls (model, thinking, fast/slow, advisor, + * usage, context sheet, word prediction) make, as an interface so the view + * model runs in JVM tests without a server. + * + * [ApiClient] implements it: [models] and [agentState] already exist, and + * [agentCommand] is the public form of its private `sendAgentCommand` (the + * `?advisor=1` query is the wrapper's spawn flag, see `lib/agent-client.ts`). + */ +interface ComposerControlsApi { + + /** `GET /api/models?cwd=` — the live model catalog the picker builds its rows from. */ + suspend fun models(cwd: String?): ModelsResponse + + /** `GET /api/agent/{id}` — `{running, state}`; never spawns a process. */ + suspend fun agentState(sessionId: String): JsonObject + + /** + * `POST /api/agent/{id}` — one omp RPC command, returning the route's + * `data` field. [advisor] appends `?advisor=1`, the spawn flag carried by + * every command of a session whose advisor toggle is on. [force] appends + * `?force=1`, which skips the server's gate against a session another omp + * process owns — only used after the user confirmed that the send goes + * into a copy. + */ + suspend fun agentCommand( + sessionId: String, + command: JsonObject, + advisor: Boolean = false, + force: Boolean = false, + ): JsonElement? +} + +/** The composer-relevant slice of `WebSessionState` / `get_state`. */ +data class AgentConfigSnapshot( + val model: ThinkingModelMeta? = null, + val thinkingLevel: String? = null, + val fastModeEnabled: Boolean? = null, + val fastModeActive: Boolean? = null, + val slowModeSupported: Boolean? = null, + val slowModeEnabled: Boolean? = null, + val slowModeScope: String? = null, + val usageLimit: UsageLimit? = null, + val contextUsage: ContextUsageInfo? = null, + val tokensPerSecond: Double? = null, + val isCompacting: Boolean? = null, +) + +private fun JsonObject.string(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull + +private fun JsonObject.bool(key: String): Boolean? = (this[key] as? JsonPrimitive)?.booleanOrNull + +private fun JsonObject.number(key: String): Double? = (this[key] as? JsonPrimitive)?.doubleOrNull + +private fun JsonObject.int(key: String): Int? = (this[key] as? JsonPrimitive)?.intOrNull + +private fun JsonObject.obj(key: String): JsonObject? = this[key] as? JsonObject + +/** Read `model` (`OmpModel` shape: `id`, `provider`, `name`, `reasoning`, `thinking.efforts`). */ +fun parseThinkingModel(element: JsonElement?): ThinkingModelMeta? { + val root = element as? JsonObject ?: return null + val provider = root.string("provider") ?: return null + val id = root.string("id") ?: root.string("modelId") ?: return null + val efforts = root.obj("thinking")?.get("efforts") as? JsonArray + return ThinkingModelMeta( + provider = provider, + modelId = id, + name = root.string("name"), + reasoning = root.bool("reasoning"), + efforts = efforts?.mapNotNull { (it as? JsonPrimitive)?.contentOrNull }, + ) +} + +/** Read `usageLimit` (`{stage:"low_priority"|"wrap_up", …}`); unknown stages are ignored. */ +fun parseUsageLimit(element: JsonElement?): UsageLimit? { + val root = element as? JsonObject ?: return null + val stage = when (root.string("stage")) { + "low_priority" -> UsageLimit.Stage.LOW_PRIORITY + "wrap_up" -> UsageLimit.Stage.WRAP_UP + else -> return null + } + return UsageLimit( + stage = stage, + resetsAtSec = root.number("resetsAtSec")?.toLong(), + allowanceLeftPercent = root.number("allowanceLeftPercent"), + extraUsage = root.bool("extraUsage") == true, + ) +} + +/** + * Parse one `WebSessionState`. Accepts the bare state object as well as the + * `{running, state}` envelope `GET /api/agent/{id}` returns, so a caller can + * hand over whichever object it already holds. + */ +fun parseAgentConfig(payload: JsonObject): AgentConfigSnapshot { + val state = (payload["state"] as? JsonObject) ?: payload + return AgentConfigSnapshot( + model = parseThinkingModel(state["model"]), + thinkingLevel = state.string("thinkingLevel"), + fastModeEnabled = state.bool("fastModeEnabled"), + fastModeActive = state.bool("fastModeActive"), + slowModeSupported = state.bool("slowModeSupported"), + slowModeEnabled = state.bool("slowModeEnabled"), + slowModeScope = state.string("slowModeScope"), + usageLimit = parseUsageLimit(state["usageLimit"]), + contextUsage = parseContextUsage(state["contextUsage"]), + tokensPerSecond = state.number("tokensPerSecond"), + isCompacting = state.bool("isCompacting"), + ) +} + +/** Parse the `get_session_stats` answer (`SessionStatsInfo`, plus `sessionName`). */ +fun parseSessionStats(payload: JsonElement?): SessionStats? { + val root = payload as? JsonObject ?: return null + val sessionId = root.string("sessionId") ?: return null + val tokens = root.obj("tokens") + return SessionStats( + sessionId = sessionId, + sessionFile = root.string("sessionFile"), + sessionName = root.string("sessionName"), + userMessages = root.int("userMessages") ?: 0, + assistantMessages = root.int("assistantMessages") ?: 0, + toolCalls = root.int("toolCalls") ?: 0, + toolResults = root.int("toolResults") ?: 0, + totalMessages = root.int("totalMessages") ?: 0, + tokens = SessionTokenTotals( + input = tokens?.int("input") ?: 0, + output = tokens?.int("output") ?: 0, + cacheRead = tokens?.int("cacheRead") ?: 0, + cacheWrite = tokens?.int("cacheWrite") ?: 0, + total = tokens?.int("total") ?: 0, + ), + cost = root.number("cost") ?: 0.0, + contextUsage = parseContextUsage(root["contextUsage"]), + ) +} + +/** The `predict_word` answer's suffix (`{suffix: null}` when omp has nothing). */ +fun parsePredictedSuffix(payload: JsonElement?): String? { + val root = payload as? JsonObject ?: return null + return root.string("suffix")?.takeIf { it.isNotEmpty() } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ExtensionApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ExtensionApi.kt new file mode 100644 index 000000000..b8ddedffb --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ExtensionApi.kt @@ -0,0 +1,26 @@ +package io.github.kahme247.ompweb.net + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +/** + * The `POST /api/agent/{id}` calls the extension-UI and host-tool controllers + * make (`extension_ui_response`, `extension_ui_input`, `set_host_tools`, + * `host_tool_result`, `host_uri_result`, …). + * + * Kept apart from [ChatApi]/[ComposerControlsApi] so those interfaces stay the + * surfaces the chat and composer view models implement in tests; the single + * method is exactly their `agentCommand`. + */ +interface ExtensionApi { + + /** One omp RPC command; returns the route's `data` field (null for fire-and-forget). */ + suspend fun command(sessionId: String, command: JsonObject): JsonElement? +} + +/** Production [ExtensionApi]: [ApiClient]'s public `agentCommand`. */ +class AgentExtensionApi(private val commands: ComposerControlsApi) : ExtensionApi { + + override suspend fun command(sessionId: String, command: JsonObject): JsonElement? = + commands.agentCommand(sessionId, command) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/FilesApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/FilesApi.kt new file mode 100644 index 000000000..bf09ef756 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/FilesApi.kt @@ -0,0 +1,346 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.chat.FileLinks +import io.github.kahme247.ompweb.domain.FileEntry +import io.github.kahme247.ompweb.domain.FileIndexEntry +import io.github.kahme247.ompweb.domain.UploadConflictStrategy +import java.net.URLEncoder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okio.BufferedSink + +/** The metadata `?type=meta` reports: the size label while a document downloads. */ +data class FileMeta(val size: Long = 0) + +/** + * One `/api/file-index` answer: the flat `files` listing (no `q`) or the + * ranked `matches` (with `q`), plus the server's truncation flag. One type for + * the explorer's search and the composer's `@` menu, so the route is parsed + * once. + */ +data class FileIndexPage( + val files: List = emptyList(), + val matches: List = emptyList(), + val truncated: Boolean = false, +) + +/** `?type=upload-check`: names that already exist and the ones replace cannot touch. */ +data class UploadCheckResult(val conflicts: List = emptyList(), val nonReplaceable: List = emptyList()) + +/** One file the upload picker handed over. */ +data class UploadFile(val name: String, val bytes: ByteArray) + +/** A per-file upload failure from the `207`/`200` body. */ +data class UploadFailure(val name: String, val message: String) + +/** + * The answer to one upload request. [conflicts] is only populated for a `409` + * from `?conflict=error`; [status] stays available so callers can tell a + * partial `207` from a clean `200`. + */ +data class UploadOutcome( + val status: Int, + val uploaded: List = emptyList(), + val skipped: List = emptyList(), + val errors: List = emptyList(), + val conflicts: List = emptyList(), + val nonReplaceable: List = emptyList(), +) { + val failed: Boolean get() = status !in 200..299 && status != 409 +} + +/** + * `/api/files/[...path]` and `/api/file-index`, as an interface so the file + * panel runs in JVM tests without a server. + * + * Git status/diff are deliberately absent: `net/GitApi.kt` is their one + * client, and the viewer uses it for its diff mode. + */ +interface FilesApi { + + /** `GET ?type=list` — one directory's entries, dirs first (the server's order). */ + suspend fun listDirectory(directory: String): List + + /** `GET ?type=read` — text or the raw bytes of an image/PDF/docx/audio/binary. */ + suspend fun read(segments: List, sessionId: String?): FileReadResult + + /** `GET ?type=meta` — the size, used by the PDF viewer. */ + suspend fun meta(segments: List, sessionId: String?): FileMeta + + /** `GET ?type=download` — bytes for SAF/share; capped like every body. */ + suspend fun download(segments: List, sessionId: String?): ByteArray + + /** `GET ?type=preview` — the server-rendered docx HTML. */ + suspend fun previewHtml(segments: List, sessionId: String?): String + + /** + * `GET /api/file-index?cwd=[&q=][&limit=][&kind=file][&refresh=1]` — the + * one client of the route: without [query] the flat listing for the + * composer's `@` menu, with it the ranked matches for the explorer. + */ + suspend fun index( + cwd: String, + query: String = "", + limit: Int? = null, + filesOnly: Boolean = false, + refresh: Boolean = false, + ): FileIndexPage + + /** `POST ?type=upload-check` with `{fileNames}`. */ + suspend fun uploadCheck(directory: String, fileNames: List): UploadCheckResult + + /** + * `POST ?type=upload&conflict=` with one `files` part. One request per file + * gives the phone a real per-file progress and a per-file result — and it + * keeps every request under the server's pre-auth proxy cap (26 MiB): + * a 100 MB selection therefore travels as several requests of at most one + * 25 MiB file each, never as one large body. + */ + suspend fun upload( + directory: String, + file: UploadFile, + strategy: UploadConflictStrategy, + onProgress: (Int) -> Unit = {}, + ): UploadOutcome +} + +/** Web `MAX_RESULT_LIMIT` (`lib/file-fuzzy.ts`). */ +const val MAX_FILE_SEARCH_LIMIT = 200 + +/** A SAF/share download larger than this is refused instead of buffered. */ +const val MAX_FILE_DOWNLOAD_BYTES: Long = 64L * 1024 * 1024 + +/** Production [FilesApi] over the app's single authenticated client. */ +fun ApiClient.filesApi(): FilesApi = HttpFilesApi(this) + +private class HttpFilesApi(private val api: ApiClient) : FilesApi { + + private val json = Json { ignoreUnknownKeys = true } + + override suspend fun listDirectory(directory: String): List { + val request = api.requestBuilder(fileApiPath(directory, "list")).get().build() + val exchange = api.exchange(request) + val text = requireOk(exchange, request) + val entries = parseObject(text)?.get("entries") as? JsonArray + ?: throw ApiError.Http(exchange.status, text.take(2048)) + return entries.mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + val name = item.string("name")?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null + FileEntry( + name = name, + isDir = item.boolean("isDir") == true, + size = item.string("size")?.toLongOrNull() ?: 0, + ) + } + } + + override suspend fun read(segments: List, sessionId: String?): FileReadResult = + api.fileRead(segments, sessionId) + + override suspend fun meta(segments: List, sessionId: String?): FileMeta { + val request = api.requestBuilder(fileApiPath(segments, "meta", sessionId)).get().build() + val text = requireOk(api.exchange(request), request) + val root = parseObject(text) ?: return FileMeta() + return FileMeta(size = root.string("size")?.toLongOrNull() ?: 0) + } + + override suspend fun download(segments: List, sessionId: String?): ByteArray = + withContext(Dispatchers.IO) { + val request = api.requestBuilder(fileApiPath(segments, "download", sessionId), accept = "*/*").get().build() + val exchange = api.exchange(request, MAX_FILE_DOWNLOAD_BYTES) + if (exchange.status !in 200..299) throw failure(exchange, request) + exchange.body + } + + override suspend fun previewHtml(segments: List, sessionId: String?): String { + val request = api.requestBuilder(fileApiPath(segments, "preview", sessionId), accept = "text/html").get().build() + return requireOk(api.exchange(request), request) + } + + override suspend fun index( + cwd: String, + query: String, + limit: Int?, + filesOnly: Boolean, + refresh: Boolean, + ): FileIndexPage { + val path = buildString { + append("api/file-index?cwd=").append(encodeQuery(cwd)) + if (query.isNotEmpty()) append("&q=").append(encodeQuery(query)) + if (limit != null) append("&limit=").append(limit) + // The panel ranks files only: directories would consume the budget + // before the limit (see `app/api/file-index/route.ts`). + if (filesOnly) append("&kind=file") + if (refresh) append("&refresh=1") + } + val request = api.requestBuilder(path).get().build() + val text = requireOk(api.exchange(request), request) + val root = parseObject(text) ?: return FileIndexPage() + return FileIndexPage( + files = root.stringList("files"), + matches = (root["matches"] as? JsonArray).orEmpty().mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + val matchPath = item.string("path")?.takeIf { it.isNotBlank() } ?: return@mapNotNull null + FileIndexEntry(matchPath, item.boolean("isDir") == true) + }, + truncated = root.boolean("truncated") == true, + ) + } + + override suspend fun uploadCheck(directory: String, fileNames: List): UploadCheckResult { + val payload = buildJsonObject { + put( + "fileNames", + JsonArray(fileNames.map { JsonPrimitive(it) }), + ) + } + val request = api.requestBuilder(fileApiPath(directory, "upload-check")) + .post(payload.toString().toRequestBody()) + .build() + val exchange = api.exchange(request) + val text = requireOk(exchange, request) + val root = parseObject(text) ?: throw ApiError.Http(exchange.status, text.take(2048)) + return UploadCheckResult( + conflicts = root.stringList("conflicts"), + nonReplaceable = root.stringList("nonReplaceable"), + ) + } + + override suspend fun upload( + directory: String, + file: UploadFile, + strategy: UploadConflictStrategy, + onProgress: (Int) -> Unit, + ): UploadOutcome { + val body = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart( + "files", + file.name, + ProgressRequestBody(file.bytes, "application/octet-stream", onProgress), + ) + .build() + val path = buildString { + append(fileApiPath(directory, "upload")) + append("&conflict=").append(strategy.wire) + } + val request = api.requestBuilder(path).post(body).build() + val exchange = api.exchange(request, MAX_FILE_UPLOAD_BODY_BYTES) + val text = exchange.text() + val root = parseObject(text) + // 409 conflict, 207 partial success and 200 are all answers about files. + if (exchange.status != 409 && exchange.status !in 200..299) throw failure(exchange, request) + return UploadOutcome( + status = exchange.status, + uploaded = root?.stringList("uploaded").orEmpty(), + skipped = root?.stringList("skipped").orEmpty(), + errors = root?.errorList().orEmpty(), + conflicts = root?.stringList("conflicts").orEmpty(), + nonReplaceable = root?.stringList("nonReplaceable").orEmpty(), + ) + } + + private fun requireOk(exchange: RawExchange, request: Request): String { + if (exchange.status in 200..299) return exchange.text() + throw failure(exchange, request) + } + + private fun failure(exchange: RawExchange, request: Request): ApiError { + val text = exchange.text() + // Auth and the server-configuration answers keep their global meaning. + if (exchange.status == 401 || exchange.status == 403 || exchange.status == 503) { + val origin = ServerAddress(request.url.scheme, request.url.host, request.url.port) + return api.errorFor(exchange.status, text, origin) + } + val root = parseObject(text) + val message = root?.string("error") + return if (message != null) { + ApiError.ServerResponse(exchange.status, root.string("code"), message) + } else { + ApiError.Http(exchange.status, text.take(2048)) + } + } + + private fun parseObject(text: String): JsonObject? = + runCatching { json.parseToJsonElement(text) as? JsonObject }.getOrNull() + + private fun JsonObject.string(key: String): String? = + (this[key] as? JsonPrimitive)?.contentOrNull + + private fun JsonObject.boolean(key: String): Boolean? = + (this[key] as? JsonPrimitive)?.booleanOrNull + + private fun JsonObject.stringList(key: String): List = + (this[key] as? JsonArray).orEmpty().mapNotNull { (it as? JsonPrimitive)?.contentOrNull } + + private fun JsonObject.errorList(): List = + (this["errors"] as? JsonArray).orEmpty().mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + val name = item.string("name") ?: return@mapNotNull null + UploadFailure(name, item.string("error") ?: "Upload failed") + } + + private fun fileApiPath(directory: String, type: String, sessionId: String? = null): String = + fileApiPath(FileLinks.apiPathSegments(directory), type, sessionId) + + private fun fileApiPath(segments: List, type: String, sessionId: String? = null): String { + val encoded = segments.joinToString("/") { encodePathSegment(it) } + val query = buildString { + append("api/files/").append(encoded).append("?type=").append(type) + if (!sessionId.isNullOrBlank()) append("&sessionId=").append(encodePathSegment(sessionId)) + } + return query + } + + private fun encodePathSegment(value: String): String = + URLEncoder.encode(value, "UTF-8").replace("+", "%20") + + private fun encodeQuery(value: String): String = URLEncoder.encode(value, "UTF-8") + + private fun String.toRequestBody(): RequestBody = + toRequestBody("application/json; charset=utf-8".toMediaType()) +} + +/** Upload bodies can exceed the generic body cap; the server caps them at 25 MiB. */ +private const val MAX_FILE_UPLOAD_BODY_BYTES: Long = 32L * 1024 * 1024 + +/** A request body that reports upload progress as OkHttp drains it. */ +private class ProgressRequestBody( + private val bytes: ByteArray, + private val contentType: String, + private val onProgress: (Int) -> Unit, +) : RequestBody() { + + override fun contentType() = contentType.toMediaType() + + override fun contentLength(): Long = bytes.size.toLong() + + override fun writeTo(sink: BufferedSink) { + var written = 0 + val total = bytes.size + if (total == 0) { + onProgress(100) + return + } + val chunk = 64 * 1024 + while (written < total) { + val count = minOf(chunk, total - written) + sink.write(bytes, written, count) + written += count + onProgress((written * 100L / total).toInt()) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/GitApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/GitApi.kt new file mode 100644 index 000000000..a66ad3a45 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/GitApi.kt @@ -0,0 +1,61 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.GitFileDiff +import io.github.kahme247.ompweb.domain.GitStatusResponse +import io.github.kahme247.ompweb.domain.parseGitFileDiff +import io.github.kahme247.ompweb.domain.parseGitStatusResponse +import java.net.URLEncoder +import okhttp3.Request + +/** + * `/api/git/status` and `/api/git/diff` (`app/api/git`), as an interface so + * the git-changes view model runs in JVM tests without a server. + * + * Both routes take absolute paths and are gated by the same file allow-list as + * `/api/files`: a 403 or 400 is a normal answer the UI shows, never a crash. + */ +interface GitApi { + + /** `GET /api/git/status?cwd=` — every changed file below [cwd]. */ + suspend fun status(cwd: String): GitStatusResponse + + /** + * `GET /api/git/diff?cwd=&path=` — the unified patch (3 lines of context) + * for one changed file, or `{supported:false}` when git has none. + */ + suspend fun diff(cwd: String, path: String): GitFileDiff +} + +/** Production [GitApi] over the app's single authenticated client. */ +fun ApiClient.gitApi(): GitApi = HttpGitApi(this) + +private class HttpGitApi(private val api: ApiClient) : GitApi { + + override suspend fun status(cwd: String): GitStatusResponse = + get("$STATUS_PATH?cwd=${encodeQuery(cwd)}", ::parseGitStatusResponse) + + override suspend fun diff(cwd: String, path: String): GitFileDiff = + get("$DIFF_PATH?cwd=${encodeQuery(cwd)}&path=${encodeQuery(path)}", ::parseGitFileDiff) + + private suspend fun get(path: String, parse: (String) -> T?): T { + val request = api.requestBuilder(path).get().build() + val response = api.exchange(request) + val text = response.text() + if (response.status !in 200..299) { + throw api.errorFor(response.status, text, originOf(request)) + } + // A 2xx body that is not the documented shape is an Http error, never + // a silently empty changes list. + return parse(text) ?: throw ApiError.Http(response.status, text.take(2048)) + } + + private fun originOf(request: Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + private fun encodeQuery(value: String): String = URLEncoder.encode(value, "UTF-8") + + private companion object { + const val STATUS_PATH = "api/git/status" + const val DIFF_PATH = "api/git/diff" + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/HttpPolicyInterceptor.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/HttpPolicyInterceptor.kt new file mode 100644 index 000000000..4dc09b0b4 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/HttpPolicyInterceptor.kt @@ -0,0 +1,105 @@ +package io.github.kahme247.ompweb.net + +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy +import okhttp3.Call +import okhttp3.EventListener +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response + +/** + * Enforces the plain-HTTP policy on the wire, not only while parsing the + * address a user typed. + * + * [install] adds the check twice on purpose: as an application interceptor it + * refuses a public `http://` URL before OkHttp opens a connection, and as a + * network interceptor it also sees every redirect hop, which an application + * interceptor never does. A redirect to another origin is refused because the + * session cookie belongs to one origin; following it would move the request + * (and its credentials) somewhere the user did not enter. Callers without + * cookies (the app updater, whose release assets legitimately redirect to a + * download host) can opt into [allowCrossOriginRedirects]; the scheme policy + * is still applied to every hop. + * + * The host-name check alone would trust `.local` and `*.ts.net` names, which + * DNS or mDNS can point at any public address. [PlainHttpPolicyListener] + * therefore checks the address OkHttp actually connects to and fails the call + * when a plain-HTTP connection would leave the private network. + */ +class HttpPolicyInterceptor( + private val allowCrossOriginRedirects: Boolean = false, +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val url = chain.request().url + if (!allowCrossOriginRedirects && !sameOrigin(chain.call().request().url, url)) { + throw IOException("Cross-origin redirect to ${url.host} was refused.") + } + if (url.scheme == "http" && !ServerAddress.allowsPlainHttp(url.host)) { + throw IOException(PLAIN_HTTP_MESSAGE) + } + return chain.proceed(chain.request()) + } + + /** + * Fails a plain-HTTP call whose peer address is not loopback, RFC 1918, + * CGNAT (100.64.0.0/10), link-local or an IPv6 unique-local address. + * `connectStart` runs after DNS resolution and before the socket connects, + * so this sees the address that would actually receive the request. + */ + private class PlainHttpPolicyListener : EventListener() { + override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { + val url = call.request().url + if (url.scheme != "http") return + val peer = inetSocketAddress.address ?: throw IOException(PLAIN_HTTP_MESSAGE) + if (!isPrivatePeer(peer)) { + throw IOException( + "Plain HTTP is only allowed for local or private network addresses. " + + "${url.host} resolved to ${peer.hostAddress}, which is not one.", + ) + } + } + } + + companion object { + const val PLAIN_HTTP_MESSAGE = ServerAddress.PLAIN_HTTP_MESSAGE + + fun install( + builder: OkHttpClient.Builder, + allowCrossOriginRedirects: Boolean = false, + ): OkHttpClient.Builder = builder + // A factory, not one shared instance: OkHttp keeps it for the life + // of the client, and a listener that ever gains a field must not be + // shared between concurrent calls. + .eventListenerFactory { PlainHttpPolicyListener() } + .addInterceptor(HttpPolicyInterceptor(allowCrossOriginRedirects)) + .addNetworkInterceptor(HttpPolicyInterceptor(allowCrossOriginRedirects)) + + /** Whether plain HTTP to [address] stays inside a local or private network. */ + fun isPrivatePeer(address: InetAddress): Boolean { + if (address.isLoopbackAddress || address.isSiteLocalAddress || address.isLinkLocalAddress) { + return true + } + val bytes = address.address + if (bytes.size == 4) { + // 100.64.0.0/10, the Tailscale/CGNAT range. + val first = bytes[0].toInt() and 0xff + val second = bytes[1].toInt() and 0xff + return first == 100 && second in 64..127 + } + if (bytes.size == 16) { + // fc00::/7 unique-local; loopback and fe80::/10 are covered above. + return (bytes[0].toInt() and 0xfe) == 0xfc + } + return false + } + + private fun sameOrigin(first: HttpUrl, second: HttpUrl): Boolean = + first.scheme == second.scheme && + first.host.equals(second.host, ignoreCase = true) && + first.port == second.port + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/McpApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/McpApi.kt new file mode 100644 index 000000000..32896e974 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/McpApi.kt @@ -0,0 +1,190 @@ +package io.github.kahme247.ompweb.net + +import java.net.URLEncoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody + +/** One project-level MCP server as `GET /api/mcp` returns it (secrets redacted). */ +data class McpServerEntry(val name: String, val config: JsonObject) + +/** A user-level server; only the fields the UI renders are sent (`safeUser`). */ +data class McpUserServer( + val name: String, + val status: String, + val type: String, + val enabled: Boolean, + val valid: Boolean, +) + +/** The user-level `~/.omp/mcp.json` summary, never its raw configs. */ +data class McpUserConfig( + val path: String, + val servers: List, + val disabledServers: List, + val error: String?, +) + +/** An entry of the merged inventory / live list. */ +data class McpLiveServer(val name: String, val source: String, val status: String, val type: String?) + +/** `GET /api/mcp` answer. [liveServers] is absent unless a session was given. */ +data class McpSnapshot( + val servers: List, + val user: McpUserConfig?, + val inventory: List, + val liveServers: List?, + val liveError: String?, + val path: String?, +) + +/** + * `/api/mcp` (`app/api/mcp/route.ts`): project MCP servers, the user-level + * summary, the disabled list and the live omp view. + */ +interface McpApi { + + /** + * [cwd] null lists only the user-level/discovered servers (the web route + * allows it). [sessionId] asks for omp's live view; [live] false skips the + * child-process call entirely (`?live=0`). + */ + suspend fun load(cwd: String?, sessionId: String? = null, live: Boolean = true, advisor: Boolean = false): McpSnapshot + + /** `PUT` — validation only; returns the server's message ("… is valid"). */ + suspend fun validate(name: String, server: JsonObject): String + + /** `POST` — create or update (rename when [previousName] differs). */ + suspend fun save(cwd: String, name: String, previousName: String?, server: JsonObject) + + /** `DELETE` — remove one project server. */ + suspend fun remove(cwd: String, name: String) +} + +/** Production [McpApi] over the app's authenticated client. */ +class HttpMcpApi(private val api: ApiClient) : McpApi { + + private val json = Json { ignoreUnknownKeys = true } + + override suspend fun load(cwd: String?, sessionId: String?, live: Boolean, advisor: Boolean): McpSnapshot { + val params = buildList { + if (cwd != null) add("cwd=${encode(cwd)}") + if (sessionId != null) add("sessionId=${encode(sessionId)}") + add("live=${if (live) 1 else 0}") + if (advisor) add("advisor=1") + } + val request = api.requestBuilder("api/mcp?${params.joinToString("&")}").get().build() + val body = send(request) + val root = parseObject(body) + return McpSnapshot( + servers = (root["servers"] as? JsonArray).orEmpty().mapNotNull { element -> + val entry = element as? JsonObject ?: return@mapNotNull null + val name = string(entry["name"]) ?: return@mapNotNull null + McpServerEntry(name, entry["config"] as? JsonObject ?: JsonObject(emptyMap())) + }, + user = (root["user"] as? JsonObject)?.let { user -> + McpUserConfig( + path = string(user["path"]).orEmpty(), + servers = (user["servers"] as? JsonArray).orEmpty().mapNotNull { element -> + val entry = element as? JsonObject ?: return@mapNotNull null + val name = string(entry["name"]) ?: return@mapNotNull null + McpUserServer( + name = name, + status = string(entry["status"]) ?: "configured", + type = string(entry["type"]) ?: "stdio", + enabled = boolean(entry["enabled"]) ?: true, + valid = boolean(entry["valid"]) ?: false, + ) + }, + disabledServers = (user["disabledServers"] as? JsonArray).orEmpty() + .mapNotNull { string(it) }, + error = string(user["error"]), + ) + }, + inventory = liveList(root["inventory"]), + liveServers = (root["liveServers"] as? JsonArray)?.let { liveList(it) }, + liveError = string(root["liveError"]), + path = string(root["path"]), + ) + } + + override suspend fun validate(name: String, server: JsonObject): String { + val payload = buildJsonObject { + put("name", name) + put("server", server) + }.toString() + val request = api.requestBuilder(MCP_PATH).put(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + val root = parseObject(send(request)) + return string(root["message"]) ?: "MCP server configuration is valid" + } + + override suspend fun save(cwd: String, name: String, previousName: String?, server: JsonObject) { + val payload = buildJsonObject { + put("cwd", cwd) + put("name", name) + if (previousName != null) put("previousName", previousName) + put("server", server) + }.toString() + val request = api.requestBuilder(MCP_PATH).post(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + send(request) + } + + override suspend fun remove(cwd: String, name: String) { + val payload = buildJsonObject { + put("cwd", cwd) + put("name", name) + }.toString() + val request = api.requestBuilder(MCP_PATH).delete(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + send(request) + } + + private suspend fun send(request: okhttp3.Request): String { + val response = api.exchange(request) + val text = response.text() + if (response.status !in 200..299) { + throw api.errorFor(response.status, text, originOf(request)) + } + return text + } + + private fun parseObject(body: String): JsonObject = try { + json.parseToJsonElement(body).jsonObject + } catch (_: Exception) { + throw ApiError.Http(200, body.take(2048)) + } + + private fun liveList(element: kotlinx.serialization.json.JsonElement?): List = + (element as? JsonArray).orEmpty().mapNotNull { item -> + val entry = item as? JsonObject ?: return@mapNotNull null + val name = string(entry["name"]) ?: return@mapNotNull null + McpLiveServer( + name = name, + source = string(entry["source"]) ?: "", + status = string(entry["status"]) ?: "configured", + type = string(entry["type"]), + ) + } + + private fun string(element: kotlinx.serialization.json.JsonElement?): String? = + (element as? JsonPrimitive)?.takeIf { it.isString }?.content + + private fun boolean(element: kotlinx.serialization.json.JsonElement?): Boolean? = + (element as? JsonPrimitive)?.takeUnless { it.isString }?.booleanOrNull + + private fun originOf(request: okhttp3.Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + private fun encode(value: String): String = URLEncoder.encode(value, "UTF-8") + + private companion object { + const val MCP_PATH = "api/mcp" + val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ModelsConfigApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ModelsConfigApi.kt new file mode 100644 index 000000000..d23d5adb6 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ModelsConfigApi.kt @@ -0,0 +1,294 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.ModelConfig +import io.github.kahme247.ompweb.domain.ModelCostConfig +import io.github.kahme247.ompweb.domain.ModelsFileConfig +import io.github.kahme247.ompweb.domain.ProviderConfig +import io.github.kahme247.ompweb.domain.parseModelsFile +import java.net.URLEncoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * Every provider/model route the Settings tab needs, as an interface so + * `ProvidersModelsViewModel` runs in JVM tests without a server: + * - `GET/PUT /api/models-config[?overwrite=true]` (the editor's file) + * - `POST /api/models-config/test` (isolated registry resolution) + * - `GET /api/models-config/catalog` (models.dev search) + * - `GET/PUT /api/model-roles`, `POST /api/providers/enable` + * - `GET /api/omp-settings` and `PUT` for the registry/retry keys + * - `GET /api/models` for the live catalog (shared with the composer) + */ +interface ModelsConfigApi { + + suspend fun loadModelsConfig(): ModelsConfigDocument + + /** Writes the whole file; [overwriteUnparseable] only after an explicit user confirm. */ + suspend fun saveModelsConfig(config: ModelsFileConfig, overwriteUnparseable: Boolean = false) + + suspend fun testModel(providerName: String, provider: ProviderConfig, model: ModelConfig): ModelTestResult + + suspend fun catalog(query: String, provider: String, baseUrl: String, limit: Int = 30): List + + suspend fun modelRoles(): ModelRolesDocument + + suspend fun saveModelRoles(roles: Map): Map + + suspend fun enableProvider(provider: String) + + suspend fun runtimeModels(): ModelsResponse + + suspend fun ompSettings(): OmpSettingsDocument + + suspend fun saveOmpSettings(settings: JsonObject): JsonObject +} + +/** `GET /api/models-config`: either the parsed file or the parse failure that blocks saving. */ +data class ModelsConfigDocument( + val config: ModelsFileConfig? = null, + val parseError: String? = null, + val path: String? = null, + /** The JSON exactly as received; [ProvidersModelsViewModel] compares it before a save. */ + val raw: JsonObject? = null, +) + +/** One row of `POST /api/models-config/test`; [ok] false carries the server's explanation. */ +data class ModelTestResult( + val ok: Boolean, + val error: String? = null, + val code: String? = null, + val latencyMs: Long? = null, + val responseText: String? = null, +) + +/** One models.dev entry from `GET /api/models-config/catalog` (prices in USD per 1M tokens). */ +data class CatalogModel( + val key: String = "", + val providerId: String = "", + val providerName: String = "", + val providerBaseUrl: String? = null, + val id: String = "", + val name: String = "", + val reasoning: Boolean? = null, + val input: List? = null, + val contextWindow: Int? = null, + val maxTokens: Int? = null, + val cost: ModelCostConfig = ModelCostConfig(), +) + +data class ModelRolesDocument( + val roles: Map = emptyMap(), + val path: String? = null, +) + +/** `GET /api/omp-settings`: the whole settings object plus its file path. */ +data class OmpSettingsDocument( + val settings: JsonObject = JsonObject(emptyMap()), + val path: String? = null, +) + +/** Production [ModelsConfigApi] over the app's single authenticated client. */ +fun ApiClient.modelsConfigApi(): ModelsConfigApi = HttpModelsConfigApi(this) + +private val modelsJson = Json { ignoreUnknownKeys = true; explicitNulls = false } + +private val JSON_MEDIA = "application/json".toMediaType() + +private fun JsonObject.string(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull + +private fun JsonObject.int(key: String): Int? = (this[key] as? JsonPrimitive)?.intOrNull + +private fun JsonObject.boolean(key: String): Boolean? = (this[key] as? JsonPrimitive)?.booleanOrNull + +private fun JsonObject.strings(key: String): List? { + val array = this[key] as? JsonArray ?: return null + return array.mapNotNull { (it as? JsonPrimitive)?.takeIf { primitive -> primitive.isString }?.contentOrNull } +} + +private fun JsonObject.stringMap(key: String): Map? { + val root = this[key] as? JsonObject ?: return null + val result = LinkedHashMap() + root.forEach { (name, value) -> + (value as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull?.let { result[name] = it } + } + return result +} + +private fun JsonPrimitive.doubleOrNull(): Double? = contentOrNull?.toDoubleOrNull() + +private fun parseJsonObject(text: String): JsonObject? = try { + modelsJson.parseToJsonElement(text) as? JsonObject +} catch (_: Exception) { + null +} + +private fun parseCatalogModel(element: JsonElement): CatalogModel? { + val root = element as? JsonObject ?: return null + val id = root.string("id")?.takeIf { it.isNotBlank() } ?: return null + val costRoot = root["cost"] as? JsonObject + return CatalogModel( + key = root.string("key").orEmpty(), + providerId = root.string("providerId").orEmpty(), + providerName = root.string("providerName").orEmpty(), + providerBaseUrl = root.string("providerBaseUrl"), + id = id, + name = root.string("name").orEmpty(), + reasoning = root.boolean("reasoning"), + input = root.strings("input"), + contextWindow = root.int("contextWindow"), + maxTokens = root.int("maxTokens"), + cost = ModelCostConfig( + input = (costRoot?.get("input") as? JsonPrimitive)?.doubleOrNull(), + output = (costRoot?.get("output") as? JsonPrimitive)?.doubleOrNull(), + cacheRead = (costRoot?.get("cacheRead") as? JsonPrimitive)?.doubleOrNull(), + cacheWrite = (costRoot?.get("cacheWrite") as? JsonPrimitive)?.doubleOrNull(), + ), + ) +} + +private class HttpModelsConfigApi(private val api: ApiClient) : ModelsConfigApi { + + override suspend fun loadModelsConfig(): ModelsConfigDocument { + val text = get(CONFIG_PATH) + val root = parseJsonObject(text) ?: throw ApiError.Http(200, text.take(2048)) + val parseError = root.string("parseError") + val path = root.string("path") + return if (parseError != null) { + ModelsConfigDocument(config = null, parseError = parseError, path = path, raw = root) + } else { + ModelsConfigDocument(config = parseModelsFile(root), parseError = null, path = path, raw = root) + } + } + + override suspend fun saveModelsConfig(config: ModelsFileConfig, overwriteUnparseable: Boolean) { + val path = if (overwriteUnparseable) "$CONFIG_PATH?overwrite=true" else CONFIG_PATH + sendJson(path, "PUT", config.toJson().toString()) + } + + override suspend fun testModel( + providerName: String, + provider: ProviderConfig, + model: ModelConfig, + ): ModelTestResult { + val payload = buildJsonObject { + put("providerName", providerName) + put("provider", provider.toJson()) + put("model", model.toJson()) + } + val request = jsonRequest(TEST_PATH, "POST", payload.toString()) + val response = api.exchange(request) + val text = response.text() + val root = parseJsonObject(text) + if (response.status !in 200..299 && root == null) throw api.errorFor(response.status, text, originOf(request)) + if (root == null) throw ApiError.Http(response.status, text.take(2048)) + return ModelTestResult( + ok = root.boolean("ok") == true, + error = root.string("error"), + code = root.string("code"), + latencyMs = root["latencyMs"]?.let { (it as? JsonPrimitive)?.contentOrNull?.toLongOrNull() }, + responseText = root.string("responseText"), + ) + } + + override suspend fun catalog(query: String, provider: String, baseUrl: String, limit: Int): List { + val path = "$CATALOG_PATH?q=${encodeQuery(query)}&provider=${encodeQuery(provider)}" + + "&baseUrl=${encodeQuery(baseUrl)}&limit=$limit" + val text = get(path) + val root = parseJsonObject(text) ?: throw ApiError.Http(200, text.take(2048)) + return (root["models"] as? JsonArray)?.mapNotNull { parseCatalogModel(it) }.orEmpty() + } + + override suspend fun modelRoles(): ModelRolesDocument { + val text = get(ROLES_PATH) + val root = parseJsonObject(text) ?: throw ApiError.Http(200, text.take(2048)) + return ModelRolesDocument( + roles = root.stringMap("roles").orEmpty(), + path = root.string("path"), + ) + } + + override suspend fun saveModelRoles(roles: Map): Map { + val payload = buildJsonObject { + put("roles", buildJsonObject { roles.forEach { (role, selector) -> put(role, selector) } }) + } + val text = sendJson(ROLES_PATH, "PUT", payload.toString()) + val root = parseJsonObject(text) ?: throw ApiError.Http(200, text.take(2048)) + return root.stringMap("roles") ?: roles + } + + override suspend fun enableProvider(provider: String) { + val payload = buildJsonObject { put("provider", provider) } + sendJson(ENABLE_PROVIDER_PATH, "POST", payload.toString()) + } + + override suspend fun runtimeModels(): ModelsResponse = api.models(null) + + override suspend fun ompSettings(): OmpSettingsDocument { + val text = get(OMP_SETTINGS_PATH) + val root = parseJsonObject(text) ?: throw ApiError.Http(200, text.take(2048)) + return OmpSettingsDocument( + settings = root["settings"] as? JsonObject ?: JsonObject(emptyMap()), + path = root.string("path"), + ) + } + + override suspend fun saveOmpSettings(settings: JsonObject): JsonObject { + val payload = buildJsonObject { put("settings", settings) } + val text = sendJson(OMP_SETTINGS_PATH, "PUT", payload.toString()) + val root = parseJsonObject(text) ?: throw ApiError.Http(200, text.take(2048)) + return root["settings"] as? JsonObject ?: settings + } + + // --- transport --------------------------------------------------------- + + private suspend fun get(path: String): String { + val request = api.requestBuilder(path).get().build() + return requireSuccess(request) + } + + private suspend fun sendJson(path: String, method: String, payload: String): String = + requireSuccess(jsonRequest(path, method, payload)) + + private fun jsonRequest(path: String, method: String, payload: String): Request { + val body = payload.toRequestBody(JSON_MEDIA) + val builder = api.requestBuilder(path) + return when (method) { + "PUT" -> builder.put(body).build() + "DELETE" -> builder.delete(body).build() + else -> builder.post(body).build() + } + } + + private suspend fun requireSuccess(request: Request): String { + val response = api.exchange(request) + val text = response.text() + if (response.status !in 200..299) throw api.errorFor(response.status, text, originOf(request)) + return text + } + + private fun originOf(request: Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + private companion object { + const val CONFIG_PATH = "api/models-config" + const val TEST_PATH = "api/models-config/test" + const val CATALOG_PATH = "api/models-config/catalog" + const val ROLES_PATH = "api/model-roles" + const val ENABLE_PROVIDER_PATH = "api/providers/enable" + const val OMP_SETTINGS_PATH = "api/omp-settings" + } +} + +private fun encodeQuery(value: String): String = URLEncoder.encode(value, "UTF-8") diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/NativeSettingsApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/NativeSettingsApi.kt new file mode 100644 index 000000000..5ea9ee3c3 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/NativeSettingsApi.kt @@ -0,0 +1,161 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.NativeSettingsDocument +import java.io.IOException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody + +/** `GET /api/omp-settings` answer: the config file path plus its native keys. */ +data class NativeSettingsSnapshot( + val path: String, + val document: NativeSettingsDocument, +) + +/** + * `/api/omp-settings` (`app/api/omp-settings/route.ts`), behind an interface + * so the settings view model runs in JVM tests without a server. + * + * The API only moves whole snapshots: the server merges the keys it owns into + * the user's `config.yml` and leaves everything else untouched, so unknown + * keys that came back from [load] must be sent back unchanged. + */ +interface NativeSettingsApi { + + suspend fun load(): NativeSettingsSnapshot + + /** `PUT {settings}` — the whole snapshot; answers the server's canonical settings. */ + suspend fun save(document: NativeSettingsDocument): NativeSettingsSnapshot +} + +/** + * Production [NativeSettingsApi]. The web aborts its GET/PUT after 12 s; a + * settings file can be large and omp's utility process may be busy, but a + * silent minute-long hang is worse than an explicit retryable failure. + */ +class HttpNativeSettingsApi( + private val api: ApiClient, + /** The web's 12 s abort; tests shrink it instead of waiting. */ + private val timeoutMillis: Long = TIMEOUT_MS, +) : NativeSettingsApi { + + private val json = Json { ignoreUnknownKeys = true } + + /** + * OkHttp's blocking `execute` does not react to coroutine cancellation, so + * the abort cancels the [okhttp3.Call] itself from a timer; that unblocks + * the IO thread immediately instead of waiting for the client's 60 s read + * timeout. + */ + private val executor = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + override suspend fun load(): NativeSettingsSnapshot = request("omp-settings") { + api.requestBuilder(PATH).get().build() + } + + override suspend fun save(document: NativeSettingsDocument): NativeSettingsSnapshot { + val payload = buildJsonObject { put("settings", document.root) }.toString() + return request("save native settings") { + api.requestBuilder(PATH) + .put(payload.toRequestBody(JSON_MEDIA_TYPE)) + .build() + } + } + + private suspend fun request(label: String, build: () -> okhttp3.Request): NativeSettingsSnapshot = + withContext(Dispatchers.IO) { + val request = build() + val call = api.openStream(request) + val timedOut = java.util.concurrent.atomic.AtomicBoolean(false) + // The whole exchange (headers *and* body) runs inside the timer + // window: cancelling the Call also stops a body that trickles in + // after the headers, and the capped read stops a chunked body + // that has no Content-Length (P11Sec 1). + val timer = executor.launch { + delay(timeoutMillis) + timedOut.set(true) + call.cancel() + } + val exchange = try { + executor.async { executeAndRead(call, label, timedOut) }.await() + } catch (cancelled: kotlinx.coroutines.CancellationException) { + call.cancel() + throw cancelled + } finally { + timer.cancel() + } + if (exchange.code !in 200..299) { + throw api.errorFor(exchange.code, exchange.text, originOf(request)) + } + parse(exchange.text, exchange.code) + } + + private fun executeAndRead( + call: okhttp3.Call, + label: String, + timedOut: java.util.concurrent.atomic.AtomicBoolean, + ): NativeExchange = try { + call.execute().use { response -> + NativeExchange( + code = response.code, + // Same hard cap as `ApiClient`'s exchange, counted per byte: + // a chunked answer has `contentLength() == -1`. + text = api.readCappedBytes(response, MAX_BODY_BYTES).toString(Charsets.UTF_8), + ) + } + } catch (error: IOException) { + throw networkFailure(label, error, timedOut) + } catch (error: ApiError.Network) { + // The capped read maps a mid-body abort (our timeout) here. + throw networkFailure(label, (error.cause as? IOException) ?: IOException(error), timedOut) + } + + private fun networkFailure( + label: String, + error: IOException, + timedOut: java.util.concurrent.atomic.AtomicBoolean, + ): ApiError.Network = + if (timedOut.get()) ApiError.Network(IOException("$label timed out", error)) else ApiError.Network(error) + + private data class NativeExchange(val code: Int, val text: String) + + private fun parse(body: String, status: Int): NativeSettingsSnapshot { + val root = try { + json.parseToJsonElement(body).jsonObject + } catch (_: Exception) { + throw ApiError.Http(status, body.take(2048)) + } + (root["error"] as? kotlinx.serialization.json.JsonPrimitive)?.contentOrNull?.let { + throw ApiError.ServerResponse(status, null, it) + } + val settings = root["settings"] as? JsonObject ?: throw ApiError.Http(status, body.take(2048)) + val path = (root["path"] as? kotlinx.serialization.json.JsonPrimitive)?.contentOrNull.orEmpty() + return NativeSettingsSnapshot(path, NativeSettingsDocument(settings)) + } + + private fun originOf(request: okhttp3.Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + /** `internal` so the JVM test can pin the web's 12 s abort without waiting for it. */ + internal companion object { + private const val PATH = "api/omp-settings" + const val TIMEOUT_MS = 12_000L + + /** Same ceiling as `ApiClient`'s capped exchange; a config file never comes close. */ + private const val MAX_BODY_BYTES = 4L * 1024 * 1024 + + private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/NotificationHistoryApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/NotificationHistoryApi.kt new file mode 100644 index 000000000..d0589308c --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/NotificationHistoryApi.kt @@ -0,0 +1,98 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.NOTIFICATION_HISTORY_MAX_IDS +import io.github.kahme247.ompweb.domain.NotificationHistoryEntry +import io.github.kahme247.ompweb.domain.isValidNotificationHistoryId +import io.github.kahme247.ompweb.domain.parseNotificationHistoryEntries +import java.net.URLEncoder +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * `/api/notifications/history` (`app/api/notifications/history/route.ts`), as + * an interface so the history view model runs in JVM tests without a server. + * + * Every method throws an [ApiError] on failure: the view model treats a failed + * change request as "the server did not accept it" and reloads the server's + * list instead of keeping a local state the server does not have. + */ +interface NotificationHistoryApi { + + /** `GET` — every entry, newest first. */ + suspend fun load(): List + + /** `PATCH {ids}` — mark up to [NOTIFICATION_HISTORY_MAX_IDS] entries read. */ + suspend fun markRead(ids: List) + + /** `DELETE ?id=` — remove one entry. */ + suspend fun remove(id: String) + + /** `DELETE ?all=1` — clear the whole history. */ + suspend fun clear() +} + +/** + * The production [NotificationHistoryApi] over the app's single authenticated + * client (origin binding, cookie jar, policy interceptor, typed errors). + */ +class HttpNotificationHistoryApi(private val api: ApiClient) : NotificationHistoryApi { + + private val json = Json { ignoreUnknownKeys = true } + + override suspend fun load(): List { + val request = api.requestBuilder(HISTORY_PATH).get().build() + val response = api.exchange(request) + val text = response.text() + if (response.status !in 200..299) throw api.errorFor(response.status, text, originOf(request)) + return try { + parseNotificationHistoryEntries(json.parseToJsonElement(text)) + } catch (_: SerializationException) { + throw ApiError.Http(200, text.take(2048)) + } + } + + override suspend fun markRead(ids: List) { + val wanted = ids.asSequence() + .filter(::isValidNotificationHistoryId) + .distinct() + .take(NOTIFICATION_HISTORY_MAX_IDS) + .toList() + if (wanted.isEmpty()) return + val body = buildJsonObject { + put("ids", buildJsonArray { wanted.forEach { add(JsonPrimitive(it)) } }) + }.toString() + send(api.requestBuilder(HISTORY_PATH).patch(body.toRequestBody(JSON_MEDIA_TYPE)).build()) + } + + override suspend fun remove(id: String) { + if (!isValidNotificationHistoryId(id)) return + send(api.requestBuilder("$HISTORY_PATH?id=${encodeQuery(id)}").delete().build()) + } + + override suspend fun clear() { + send(api.requestBuilder("$HISTORY_PATH?all=1").delete().build()) + } + + private suspend fun send(request: okhttp3.Request) { + val response = api.exchange(request) + if (response.status !in 200..299) { + throw api.errorFor(response.status, response.text(), originOf(request)) + } + } + + private fun originOf(request: okhttp3.Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + private fun encodeQuery(value: String): String = URLEncoder.encode(value, "UTF-8") + + private companion object { + const val HISTORY_PATH = "api/notifications/history" + val JSON_MEDIA_TYPE = "application/json".toMediaType() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/NotificationsApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/NotificationsApi.kt new file mode 100644 index 000000000..ac15c0c75 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/NotificationsApi.kt @@ -0,0 +1,222 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.NotifyAnswer +import io.github.kahme247.ompweb.domain.NotifyPrefs +import io.github.kahme247.ompweb.domain.NotifyProject +import io.github.kahme247.ompweb.domain.PresenceReport +import io.github.kahme247.ompweb.domain.notifyProject +import java.net.URLEncoder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody + +/** What `POST /api/notifications/test` answered. */ +sealed interface TestPushResult { + data object Sent : TestPushResult + + /** 502 `push_failed`: the push service's own answer, shown verbatim. */ + data class Failed(val message: String) : TestPushResult +} + +/** The three 200 statuses of `POST /api/notifications/answer`. */ +enum class AnswerOutcome { Answered, AlreadySettled, SessionGone } + +/** + * The notification routes of ompweb (`docs/android/protocol.md` §6.3). Kept + * behind an interface so the service/receiver can be tested without a server. + */ +interface NotificationsApi { + /** + * `PUT /api/notifications/devices`; an omitted subscription keeps a + * stored one. [origin] pins a captured server for the best-effort + * unregister during sign-out, when the live origin may already change. + */ + suspend fun putDevice(deviceId: String, prefs: NotifyPrefs, origin: ServerAddress? = null) + + /** Whether the server holds a Web Push subscription for this device (native never does). */ + suspend fun isSubscribed(deviceId: String): Boolean + + /** `POST /api/notifications/presence` — best effort, 204 expected. */ + suspend fun reportPresence(report: PresenceReport) + + suspend fun sendTest(deviceId: String): TestPushResult + + /** Answers a pending dialog without spawning omp. */ + suspend fun answer(sessionId: String, requestId: String, answer: NotifyAnswer): AnswerOutcome + + /** + * Session id → project for the notification groups, from `GET /api/sessions` + * (`projectKey`, else `projectRoot`, else `cwd`). Only ids present in the + * list are returned; a missing session stays ungrouped. + */ + suspend fun sessionProjects(): Map + + /** + * A notification "Reply" on a completed run is a normal follow-up prompt: + * `POST /api/agent/{id} {type:"prompt"}` (spawns the session when it is no + * longer live, exactly like sending from the chat). + */ + suspend fun followUp(sessionId: String, message: String) +} + +class HttpNotificationsApi( + private val api: ApiClient, + private val origin: () -> ServerAddress?, +) : NotificationsApi { + + private val json = Json { ignoreUnknownKeys = true } + + override suspend fun putDevice( + deviceId: String, + prefs: NotifyPrefs, + origin: ServerAddress?, + ) = withContext(Dispatchers.IO) { + val body = buildJsonObject { + put("deviceId", deviceId) + put("prefs", prefs.toJson()) + } + val builder = origin?.let { + okhttp3.Request.Builder() + .url(it.apiUrl("api/notifications/devices")) + .header("Accept", "application/json") + } ?: api.requestBuilder("api/notifications/devices") + // A pinned request must also report its own origin: a late 401 of the + // previous installation must not sign out the configured one. + requireOk( + api.exchange( + builder.put(body.toString().toRequestBody(JSON_MEDIA_TYPE)).build(), + ), + requestOrigin = origin ?: origin(), + ) + } + + override suspend fun isSubscribed(deviceId: String): Boolean = withContext(Dispatchers.IO) { + val response = api.exchange( + api.requestBuilder("api/notifications/devices?deviceId=${encodeQuery(deviceId)}").get().build(), + ) + if (response.status !in 200..299) return@withContext false + (decode(response.text())["subscribed"] as? kotlinx.serialization.json.JsonPrimitive) + ?.contentOrNull + ?.toBooleanStrictOrNull() + ?: false + } + + override suspend fun reportPresence(report: PresenceReport) { + val body = buildJsonObject { + put("clientId", report.clientId) + put("deviceId", report.deviceId) + put("visible", report.visible) + report.sessionId?.let { put("sessionId", it) } + put("seq", report.seq) + } + // Presence is a hint, never a hard dependency: a failed report must not + // surface anywhere (and must not sign the user out on a 401). + runCatching { + withContext(Dispatchers.IO) { + api.exchange( + api.requestBuilder("api/notifications/presence") + .post(body.toString().toRequestBody(JSON_MEDIA_TYPE)) + .build(), + ) + } + } + } + + override suspend fun sendTest(deviceId: String): TestPushResult = withContext(Dispatchers.IO) { + val body = buildJsonObject { put("deviceId", deviceId) } + val response = api.exchange( + api.requestBuilder("api/notifications/test") + .post(body.toString().toRequestBody(JSON_MEDIA_TYPE)) + .build(), + ) + when { + response.status in 200..299 -> TestPushResult.Sent + response.status == 502 -> TestPushResult.Failed(errorMessage(response.text())) + else -> throw api.errorFor(response.status, response.text(), origin() ?: throw ApiError.NotConfigured) + } + } + + override suspend fun answer(sessionId: String, requestId: String, answer: NotifyAnswer): AnswerOutcome = + withContext(Dispatchers.IO) { + val body = buildJsonObject { + put("sessionId", sessionId) + put("requestId", requestId) + put("response", answer.toJson()) + } + val response = api.exchange( + api.requestBuilder("api/notifications/answer") + .post(body.toString().toRequestBody(JSON_MEDIA_TYPE)) + .build(), + ) + requireOk(response) + when (decode(response.text())["status"]?.jsonPrimitive?.contentOrNull) { + "answered" -> AnswerOutcome.Answered + "alreadySettled" -> AnswerOutcome.AlreadySettled + "sessionGone" -> AnswerOutcome.SessionGone + else -> throw ApiError.Http(200, response.text().take(2048)) + } + } + + override suspend fun sessionProjects(): Map = withContext(Dispatchers.IO) { + // `GET /api/sessions` is conditional (ETag), so a refresh is one 304 + // once the list is warm. A project is the canonical `projectKey` (which + // collapses worktrees), else the session's projectRoot/cwd. + val list = api.sessions() + list.sessions.mapNotNull { session -> + val project = notifyProject(session.projectKey ?: session.projectRoot ?: session.cwd) + ?: return@mapNotNull null + session.id to project + }.toMap() + } + + override suspend fun followUp(sessionId: String, message: String) = withContext(Dispatchers.IO) { + val body = buildJsonObject { + put("type", "prompt") + put("message", message) + } + requireOk( + api.exchange( + api.requestBuilder("api/agent/${encodePathSegment(sessionId)}") + .post(body.toString().toRequestBody(JSON_MEDIA_TYPE)) + .build(), + limit = MAX_AGENT_RESPONSE_BYTES, + ), + ) + } + + private fun requireOk(response: RawExchange, requestOrigin: ServerAddress? = origin()) { + if (response.status in 200..299) return + throw api.errorFor(response.status, response.text(), requestOrigin ?: throw ApiError.NotConfigured) + } + + private fun decode(body: String): JsonObject = try { + json.parseToJsonElement(body).jsonObject + } catch (_: Exception) { + throw ApiError.Http(200, body.take(2048)) + } + + private fun errorMessage(body: String): String = runCatching { decode(body)["error"]?.jsonPrimitive?.contentOrNull } + .getOrNull() + ?.takeIf { it.isNotBlank() } + ?: "push_failed" + + private fun encodePathSegment(value: String): String = + URLEncoder.encode(value, "UTF-8").replace("+", "%20") + + private fun encodeQuery(value: String): String = URLEncoder.encode(value, "UTF-8") + + private companion object { + val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + + /** A prompt answer is a small JSON ack, not a transcript dump. */ + const val MAX_AGENT_RESPONSE_BYTES = 4L * 1024 * 1024 + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/PluginsApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/PluginsApi.kt new file mode 100644 index 000000000..bba6d715f --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/PluginsApi.kt @@ -0,0 +1,179 @@ +package io.github.kahme247.ompweb.net + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody + +/** Resource counts of one plugin or of the whole plugin set. */ +data class PluginResourceCounts( + val extensions: Int = 0, + val skills: Int = 0, + val prompts: Int = 0, + val themes: Int = 0, +) { + val total: Int get() = extensions + skills + prompts + themes +} + +/** One file a plugin contributes. */ +data class PluginResourceInfo(val kind: String, val name: String, val path: String, val relativePath: String) + +/** A plugin warning/error line. */ +data class PluginDiagnostic(val type: String, val message: String, val source: String?, val path: String?) + +/** One installed/configured plugin (`PluginPackageInfo`). */ +data class PluginPackageInfo( + val source: String, + val scope: String, + val filtered: Boolean, + val disabled: Boolean, + val installedPath: String?, + val packageName: String?, + val version: String?, + val configuredVersion: String?, + val counts: PluginResourceCounts, + val resources: List, + val status: String, +) + +/** `GET /api/plugins` and every action's refreshed answer. */ +data class PluginsSnapshot( + val packages: List, + val totals: PluginResourceCounts, + val diagnostics: List, +) + +/** `POST /api/plugins` actions. */ +enum class PluginAction(val wire: String) { INSTALL("install"), REMOVE("remove"), UPDATE("update"), DISABLE("disable"), ENABLE("enable") } + +/** + * `/api/plugins` (`app/api/plugins/route.ts`): plugin management delegated to + * the user's omp binary. Every action answers the refreshed full list. + */ +interface PluginsApi { + + suspend fun load(cwd: String): PluginsSnapshot + + /** `scope` is `global` or `project`; [source] is required by omp for most actions. */ + suspend fun action(action: PluginAction, source: String?, scope: String, cwd: String): PluginsSnapshot + + /** Reload the open session so its omp process picks up plugin changes. */ + suspend fun reload(sessionId: String) +} + +/** Production [PluginsApi]; reload reuses the chat transport's `POST /api/agent/{id}/reload`. */ +class HttpPluginsApi(private val api: ApiClient) : PluginsApi { + + private val json = Json { ignoreUnknownKeys = true } + + override suspend fun load(cwd: String): PluginsSnapshot { + val request = api.requestBuilder("$PLUGINS_PATH?cwd=${java.net.URLEncoder.encode(cwd, "UTF-8")}").get().build() + return parse(send(request)) + } + + override suspend fun action(action: PluginAction, source: String?, scope: String, cwd: String): PluginsSnapshot { + val payload = buildJsonObject { + put("action", action.wire) + if (source != null) put("source", source) + put("scope", scope) + put("cwd", cwd) + }.toString() + val request = api.requestBuilder(PLUGINS_PATH).post(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + return parse(send(request)) + } + + override suspend fun reload(sessionId: String) { + api.reload(sessionId) + } + + private suspend fun send(request: okhttp3.Request): String { + val response = api.exchange(request) + val text = response.text() + if (response.status !in 200..299) { + throw api.errorFor(response.status, text, originOf(request)) + } + return text + } + + private fun parse(body: String): PluginsSnapshot { + val root = try { + json.parseToJsonElement(body).jsonObject + } catch (_: Exception) { + throw ApiError.Http(200, body.take(2048)) + } + return PluginsSnapshot( + packages = (root["packages"] as? JsonArray).orEmpty().mapNotNull(::parsePackage), + totals = parseCounts(root["totals"] as? JsonObject), + diagnostics = (root["diagnostics"] as? JsonArray).orEmpty().mapNotNull { element -> + val entry = element as? JsonObject ?: return@mapNotNull null + PluginDiagnostic( + type = string(entry["type"]) ?: "warning", + message = string(entry["message"]).orEmpty(), + source = string(entry["source"]), + path = string(entry["path"]), + ) + }, + ) + } + + private fun parsePackage(element: JsonElement): PluginPackageInfo? { + val entry = element as? JsonObject ?: return null + val source = string(entry["source"]) ?: return null + return PluginPackageInfo( + source = source, + scope = string(entry["scope"]) ?: "global", + filtered = boolean(entry["filtered"]) ?: false, + disabled = boolean(entry["disabled"]) ?: false, + installedPath = string(entry["installedPath"]), + packageName = string(entry["packageName"]), + version = string(entry["version"]), + configuredVersion = string(entry["configuredVersion"]), + counts = parseCounts(entry["counts"] as? JsonObject), + resources = (entry["resources"] as? JsonArray).orEmpty().mapNotNull { item -> + val resource = item as? JsonObject ?: return@mapNotNull null + PluginResourceInfo( + kind = string(resource["kind"]) ?: "extension", + name = string(resource["name"]).orEmpty(), + path = string(resource["path"]).orEmpty(), + relativePath = string(resource["relativePath"]).orEmpty(), + ) + }, + status = string(entry["status"]) ?: "installed", + ) + } + + private fun parseCounts(entry: JsonObject?): PluginResourceCounts = PluginResourceCounts( + extensions = (entry?.get("extensions") as? JsonPrimitive)?.intOrNull ?: 0, + skills = (entry?.get("skills") as? JsonPrimitive)?.intOrNull ?: 0, + prompts = (entry?.get("prompts") as? JsonPrimitive)?.intOrNull ?: 0, + themes = (entry?.get("themes") as? JsonPrimitive)?.intOrNull ?: 0, + ) + + private fun string(element: JsonElement?): String? = + (element as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull + + private fun boolean(element: JsonElement?): Boolean? = + (element as? JsonPrimitive)?.takeUnless { it.isString }?.let { p -> + when (p.content) { + "true" -> true + "false" -> false + else -> null + } + } + + private fun originOf(request: okhttp3.Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + private companion object { + const val PLUGINS_PATH = "api/plugins" + val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/SearchApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/SearchApi.kt new file mode 100644 index 000000000..df1403ae8 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/SearchApi.kt @@ -0,0 +1,45 @@ +package io.github.kahme247.ompweb.net + +import kotlinx.serialization.Serializable + +/** + * One hit of `GET /api/sessions/messages/search` (server + * `lib/session-message-search.ts`). Offsets are UTF-16 code-unit positions + * inside [snippet], the same unit Kotlin strings use, so the row can highlight + * without recomputing the match. + */ +@Serializable +data class SessionSearchHit( + val sessionId: String = "", + val title: String = "", + val entryId: String = "", + val role: String = "user", + val timestamp: String = "", + val snippet: String = "", + val matchStart: Int = 0, + val matchEnd: Int = 0, +) + +/** + * One bounded page of the search; [cursor] resumes a partial scan. + * + * [skippedFiles] counts session files this page had to leave out because they + * exceed the server's 64 MiB scan limit (`SEARCH_LARGE_FILE_BYTES`): their text + * is not searchable, so a miss is not proof that the phrase is absent. + */ +@Serializable +data class SessionSearchPage( + val results: List = emptyList(), + val partial: Boolean = false, + val cursor: String? = null, + val skippedFiles: Int = 0, +) + +/** Full-text search for the session list ("Search in messages"). */ +interface SessionSearchApi { + suspend fun searchSessions( + query: String, + limit: Int = 20, + cursor: String? = null, + ): SessionSearchPage +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ServerAddress.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ServerAddress.kt new file mode 100644 index 000000000..d2c3a4c1f --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ServerAddress.kt @@ -0,0 +1,225 @@ +package io.github.kahme247.ompweb.net + +/** + * A validated ompweb server address. + * + * [scheme] is `http` or `https`, [host] never contains brackets, and [port] + * is always explicit. [origin] is safe to concatenate with API paths. + */ +data class ServerAddress( + val scheme: String, + val host: String, + val port: Int = DEFAULT_PORT, +) { + val origin: String + get() { + val renderedHost = if (host.contains(':')) "[$host]" else host + return "$scheme://$renderedHost:$port" + } + + fun apiUrl(path: String): String = "$origin/${path.trimStart('/')}" + + companion object { + const val DEFAULT_PORT = 30177 + + /** The one wording for a refused plain-HTTP address. */ + const val PLAIN_HTTP_MESSAGE = + "Plain HTTP is only allowed for local or private network addresses. Use https:// for this host." + + /** + * Parses what people actually type into the app: + * `100.64.0.10`, `100.64.0.10:30177`, `host`, `host.tailnet.ts.net`, + * full `http://` / `https://` URLs and bracketed IPv6 literals. + * + * This is the host-name pre-check: plain HTTP is accepted only for + * loopback, RFC 1918, CGNAT (100.64.0.0/10), link-local, `.local`, + * single-label hosts and Tailscale MagicDNS (`*.ts.net`). The binding + * decision is made on the resolved peer address in + * [HttpPolicyInterceptor], because a name alone cannot prove that the + * server sits on a private network. + */ + fun parse(raw: String): ServerAddressResult { + val input = raw.trim() + if (input.isEmpty()) return ServerAddressResult.Invalid("Enter a server address.") + if (input.length > 512) return ServerAddressResult.Invalid("The address is too long.") + if (input.any { it.isWhitespace() }) { + return ServerAddressResult.Invalid("The address must not contain spaces.") + } + if (input.any { it.code < 0x20 || it.code == 0x7f }) { + return ServerAddressResult.Invalid("The address contains characters that are not allowed.") + } + + val schemeSeparator = input.indexOf("://") + val explicitScheme = if (schemeSeparator > 0) { + val candidate = input.substring(0, schemeSeparator) + if (!SCHEME_NAME.matches(candidate)) { + return ServerAddressResult.Invalid("Enter a server address.") + } + candidate.lowercase().also { scheme -> + if (scheme != "http" && scheme != "https") { + return ServerAddressResult.Invalid("Only http:// and https:// addresses are supported.") + } + } + } else { + null + } + // A copied browser URL usually ends in the root path `/`; that one + // trailing slash is not a path and is accepted (nothing deeper is). + val authority = (if (explicitScheme != null) { + input.substring(schemeSeparator + 3) + } else { + input + }).removeSuffix("/") + + when { + authority.isEmpty() -> return ServerAddressResult.Invalid("Enter a server address.") + authority.contains('/') -> + return ServerAddressResult.Invalid("The address must not contain a path.") + authority.contains('?') -> + return ServerAddressResult.Invalid("The address must not contain a query.") + authority.contains('#') -> + return ServerAddressResult.Invalid("The address must not contain a fragment.") + authority.contains('@') -> + return ServerAddressResult.Invalid("Do not include a username or password in the address.") + } + + val parts = when (val split = splitHostAndPort(authority)) { + is HostAndPort -> split + is HostPortError -> return ServerAddressResult.Invalid(split.message) + } + + val host = parts.host.lowercase().removeSuffix(".") + if (host.isEmpty()) return ServerAddressResult.Invalid("Enter a server address.") + + val port = parts.portText?.let { portText -> + val value = portText.toIntOrNull() + if (value == null || value !in 1..65535) { + return ServerAddressResult.Invalid("The port must be a number between 1 and 65535.") + } + value + } ?: DEFAULT_PORT + + val hostError = validateHost(host) + if (hostError != null) return ServerAddressResult.Invalid(hostError) + + val scheme = explicitScheme ?: defaultScheme(host) + if (scheme == "http" && defaultScheme(host) == "https") { + return ServerAddressResult.Invalid(PLAIN_HTTP_MESSAGE) + } + + return ServerAddressResult.Valid(ServerAddress(scheme = scheme, host = host, port = port)) + } + + private val SCHEME_NAME = Regex("[A-Za-z][A-Za-z0-9+.-]*") + + private sealed interface HostPortSplit + private data class HostAndPort(val host: String, val portText: String?) : HostPortSplit + private data class HostPortError(val message: String) : HostPortSplit + + private fun splitHostAndPort(authority: String): HostPortSplit { + if (authority.startsWith("[")) { + val close = authority.indexOf(']') + if (close < 0) { + return HostPortError("Close the IPv6 address with a bracket, for example [fd7a::1]:30177.") + } + val host = authority.substring(1, close) + if (!isIpv6(host)) return HostPortError("That is not a valid IPv6 address.") + val tail = authority.substring(close + 1) + return when { + tail.isEmpty() -> HostAndPort(host, null) + tail.startsWith(":") -> HostAndPort(host, tail.substring(1)) + else -> HostPortError("Use only :port after the IPv6 bracket.") + } + } + return when (authority.count { it == ':' }) { + 0 -> HostAndPort(authority, null) + 1 -> HostAndPort(authority.substringBefore(':'), authority.substringAfter(':')) + else -> HostPortError("Put an IPv6 address in brackets, for example [fd7a::1]:30177.") + } + } + + private fun validateHost(host: String): String? { + if (isIpv6(host) || isIpv4(host)) return null + if (host.length > 253) return "The host name is too long." + val labels = host.split('.') + if (labels.any { label -> + label.isEmpty() || + label.length > 63 || + label.first() == '-' || + label.last() == '-' || + label.any { !it.isLetterOrDigit() && it != '-' } + } + ) { + return "The address contains characters that are not valid in a host name." + } + return null + } + + private fun isIpv4(host: String): Boolean { + val parts = host.split('.') + if (parts.size != 4) return false + return parts.all { part -> + part.isNotEmpty() && + part.length <= 3 && + part.all(Char::isDigit) && + part.toInt() in 0..255 + } + } + + private fun isIpv6(host: String): Boolean { + if (!host.contains(':')) return false + return try { + java.net.InetAddress.getByName(host) + true + } catch (_: Exception) { + false + } + } + + private fun defaultScheme(host: String): String { + if (isIpv4(host)) { + val octets = host.split('.').map(String::toInt) + val privateIpv4 = octets[0] == 127 || + octets[0] == 10 || + (octets[0] == 192 && octets[1] == 168) || + (octets[0] == 172 && octets[1] in 16..31) || + (octets[0] == 100 && octets[1] in 64..127) || + (octets[0] == 169 && octets[1] == 254) + return if (privateIpv4) "http" else "https" + } + if (isIpv6(host)) { + val firstHextet = host.removePrefix("[").substringBefore(':').ifEmpty { "0" } + val value = firstHextet.toIntOrNull(16) ?: 0xffff + val linkLocal = value and 0xffc0 == 0xfe80 + val uniqueLocal = value and 0xfe00 == 0xfc00 + val loopback = host == "::1" || host == "0:0:0:0:0:0:0:1" + return if (loopback || linkLocal || uniqueLocal) "http" else "https" + } + val lower = host.lowercase() + return if ( + lower == "localhost" || + lower.endsWith(".localhost") || + lower.endsWith(".local") || + lower.endsWith(".ts.net") + ) { + "http" + } else if (!lower.contains('.')) { + "http" + } else { + "https" + } + } + + /** + * The host-name pre-check of the plain-HTTP policy, shared with + * [HttpPolicyInterceptor]; the network layer additionally requires + * every resolved peer address to be private. + */ + fun allowsPlainHttp(host: String): Boolean = defaultScheme(host) == "http" + } +} + +sealed interface ServerAddressResult { + data class Valid(val address: ServerAddress) : ServerAddressResult + data class Invalid(val message: String) : ServerAddressResult +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/ServerProbe.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/ServerProbe.kt new file mode 100644 index 000000000..ad5b65c6c --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/ServerProbe.kt @@ -0,0 +1,171 @@ +package io.github.kahme247.ompweb.net + +import java.io.IOException +import java.util.concurrent.TimeUnit +import javax.net.ssl.SSLException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response + +/** What an unauthenticated `GET /api/sessions` tells us about the server. */ +enum class ProbeResult { + REACHABLE, + PASSWORD_REQUIRED, + MISCONFIGURED, + FORBIDDEN, + NOT_OMPWEB, + UNREACHABLE, + TLS_ERROR, +} + +/** The capability handshake answer for one server address. */ +sealed interface AppInfoProbe { + /** `/api/app-info` exists and its `api` version is one this app speaks. */ + data class Supported(val info: AppInfo) : AppInfoProbe + + /** The endpoint is missing or its `api` version is unknown: the server needs an update. */ + data class TooOld(val serverVersion: String?) : AppInfoProbe + + data object PasswordRequired : AppInfoProbe + data object Forbidden : AppInfoProbe + data object Misconfigured : AppInfoProbe + data object Unreachable : AppInfoProbe + data object TlsError : AppInfoProbe + data object NotOmpweb : AppInfoProbe +} + +/** + * Sends a single, short, unauthenticated request to `/api/sessions`. + * + * The request deliberately carries no `Origin` and no `Sec-Fetch-Site` header: + * ompweb's origin gate answers those with 403, and a native client is not a + * browser. A missing `.body` on 401 is treated as not-ompweb rather than a + * password prompt, so a random web server cannot imitate the sign-in flow. + * + * Redirects are not followed and the answer body is capped, so a server the + * user mistyped can neither move the request to another origin nor make the + * probe allocate an unbounded body. + */ +class ServerProbe( + private val client: OkHttpClient = defaultClient(), +) { + suspend fun probe(address: ServerAddress): ProbeResult = withContext(Dispatchers.IO) { + val request = Request.Builder() + .url(address.apiUrl("api/sessions")) + .header("Accept", "application/json") + .get() + .build() + + try { + client.newCall(request).execute().use { response -> + val body = readCappedBody(response) ?: return@withContext ProbeResult.NOT_OMPWEB + when (response.code) { + 200 -> if (isSessionList(body)) ProbeResult.REACHABLE else ProbeResult.NOT_OMPWEB + 401 -> if (hasPasswordRequiredCode(body)) { + ProbeResult.PASSWORD_REQUIRED + } else { + ProbeResult.NOT_OMPWEB + } + 503 -> ProbeResult.MISCONFIGURED + 403 -> ProbeResult.FORBIDDEN + else -> ProbeResult.NOT_OMPWEB + } + } + } catch (_: SSLException) { + ProbeResult.TLS_ERROR + } catch (_: IOException) { + ProbeResult.UNREACHABLE + } + } + + /** + * Reads `GET /api/app-info`, the handshake that tells the app whether the + * server is new enough to talk to. The auth gate runs before routing, so an + * unauthenticated server with a password answers 401 here and the real + * version check happens again after sign-in. + */ + suspend fun probeAppInfo(address: ServerAddress): AppInfoProbe = withContext(Dispatchers.IO) { + val request = Request.Builder() + .url(address.apiUrl("api/app-info")) + .header("Accept", "application/json") + .get() + .build() + + try { + client.newCall(request).execute().use { response -> + val body = readCappedBody(response) ?: return@withContext AppInfoProbe.NotOmpweb + when (response.code) { + 200 -> { + val info = try { + json.decodeFromString(AppInfo.serializer(), body) + } catch (_: Exception) { + null + } + when { + info == null -> AppInfoProbe.NotOmpweb + info.api != SUPPORTED_API -> AppInfoProbe.TooOld(info.version.ifBlank { null }) + else -> AppInfoProbe.Supported(info) + } + } + // An older ompweb has no such route; Next answers 404. + 404, 405 -> AppInfoProbe.TooOld(null) + 401 -> AppInfoProbe.PasswordRequired + 403 -> AppInfoProbe.Forbidden + 503 -> AppInfoProbe.Misconfigured + else -> AppInfoProbe.NotOmpweb + } + } + } catch (_: SSLException) { + AppInfoProbe.TlsError + } catch (_: IOException) { + AppInfoProbe.Unreachable + } + } + + companion object { + /** A probe answer is a small JSON object; anything larger is not ompweb. */ + const val MAX_BODY_BYTES = 64 * 1024 + + /** The `/api/app-info` version this app was written against. */ + const val SUPPORTED_API = 1 + + private val json = Json { ignoreUnknownKeys = true } + + private fun defaultClient(): OkHttpClient = HttpPolicyInterceptor.install( + OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .callTimeout(8, TimeUnit.SECONDS) + .retryOnConnectionFailure(false) + .followRedirects(false), + ).build() + + /** Read at most [MAX_BODY_BYTES]; `null` means the body exceeded the cap. */ + private fun readCappedBody(response: Response): String? { + if (response.body.contentLength() > MAX_BODY_BYTES) return null + val bytes = response.peekBody(MAX_BODY_BYTES.toLong() + 1).bytes() + return if (bytes.size > MAX_BODY_BYTES) null else bytes.toString(Charsets.UTF_8) + } + + private fun isSessionList(body: String): Boolean = try { + val root = json.parseToJsonElement(body) as? JsonObject + root?.get("sessions") is JsonArray + } catch (_: Exception) { + false + } + + private fun hasPasswordRequiredCode(body: String): Boolean = try { + val root = json.parseToJsonElement(body) as? JsonObject + root?.get("code")?.jsonPrimitive?.contentOrNull == "password_required" + } catch (_: Exception) { + false + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/SkillDiagnosticsApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/SkillDiagnosticsApi.kt new file mode 100644 index 000000000..809db1c26 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/SkillDiagnosticsApi.kt @@ -0,0 +1,22 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.SkillDiagnosticsSnapshot + +/** + * The two skill-resolution calls the startup notice and the Skills inspector + * make (web `lib/rpc-manager.ts` `get_skill_diagnostics` / + * `set_skill_startup_diagnostics`). + * + * Both answer with the effective report. `null` means the server has no + * report for this session (an omp without the RPC, or a payload the + * allowlisted parser rejected), so the UI shows "unavailable" instead of + * claiming a clean result. + */ +interface SkillDiagnosticsApi { + + /** `{type:"get_skill_diagnostics"}`. */ + suspend fun skillDiagnostics(sessionId: String): SkillDiagnosticsSnapshot? + + /** `{type:"set_skill_startup_diagnostics", enabled}`; answers with the effective report. */ + suspend fun setSkillStartupDiagnostics(sessionId: String, enabled: Boolean): SkillDiagnosticsSnapshot? +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/SkillsApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/SkillsApi.kt new file mode 100644 index 000000000..08379ad62 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/SkillsApi.kt @@ -0,0 +1,219 @@ +package io.github.kahme247.ompweb.net + +import java.net.URLEncoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody + +/** Where a skill came from (`SkillInfo.sourceInfo`). */ +data class SkillSourceInfo(val source: String?, val scope: String?) + +/** Install provenance of a skill (`SkillInfo.install`). */ +data class SkillInstallInfo( + val packageName: String, + val scope: String, + val source: String, + val skillsShUrl: String?, + val versionHash: String?, + val canCheckForUpdates: Boolean, +) + +/** + * One skill from `GET /api/skills`. [togglable] is false for plugin/registry + * copies whose `SKILL.md` is not user-owned (the PATCH would be refused). + */ +data class NativeSkillInfo( + val name: String, + val description: String, + val filePath: String, + val baseDir: String, + val disableModelInvocation: Boolean, + val sourceInfo: SkillSourceInfo, + val install: SkillInstallInfo?, + val togglable: Boolean, +) + +/** One skills.sh search hit. */ +data class SkillSearchResult(val packageName: String, val installs: String, val url: String) + +/** Result of checking one installed skill (`POST /api/skills/check`). */ +data class SkillUpdateResult( + val packageName: String, + val scope: String, + val state: String, + val currentVersion: String?, + val latestVersion: String?, + val message: String?, +) + +/** + * `/api/skills` plus its actions: list, toggle, search, install, check and + * update (the route family under `app/api/skills`). Failures are [ApiError]s; + * the server's message + * is the user-facing wording. + */ +interface SkillsApi { + + suspend fun list(cwd: String): List + + /** Returns the value the file now has (the server reports the truth). */ + suspend fun setModelInvocationDisabled(filePath: String, disabled: Boolean, cwd: String?): Boolean + + suspend fun search(query: String, limit: Int = 50): List + + /** `scope` is `global` or `project`; a project install needs [cwd]. */ + suspend fun install(packageName: String, scope: String, cwd: String?) + + suspend fun checkUpdates(cwd: String, packageName: String? = null, scope: String? = null): List + + suspend fun update(cwd: String, packageName: String, scope: String) +} + +/** Production [SkillsApi] over the app's authenticated client. */ +class HttpSkillsApi(private val api: ApiClient) : SkillsApi { + + private val json = Json { ignoreUnknownKeys = true } + + override suspend fun list(cwd: String): List { + val request = api.requestBuilder("$SKILLS_PATH?cwd=${encode(cwd)}").get().build() + val root = parseObject(send(request)) + return (root["skills"] as? JsonArray).orEmpty().mapNotNull(::parseSkill) + } + + override suspend fun setModelInvocationDisabled(filePath: String, disabled: Boolean, cwd: String?): Boolean { + val payload = buildJsonObject { + put("filePath", filePath) + put("disableModelInvocation", disabled) + if (cwd != null) put("cwd", cwd) + }.toString() + val request = api.requestBuilder(SKILLS_PATH).patch(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + val root = parseObject(send(request)) + return boolean(root["disableModelInvocation"]) ?: disabled + } + + override suspend fun search(query: String, limit: Int): List { + val payload = buildJsonObject { + put("query", query) + put("limit", limit) + }.toString() + val request = api.requestBuilder("$SKILLS_PATH/search").post(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + val root = parseObject(send(request)) + return (root["results"] as? JsonArray).orEmpty().mapNotNull { element -> + val entry = element as? JsonObject ?: return@mapNotNull null + val name = string(entry["package"]) ?: return@mapNotNull null + SkillSearchResult(name, string(entry["installs"]).orEmpty(), string(entry["url"]).orEmpty()) + } + } + + override suspend fun install(packageName: String, scope: String, cwd: String?) { + val payload = buildJsonObject { + put("package", packageName) + put("scope", scope) + if (cwd != null) put("cwd", cwd) + }.toString() + val request = api.requestBuilder("$SKILLS_PATH/install").post(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + send(request) + } + + override suspend fun checkUpdates(cwd: String, packageName: String?, scope: String?): List { + val payload = buildJsonObject { + put("cwd", cwd) + if (packageName != null) put("package", packageName) + if (scope != null) put("scope", scope) + }.toString() + val request = api.requestBuilder("$SKILLS_PATH/check").post(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + val root = parseObject(send(request)) + return (root["updates"] as? JsonArray).orEmpty().mapNotNull { element -> + val entry = element as? JsonObject ?: return@mapNotNull null + val name = string(entry["package"]) ?: return@mapNotNull null + SkillUpdateResult( + packageName = name, + scope = string(entry["scope"]) ?: "global", + state = string(entry["state"]) ?: "error", + currentVersion = string(entry["currentVersion"]), + latestVersion = string(entry["latestVersion"]), + message = string(entry["message"]), + ) + } + } + + override suspend fun update(cwd: String, packageName: String, scope: String) { + val payload = buildJsonObject { + put("cwd", cwd) + put("package", packageName) + put("scope", scope) + }.toString() + val request = api.requestBuilder("$SKILLS_PATH/update").post(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + send(request) + } + + private fun parseSkill(element: JsonElement): NativeSkillInfo? { + val entry = element as? JsonObject ?: return null + val name = string(entry["name"]) ?: return null + val source = entry["sourceInfo"] as? JsonObject + val install = (entry["install"] as? JsonObject)?.let { info -> + val packageName = string(info["package"]) ?: return@let null + SkillInstallInfo( + packageName = packageName, + scope = string(info["scope"]) ?: "global", + source = string(info["source"]).orEmpty(), + skillsShUrl = string(info["skillsShUrl"]), + versionHash = string(info["versionHash"]), + canCheckForUpdates = boolean(info["canCheckForUpdates"]) ?: false, + ) + } + return NativeSkillInfo( + name = name, + description = string(entry["description"]).orEmpty(), + filePath = string(entry["filePath"]).orEmpty(), + baseDir = string(entry["baseDir"]).orEmpty(), + disableModelInvocation = boolean(entry["disableModelInvocation"]) ?: false, + sourceInfo = SkillSourceInfo( + source = source?.let { string(it["source"]) }, + scope = source?.let { string(it["scope"]) }, + ), + install = install, + togglable = boolean(entry["togglable"]) ?: true, + ) + } + + private suspend fun send(request: okhttp3.Request): String { + val response = api.exchange(request) + val text = response.text() + if (response.status !in 200..299) { + throw api.errorFor(response.status, text, originOf(request)) + } + return text + } + + private fun parseObject(body: String): JsonObject = try { + json.parseToJsonElement(body).jsonObject + } catch (_: Exception) { + throw ApiError.Http(200, body.take(2048)) + } + + private fun string(element: JsonElement?): String? = + (element as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull + + private fun boolean(element: JsonElement?): Boolean? = + (element as? JsonPrimitive)?.takeUnless { it.isString }?.booleanOrNull + + private fun originOf(request: okhttp3.Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + private fun encode(value: String): String = URLEncoder.encode(value, "UTF-8") + + private companion object { + const val SKILLS_PATH = "api/skills" + val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/SttApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/SttApi.kt new file mode 100644 index 000000000..5ea6ae4d7 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/SttApi.kt @@ -0,0 +1,275 @@ +package io.github.kahme247.ompweb.net + +import io.github.kahme247.ompweb.domain.SttAfter +import io.github.kahme247.ompweb.domain.SttClaim +import io.github.kahme247.ompweb.domain.SttJob +import io.github.kahme247.ompweb.domain.SttJobStatus +import java.net.URLEncoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * The transcription routes of `/api/stt`, as an interface so the dictation + * view model runs in JVM tests without a server. + * + * The job protocol is `lib/stt-jobs.ts`: the upload keeps the audio and starts + * a server-side job, polls only carry the status, and the transcript is + * delivered by exactly one claim token. `null` from [job], [claim] and [audio] + * is a 404 (unknown/expired job); every other failure is an [ApiError]. + */ +interface SttApi { + + /** + * `POST /api/stt` (multipart `file, scope, owner, after`) → 202 `{jobId}`. + * 501/413/429 answer their message as [SttStartOutcome.Rejected]. + */ + suspend fun start( + scope: String?, + owner: String, + after: SttAfter?, + audio: ByteArray, + fileName: String, + mimeType: String, + ): SttStartOutcome + + /** `GET /api/stt?scope=` — live jobs of that composer, newest first. */ + suspend fun jobs(scope: String): List + + /** `GET /api/stt/{jobId}` — status only, never the transcript. */ + suspend fun job(jobId: String): SttJob? + + /** `DELETE ?claim=&owner=` — the one call that delivers the transcript. */ + suspend fun claim(jobId: String, claimToken: String, ownerToken: String): SttClaim? + + /** `DELETE` without a claim: discards the job and aborts its upstream request. */ + suspend fun discard(jobId: String) + + /** `POST /api/stt/{jobId}?owner=` — retry with the audio the server kept. */ + suspend fun retry(jobId: String, ownerToken: String): SttRetryOutcome + + /** `GET /api/stt/{jobId}/audio` — the kept recording, for playback. */ + suspend fun audio(jobId: String): SttAudio? +} + +/** `POST /api/stt` answer: a started job, or the server's refusal. */ +sealed interface SttStartOutcome { + data class Accepted(val jobId: String) : SttStartOutcome + + /** + * 501 unconfigured, 413 too large, 429 busy, 400 invalid — the server's + * message. [notConfigured] marks the 501 answer so `Auto` can use the + * phone's recognizer from then on. + */ + data class Rejected( + val message: String, + val notConfigured: Boolean = false, + ) : SttStartOutcome +} + +/** `POST /api/stt/{jobId}?owner=` answer (retry with the audio the server kept). */ +sealed interface SttRetryOutcome { + data class Accepted(val job: SttJob) : SttRetryOutcome + + /** 404: the job is gone (server restart, expiry, pruned tombstone). */ + data object Missing : SttRetryOutcome + + /** 429: too many transcriptions in progress. */ + data object Busy : SttRetryOutcome + + data class Rejected(val message: String, val notConfigured: Boolean = false) : SttRetryOutcome +} + +/** The recording the server kept for a live job, for playback in any composer. */ +class SttAudio(val bytes: ByteArray, val mimeType: String) + +/** + * [SttApi] over the app's single authenticated client. All HTTP goes through + * `ApiClient` (origin binding, cookie jar, policy interceptor, typed errors); + * this class only builds the multipart upload and the binary download and + * parses the small job payloads. + */ +class HttpSttApi(private val api: ApiClient) : SttApi { + + override suspend fun start( + scope: String?, + owner: String, + after: SttAfter?, + audio: ByteArray, + fileName: String, + mimeType: String, + ): SttStartOutcome { + val multipart = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", fileName, audio.toRequestBody(mimeType.toMediaType())) + .addFormDataPart("owner", owner) + if (scope != null) multipart.addFormDataPart("scope", scope) + after?.wire?.let { multipart.addFormDataPart("after", it) } + val request = api.requestBuilder(STT_PATH).post(multipart.build()).build() + val response = api.exchange(request) + val text = response.text() + if (response.status in 200..299) { + val jobId = parseObject(text)?.get("jobId")?.jsonPrimitive?.contentOrNull + if (jobId.isNullOrBlank()) { + return SttStartOutcome.Rejected("Transcription failed (HTTP ${response.status})") + } + return SttStartOutcome.Accepted(jobId) + } + val message = errorMessage(text) + return when (response.status) { + 429 -> SttStartOutcome.Rejected(message ?: BUSY) + 501 -> SttStartOutcome.Rejected(message ?: NOT_CONFIGURED, notConfigured = true) + 413 -> SttStartOutcome.Rejected(message ?: TOO_LARGE) + 400 -> SttStartOutcome.Rejected(message ?: REJECTED) + else -> throw api.errorFor(response.status, text, originOf(request)) + } + } + + override suspend fun jobs(scope: String): List { + val request = api.requestBuilder("$STT_PATH?scope=${encodeQuery(scope)}").get().build() + val response = api.exchange(request) + if (response.status !in 200..299) throw api.errorFor(response.status, response.text(), originOf(request)) + val text = response.text() + val root = parseObject(text) ?: throw ApiError.Http(200, text.take(2048)) + return root["jobs"]?.jsonArray?.mapNotNull { element -> + parseJob(element as? JsonObject ?: return@mapNotNull null) + }.orEmpty() + } + + override suspend fun job(jobId: String): SttJob? = + readJob(api.requestBuilder("$STT_PATH/${encodePathSegment(jobId)}").get().build()) + + override suspend fun claim(jobId: String, claimToken: String, ownerToken: String): SttClaim? { + val path = "$STT_PATH/${encodePathSegment(jobId)}" + + "?claim=${encodeQuery(claimToken)}&owner=${encodeQuery(ownerToken)}" + val request = api.requestBuilder(path).delete().build() + val response = api.exchange(request) + if (response.status == 404) return null + val text = response.text() + if (response.status !in 200..299) throw api.errorFor(response.status, text, originOf(request)) + val root = parseObject(text) ?: throw ApiError.Http(200, text.take(2048)) + val id = root.string("id") ?: jobId + val status = statusOf(root.string("status")) ?: throw ApiError.Http(200, text.take(2048)) + return SttClaim( + id = id, + status = status, + error = root.string("error"), + text = root.string("text"), + after = afterOf(root.string("after")), + ) + } + + override suspend fun discard(jobId: String) { + val request = api.requestBuilder("$STT_PATH/${encodePathSegment(jobId)}").delete().build() + val response = api.exchange(request) + // A job someone else already took is a successful discard for us. + if (response.status == 404) return + if (response.status !in 200..299) throw api.errorFor(response.status, response.text(), originOf(request)) + } + + override suspend fun retry(jobId: String, ownerToken: String): SttRetryOutcome { + val path = "$STT_PATH/${encodePathSegment(jobId)}?owner=${encodeQuery(ownerToken)}" + val request = api.requestBuilder(path).post(EMPTY_BODY).build() + val response = api.exchange(request) + val text = response.text() + return when { + response.status == 404 -> SttRetryOutcome.Missing + response.status == 429 -> SttRetryOutcome.Busy + response.status == 501 -> SttRetryOutcome.Rejected( + errorMessage(text) ?: NOT_CONFIGURED, + notConfigured = true, + ) + response.status in 200..299 -> { + val job = parseJob(parseObject(text)) + if (job == null) SttRetryOutcome.Rejected("Transcription failed (HTTP ${response.status})") + else SttRetryOutcome.Accepted(job) + } + else -> throw api.errorFor(response.status, text, originOf(request)) + } + } + + override suspend fun audio(jobId: String): SttAudio? { + val request = api.requestBuilder("$STT_PATH/${encodePathSegment(jobId)}/audio", accept = "audio/*, */*") + .get() + .build() + // The server keeps up to 25 MB of audio per job (`MAX_STT_AUDIO_BYTES`). + val response = api.exchange(request, limit = MAX_AUDIO_BYTES) + if (response.status == 404) return null + if (response.status !in 200..299) throw api.errorFor(response.status, response.text(), originOf(request)) + val mimeType = response.contentType?.substringBefore(';')?.trim().orEmpty() + if (!mimeType.startsWith("audio/")) return null + return SttAudio(response.body, mimeType) + } + + private suspend fun readJob(request: okhttp3.Request): SttJob? { + val response = api.exchange(request) + if (response.status == 404) return null + val text = response.text() + if (response.status !in 200..299) throw api.errorFor(response.status, text, originOf(request)) + val job = parseJob(parseObject(text)) ?: throw ApiError.Http(200, text.take(2048)) + return job + } + + private fun parseJob(root: JsonObject?): SttJob? { + if (root == null) return null + val id = root.string("id") ?: return null + val status = statusOf(root.string("status")) ?: return null + return SttJob(id = id, status = status, error = root.string("error")) + } + + private fun JsonObject.string(key: String): String? = this[key]?.jsonPrimitive?.contentOrNull + + private fun parseObject(text: String): JsonObject? = try { + json.parseToJsonElement(text).jsonObject + } catch (_: Exception) { + null + } + + private fun errorMessage(text: String): String? = parseObject(text)?.string("error") + + private fun originOf(request: okhttp3.Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + private fun statusOf(value: String?): SttJobStatus? = when (value) { + SttJobStatus.Pending.wire -> SttJobStatus.Pending + SttJobStatus.Done.wire -> SttJobStatus.Done + SttJobStatus.Error.wire -> SttJobStatus.Error + SttJobStatus.Gone.wire -> SttJobStatus.Gone + else -> null + } + + private fun afterOf(value: String?): SttAfter? = when (value) { + SttAfter.Send.wire -> SttAfter.Send + SttAfter.Steer.wire -> SttAfter.Steer + SttAfter.FollowUp.wire -> SttAfter.FollowUp + else -> null + } + + private fun encodePathSegment(value: String): String = + URLEncoder.encode(value, "UTF-8").replace("+", "%20") + + private fun encodeQuery(value: String): String = URLEncoder.encode(value, "UTF-8") + + private companion object { + const val STT_PATH = "api/stt" + + /** `MAX_STT_AUDIO_BYTES` in `lib/stt.ts`. */ + const val MAX_AUDIO_BYTES = 25L * 1024 * 1024 + + val EMPTY_BODY = ByteArray(0).toRequestBody("application/json".toMediaType()) + + const val NOT_CONFIGURED = "Speech recognition is not configured on the server" + const val TOO_LARGE = "Recording too large (max 25 MB)" + const val BUSY = "Too many transcriptions in progress" + const val REJECTED = "The recording was rejected" + + val json = Json { ignoreUnknownKeys = true } + } +} + diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/SystemApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/SystemApi.kt new file mode 100644 index 000000000..8992564b5 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/SystemApi.kt @@ -0,0 +1,693 @@ +package io.github.kahme247.ompweb.net + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * `/api/app-info`, `/api/app-update`, `/api/omp-update`, `/api/web-settings`, + * `/api/mobile-access`, `/api/windows-service`, `/api/usage` and + * `/api/provider-usage`: the server-side surface of Settings → System & + * Updates and the two usage views. + * + * Interfaces so the settings view model runs in JVM tests; the HTTP + * implementation reuses the app's one authenticated client. + */ +interface SystemApi { + + suspend fun appInfo(): ServerAppInfo + + suspend fun appUpdate(force: Boolean = false): AppUpdateInfo + + /** `GET /api/app-update/notes`; `null` when the server has no notes (204). */ + suspend fun appUpdateNotes(): ReleaseNotes? + + /** `POST /api/app-update {action:"prepare"}`, 202 `{attemptId, targetVersion}`. */ + suspend fun appUpdatePrepare(): SelfUpdatePrepare + + /** `POST /api/app-update {action:"commit", attemptId}` (replaying an attempt is accepted). */ + suspend fun appUpdateCommit(attemptId: String) + + /** `POST /api/app-update {action:"status"}`; `null` until an attempt exists. */ + suspend fun appUpdateStatus(): SelfUpdateStatus? + + suspend fun acknowledgeAppUpdate(attemptId: String) + + suspend fun ompUpdateCheck(force: Boolean = false): OmpUpdateInfo + + /** `POST /api/omp-update {action:"update"}`, 202 `{attemptId, targetVersion}`. */ + suspend fun ompUpdatePrepare(): SelfUpdatePrepare + + suspend fun ompUpdateCommit(attemptId: String) + + suspend fun ompUpdateStatus(): SelfUpdateStatus? + + suspend fun acknowledgeOmpUpdate(attemptId: String) + + /** `POST {action:"restart"}` — restarts every live RPC session. */ + suspend fun restartSessions(): RestartSessionsResult + + suspend fun webSettings(): WebServerSettings + + /** + * `PUT /api/web-settings`. The route only updates keys that are present, + * so a null [agentEnv] leaves the stored variables untouched. + */ + suspend fun saveWebSettings( + autoResumeSessions: Boolean, + agentEnv: String? = null, + ): WebServerSettings + + suspend fun mobileAccess(): MobileAccessInfo + + suspend fun windowsService(): WindowsServiceInfo + + suspend fun windowsAction(action: WindowsServiceAction, cleanConfig: Boolean = false): WindowsServiceActionInfo + + suspend fun usage( + range: UsageTimeRange, + granularity: UsageGranularity, + refresh: Boolean = false, + ): UsageReport + + suspend fun providerUsage(refresh: Boolean = false): ProviderUsageSnapshot +} + +data class ServerAppInfo( + val name: String, + val version: String, + val api: Int, +) + +/** `NpmUpdateStatus` + the self-update support flag. */ +data class AppUpdateInfo( + val currentVersion: String?, + val availableVersion: String?, + val updateAvailable: Boolean, + val updateCommand: String, + val updatesDisabled: Boolean, + /** The server can install this package itself (`selfUpdateSupported`). */ + val selfUpdateSupported: Boolean = false, +) + +/** + * `SelfUpdateStatus` (`lib/self-update.ts`): the server-side install attempt. + * `state` is `prepared|running|succeeded|failed`; `stage` refines a running + * attempt. Only the fields the card and poll use are kept. + */ +data class SelfUpdateStatus( + val attemptId: String, + val state: String, + val stage: String? = null, + val targetVersion: String = "", + val error: String? = null, +) + +/** 202 answer of `prepare`/`update`: the attempt the client must commit. */ +data class SelfUpdatePrepare(val attemptId: String, val targetVersion: String) + +/** `GET /api/app-update/notes` (`GitHubReleaseNotes`); only github.com links are sent. */ +data class ReleaseNotes( + val version: String, + val body: String, + val htmlUrl: String, +) + +/** `OmpUpdateStatus` from `POST /api/omp-update {action:"check"}`. */ +data class OmpUpdateInfo( + val currentVersion: String?, + val availableVersion: String?, + val updateAvailable: Boolean, + val updateCommand: String, + val updatesDisabled: Boolean, +) + +data class RestartSessionsResult(val sessionsRestarted: Int) + +data class WebServerSettings( + val autoResumeSessions: Boolean, + val agentEnv: Map, +) + +data class MobileAccessBinding(val hostname: String?, val exposure: String, val port: Int) + +data class MobileAccessAddress(val url: String, val label: String) + +data class MobileAccessMethod(val id: String, val commands: List) + +data class MobileAccessInfo( + val binding: MobileAccessBinding, + val passwordConfigured: Boolean, + val passwordProblem: Boolean, + val addresses: List, + val methods: List, +) + +data class WindowsServiceInfo( + val isWindows: Boolean, + val isInstalled: Boolean, + val autostart: Boolean, + val isRunning: Boolean, + val port: Int?, + val hostname: String?, + val mode: String?, + val desktopShortcutExists: Boolean, + val serviceUrl: String?, + val version: String?, +) + +enum class WindowsServiceAction(val wire: String) { + INSTALL("install"), + UNINSTALL("uninstall"), + TOGGLE_AUTOSTART("toggle-autostart"), + START("start"), + STOP("stop"), + RESTART("restart"), +} + +data class WindowsServiceActionInfo( + val success: Boolean, + val message: String?, + val status: WindowsServiceInfo?, +) + +// ------------------------------------------------------------------ usage + +enum class UsageTimeRange(val wire: String) { + TODAY("today"), + SEVEN_DAYS("7d"), + THIRTY_DAYS("30d"), + NINETY_DAYS("90d"), + MONTH("month"), + ALL("all"), +} + +enum class UsageGranularity(val wire: String) { + DAILY("daily"), + MONTHLY("monthly"), + PROJECTS("projects"), +} + +data class UsageCostQuality( + val providerReported: Double, + val modelPriced: Double, + val unpriced: Double, +) + +data class UsageSummary( + val totalCost: Double, + val totalTokens: Long, + val inputTokens: Long, + val outputTokens: Long, + val reasoningTokens: Long, + val cacheReadTokens: Long, + val cacheWriteTokens: Long, + val cacheSavings: Double, + val activeDays: Int, + val tokensPerActiveDay: Long, + val cachePercentage: Double, + val costQuality: UsageCostQuality, +) + +data class ProviderUsageSummary( + val provider: String, + val name: String, + val cost: Double, + val tokens: Long, + val share: Double, + val color: String, +) + +data class ModelUsageSummary( + val model: String, + val provider: String, + val cost: Double, + val tokens: Long, + val share: Double, + val recordsCount: Int, +) + +data class UsageTimeSeriesPoint( + val date: String, + val label: String, + val totalCost: Double, + val totalTokens: Long, +) + +data class UsageDaySummary( + val date: String, + val label: String, + val cost: Double, + val tokens: Long, + val share: Double, +) + +data class UsageProjectSummary( + val project: String, + val projectName: String, + val cost: Double, + val tokens: Long, + val share: Double, + val sessionsCount: Int, +) + +data class UsageScanInfo( + val transcriptsScanned: Int, + val transcriptsOutsideWindow: Int, + val usageRecordsCount: Int, + val durationSeconds: Double, +) + +data class UsageReport( + val timeRange: String, + val granularity: String, + val summary: UsageSummary, + val providers: List, + val timeSeries: List, + val modelBreakdown: List, + val dayBreakdown: List, + val projectBreakdown: List, + val scanInfo: UsageScanInfo, +) + +data class ProviderUsageWindow( + val percent: Double, + val resetMinutes: Int? = null, + val resetHours: Int? = null, +) + +data class ProviderUsageReport( + val provider: String, + val accountLabel: String? = null, + val accountIndex: Int? = null, + val plan: String? = null, + val modelId: String? = null, + val tier: String? = null, + val noLimits: Boolean = false, + val fiveHour: ProviderUsageWindow? = null, + val sevenDay: ProviderUsageWindow? = null, + val monthly: ProviderUsageWindow? = null, +) + +data class ProviderUsageSnapshot( + val generatedAt: Long?, + val reports: List, +) + +/** Production [SystemApi] over the app's authenticated client. */ +fun ApiClient.systemApi(): SystemApi = HttpSystemApi(this) + +private class HttpSystemApi(private val api: ApiClient) : SystemApi { + + override suspend fun appInfo(): ServerAppInfo { + val root = getObject("api/app-info") + return ServerAppInfo( + name = root.string("name") ?: "ompweb", + version = root.string("version") ?: "unknown", + api = root.int("api") ?: 0, + ) + } + + override suspend fun appUpdate(force: Boolean): AppUpdateInfo { + val root = getObject("api/app-update" + if (force) "?force=1" else "") + return AppUpdateInfo( + currentVersion = root.string("currentVersion"), + availableVersion = root.string("availableVersion"), + updateAvailable = root.boolean("updateAvailable") ?: false, + updateCommand = root.string("updateCommand") ?: "ompweb update", + updatesDisabled = root.boolean("updatesDisabled") ?: false, + selfUpdateSupported = root.boolean("selfUpdateSupported") ?: false, + ) + } + + override suspend fun appUpdateNotes(): ReleaseNotes? { + val text = send(api.requestBuilder("api/app-update/notes").get().build()) + if (text.isBlank()) return null // 204: no notes for the pending version. + val root = parseObjectOrNull(text) ?: return null + val version = root.string("version") ?: return null + val body = root.string("body") ?: return null + val url = root.string("htmlUrl") ?: return null + return ReleaseNotes(version = version, body = body, htmlUrl = url) + } + + override suspend fun appUpdatePrepare(): SelfUpdatePrepare = + parsePrepare(postObject("api/app-update", action("prepare"))) + + override suspend fun appUpdateCommit(attemptId: String) { + postObject("api/app-update", commitPayload(attemptId)) + } + + override suspend fun appUpdateStatus(): SelfUpdateStatus? = + (postElementOrNull("api/app-update", action("status")) as? JsonObject)?.let(::parseSelfUpdate) + + override suspend fun acknowledgeAppUpdate(attemptId: String) { + postObject("api/app-update", acknowledgePayload(attemptId)) + } + + override suspend fun ompUpdatePrepare(): SelfUpdatePrepare = + parsePrepare(postObject("api/omp-update", action("update"))) + + override suspend fun ompUpdateCommit(attemptId: String) { + postObject("api/omp-update", commitPayload(attemptId)) + } + + override suspend fun ompUpdateStatus(): SelfUpdateStatus? = + (postElementOrNull("api/omp-update", action("status")) as? JsonObject)?.let(::parseSelfUpdate) + + override suspend fun acknowledgeOmpUpdate(attemptId: String) { + postObject("api/omp-update", acknowledgePayload(attemptId)) + } + + override suspend fun ompUpdateCheck(force: Boolean): OmpUpdateInfo { + val payload = buildJsonObject { + put("action", "check") + if (force) put("force", true) + }.toString() + val root = postObject("api/omp-update", payload) + return OmpUpdateInfo( + currentVersion = root.string("currentVersion"), + availableVersion = root.string("availableVersion"), + updateAvailable = root.boolean("updateAvailable") ?: false, + updateCommand = root.string("updateCommand") ?: "omp update", + updatesDisabled = root.boolean("updatesDisabled") ?: false, + ) + } + + override suspend fun restartSessions(): RestartSessionsResult { + val root = postObject("api/omp-update", buildJsonObject { put("action", "restart") }.toString()) + return RestartSessionsResult(root.int("sessionsRestarted") ?: 0) + } + + override suspend fun webSettings(): WebServerSettings = + parseWebSettings(getObject("api/web-settings")) + + override suspend fun saveWebSettings( + autoResumeSessions: Boolean, + agentEnv: String?, + ): WebServerSettings { + val payload = buildJsonObject { + put("autoResumeSessions", autoResumeSessions) + if (agentEnv != null) put("agentEnv", agentEnv) + }.toString() + val request = api.requestBuilder("api/web-settings") + .put(payload.toRequestBody(JSON_MEDIA_TYPE)) + .build() + return parseWebSettings(parseObject(send(request))) + } + + override suspend fun mobileAccess(): MobileAccessInfo { + val root = getObject("api/mobile-access") + val binding = root["binding"] as? JsonObject ?: JsonObject(emptyMap()) + val password = root["password"] as? JsonObject ?: JsonObject(emptyMap()) + return MobileAccessInfo( + binding = MobileAccessBinding( + hostname = binding.string("hostname"), + exposure = binding.string("exposure") ?: "unknown", + port = binding.int("port") ?: 0, + ), + passwordConfigured = password.boolean("configured") ?: false, + passwordProblem = password.boolean("problem") ?: false, + addresses = root.array("addresses").mapNotNull { element -> + val entry = element as? JsonObject ?: return@mapNotNull null + val url = entry.string("url") ?: return@mapNotNull null + MobileAccessAddress(url, entry.string("label") ?: "lan") + }, + methods = root.array("methods").mapNotNull { element -> + val entry = element as? JsonObject ?: return@mapNotNull null + val id = entry.string("id") ?: return@mapNotNull null + MobileAccessMethod(id, entry.array("commands").mapNotNull { it.stringValue() }) + }, + ) + } + + override suspend fun windowsService(): WindowsServiceInfo = parseWindows(getObject("api/windows-service")) + + override suspend fun windowsAction( + action: WindowsServiceAction, + cleanConfig: Boolean, + ): WindowsServiceActionInfo { + val payload = buildJsonObject { + put("action", action.wire) + if (action == WindowsServiceAction.UNINSTALL && cleanConfig) put("cleanConfig", true) + }.toString() + val root = postObject("api/windows-service", payload) + val status = root["status"] as? JsonObject + return WindowsServiceActionInfo( + success = root.boolean("success") ?: false, + message = root.string("message"), + status = status?.let(::parseWindows), + ) + } + + override suspend fun usage( + range: UsageTimeRange, + granularity: UsageGranularity, + refresh: Boolean, + ): UsageReport { + val query = buildString { + append("api/usage?range=").append(range.wire) + append("&granularity=").append(granularity.wire) + if (refresh) append("&refresh=true") + } + return parseUsage(getObject(query)) + } + + override suspend fun providerUsage(refresh: Boolean): ProviderUsageSnapshot { + val root = getObject("api/provider-usage" + if (refresh) "?refresh=true" else "") + return ProviderUsageSnapshot( + generatedAt = root.long("generatedAt"), + reports = root.array("reports").mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + ProviderUsageReport( + provider = item.string("provider") ?: return@mapNotNull null, + accountLabel = item.string("accountLabel"), + accountIndex = item.int("accountIndex"), + plan = item.string("plan"), + modelId = item.string("modelId"), + tier = item.string("tier"), + noLimits = item.boolean("noLimits") ?: false, + fiveHour = window(item["fiveHour"]), + sevenDay = window(item["sevenDay"]), + monthly = window(item["monthly"]), + ) + }, + ) + } + + // ------------------------------------------------------------ transport + + private suspend fun getObject(path: String): JsonObject = + parseObject(send(api.requestBuilder(path).get().build())) + + private suspend fun postObject(path: String, payload: String): JsonObject { + val request = api.requestBuilder(path) + .post(payload.toRequestBody(JSON_MEDIA_TYPE)) + .build() + return parseObject(send(request)) + } + + private suspend fun send(request: Request): String { + val response = api.exchange(request) + val text = response.text() + if (response.status in 200..299) return text + throw when (response.status) { + 401, 403 -> api.errorFor( + response.status, + text, + ServerAddress(request.url.scheme, request.url.host, request.url.port), + ) + else -> { + val body = parseObjectOrNull(text) + ApiError.ServerResponse( + status = response.status, + code = body?.string("code"), + serverMessage = body?.string("error"), + ) + } + } + } + + private fun parseWebSettings(root: JsonObject): WebServerSettings = WebServerSettings( + autoResumeSessions = root.boolean("autoResumeSessions") ?: false, + agentEnv = (root["agentEnv"] as? JsonObject) + ?.mapNotNull { (key, value) -> (value as? JsonPrimitive)?.contentOrNull?.let { key to it } } + ?.toMap() + ?: emptyMap(), + ) + + private fun parseWindows(root: JsonObject): WindowsServiceInfo = WindowsServiceInfo( + isWindows = root.boolean("isWindows") ?: false, + isInstalled = root.boolean("isInstalled") ?: false, + autostart = root.boolean("autostart") ?: false, + isRunning = root.boolean("isRunning") ?: false, + port = root.int("port"), + hostname = root.string("hostname"), + mode = root.string("mode"), + desktopShortcutExists = root.boolean("desktopShortcutExists") ?: false, + serviceUrl = root.string("serviceUrl"), + version = root.string("version"), + ) + + private fun parseUsage(root: JsonObject): UsageReport { + val summary = root["summary"] as? JsonObject ?: JsonObject(emptyMap()) + val quality = summary["costQuality"] as? JsonObject ?: JsonObject(emptyMap()) + val scan = root["scanInfo"] as? JsonObject ?: JsonObject(emptyMap()) + return UsageReport( + timeRange = root.string("timeRange") ?: "", + granularity = root.string("granularity") ?: "", + summary = UsageSummary( + totalCost = summary.double("totalCost") ?: 0.0, + totalTokens = summary.long("totalTokens") ?: 0, + inputTokens = summary.long("inputTokens") ?: 0, + outputTokens = summary.long("outputTokens") ?: 0, + reasoningTokens = summary.long("reasoningTokens") ?: 0, + cacheReadTokens = summary.long("cacheReadTokens") ?: 0, + cacheWriteTokens = summary.long("cacheWriteTokens") ?: 0, + cacheSavings = summary.double("cacheSavings") ?: 0.0, + activeDays = summary.int("activeDays") ?: 0, + tokensPerActiveDay = summary.long("tokensPerActiveDay") ?: 0, + cachePercentage = summary.double("cachePercentage") ?: 0.0, + costQuality = UsageCostQuality( + providerReported = quality.double("providerReported") ?: 0.0, + modelPriced = quality.double("modelPriced") ?: 0.0, + unpriced = quality.double("unpriced") ?: 0.0, + ), + ), + providers = root.array("providers").mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + ProviderUsageSummary( + provider = item.string("provider") ?: return@mapNotNull null, + name = item.string("name") ?: item.string("provider") ?: return@mapNotNull null, + cost = item.double("cost") ?: 0.0, + tokens = item.long("tokens") ?: 0, + share = item.double("share") ?: 0.0, + color = item.string("color") ?: "", + ) + }, + timeSeries = root.array("timeSeries").mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + UsageTimeSeriesPoint( + date = item.string("date") ?: "", + label = item.string("label") ?: item.string("date") ?: "", + totalCost = item.double("totalCost") ?: 0.0, + totalTokens = item.long("totalTokens") ?: 0, + ) + }, + modelBreakdown = root.array("modelBreakdown").mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + ModelUsageSummary( + model = item.string("model") ?: return@mapNotNull null, + provider = item.string("provider") ?: "", + cost = item.double("cost") ?: 0.0, + tokens = item.long("tokens") ?: 0, + share = item.double("share") ?: 0.0, + recordsCount = item.int("recordsCount") ?: 0, + ) + }, + dayBreakdown = root.array("dayBreakdown").mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + UsageDaySummary( + date = item.string("date") ?: "", + label = item.string("label") ?: item.string("date") ?: "", + cost = item.double("cost") ?: 0.0, + tokens = item.long("tokens") ?: 0, + share = item.double("share") ?: 0.0, + ) + }, + projectBreakdown = root.array("projectBreakdown").mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + UsageProjectSummary( + project = item.string("project") ?: "", + projectName = item.string("projectName") ?: item.string("project") ?: "", + cost = item.double("cost") ?: 0.0, + tokens = item.long("tokens") ?: 0, + share = item.double("share") ?: 0.0, + sessionsCount = item.int("sessionsCount") ?: 0, + ) + }, + scanInfo = UsageScanInfo( + transcriptsScanned = scan.int("transcriptsScanned") ?: 0, + transcriptsOutsideWindow = scan.int("transcriptsOutsideWindow") ?: 0, + usageRecordsCount = scan.int("usageRecordsCount") ?: 0, + durationSeconds = scan.double("durationSeconds") ?: 0.0, + ), + ) + } + + private fun parseSelfUpdate(root: JsonObject): SelfUpdateStatus = SelfUpdateStatus( + attemptId = root.string("attemptId") ?: "", + state = root.string("state") ?: "prepared", + stage = root.string("stage"), + targetVersion = root.string("targetVersion") ?: "", + error = root.string("error"), + ) + + private fun parsePrepare(root: JsonObject): SelfUpdatePrepare = SelfUpdatePrepare( + attemptId = root.string("attemptId") ?: throw ApiError.Http(202, root.toString()), + targetVersion = root.string("targetVersion") ?: "", + ) + + private fun action(name: String): String = buildJsonObject { put("action", name) }.toString() + + private fun commitPayload(attemptId: String): String = buildJsonObject { + put("action", "commit") + put("attemptId", attemptId) + }.toString() + + private fun acknowledgePayload(attemptId: String): String = buildJsonObject { + put("action", "acknowledge") + put("attemptId", attemptId) + }.toString() + + /** `status` answers JSON `null` while no attempt exists. */ + private suspend fun postElementOrNull(path: String, payload: String): JsonElement? { + val request = api.requestBuilder(path) + .post(payload.toRequestBody(JSON_MEDIA_TYPE)) + .build() + return runCatching { Json.parseToJsonElement(send(request)) }.getOrNull() + } + + private fun window(element: JsonElement?): ProviderUsageWindow? { + val item = element as? JsonObject ?: return null + return ProviderUsageWindow( + percent = item.double("percent") ?: return null, + resetMinutes = item.int("resetMinutes"), + resetHours = item.int("resetHours"), + ) + } + + private fun parseObject(body: String): JsonObject = + parseObjectOrNull(body) ?: throw ApiError.Http(200, body.take(2048)) + + private fun parseObjectOrNull(body: String): JsonObject? = + runCatching { Json.parseToJsonElement(body) as? JsonObject }.getOrNull() + + private companion object { + val JSON_MEDIA_TYPE = "application/json".toMediaType() + } +} + +// ------------------------------------------------------------ JSON helpers + +private fun JsonObject.string(name: String): String? = (this[name] as? JsonPrimitive)?.contentOrNull +private fun JsonObject.boolean(name: String): Boolean? = (this[name] as? JsonPrimitive)?.booleanOrNull +private fun JsonObject.int(name: String): Int? = (this[name] as? JsonPrimitive)?.intOrNull +private fun JsonObject.long(name: String): Long? = + (this[name] as? JsonPrimitive)?.let { it.longOrNull ?: it.doubleOrNull?.toLong() } + +private fun JsonObject.double(name: String): Double? = (this[name] as? JsonPrimitive)?.doubleOrNull +private fun JsonObject.array(name: String): JsonArray = this[name] as? JsonArray ?: JsonArray(emptyList()) +private fun JsonElement.stringValue(): String? = (this as? JsonPrimitive)?.contentOrNull +private val JsonPrimitive.longOrNull: Long? + get() = contentOrNull?.toLongOrNull() diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/net/WorktreesApi.kt b/android/app/src/main/java/io/github/kahme247/ompweb/net/WorktreesApi.kt new file mode 100644 index 000000000..4e7139825 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/net/WorktreesApi.kt @@ -0,0 +1,97 @@ +package io.github.kahme247.ompweb.net + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * `/api/worktrees` (`app/api/worktrees/route.ts`): list, create, remove the + * worktrees of one repository, as an interface so the Worktrees view model + * runs in JVM tests without a server. + * + * Create and remove carry the project root (the server resolves worktrees and + * subdirectories itself) and report the route's stable `code`s, which the pane + * turns into "ask before force" (409 `worktree_dirty`) or an inline error. + */ +interface WorktreesApi { + + /** `GET ?cwd=` — the repo's worktrees plus whether the cwd is a Git top level. */ + suspend fun list(cwd: String): WorktreeList + + /** `POST {cwd, branch}` — the new worktree (existing branch reused, else created). */ + suspend fun create(cwd: String, branch: String): WorktreeEntry + + /** + * `DELETE {cwd, path, force?}`. A dirty worktree answers 409 + * `worktree_dirty` as [`ApiError.ServerResponse`]; the caller retries with + * `force = true` after the user confirmed. + */ + suspend fun remove(cwd: String, path: String, force: Boolean) +} + +/** Production [WorktreesApi] over the app's single authenticated client. */ +fun ApiClient.worktreesApi(): WorktreesApi = HttpWorktreesApi(this) + +private class HttpWorktreesApi(private val api: ApiClient) : WorktreesApi { + + override suspend fun list(cwd: String): WorktreeList = api.worktrees(cwd) + + override suspend fun create(cwd: String, branch: String): WorktreeEntry { + val payload = buildJsonObject { + put("cwd", cwd) + put("branch", branch) + }.toString() + val request = api.requestBuilder(PATH).post(payload.toRequestBody(JSON_MEDIA_TYPE)).build() + val body = send(request) + val created = parseObject(body) + val path = created?.get("path")?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() } + ?: throw ApiError.Http(200, body.take(2048)) + return WorktreeEntry( + path = path, + branch = created["branch"]?.jsonPrimitive?.contentOrNull ?: branch, + isMain = false, + ) + } + + override suspend fun remove(cwd: String, path: String, force: Boolean) { + val payload = buildJsonObject { + put("cwd", cwd) + put("path", path) + put("force", force) + }.toString() + send(api.requestBuilder(PATH).delete(payload.toRequestBody(JSON_MEDIA_TYPE)).build()) + } + + private suspend fun send(request: Request): String { + val response = api.exchange(request) + val text = response.text() + if (response.status in 200..299) return text + // 401/403 keep their global meaning (sign out / not allowed). + if (response.status == 401 || response.status == 403) { + throw api.errorFor(response.status, text, originOf(request)) + } + val error = parseObject(text) + throw ApiError.ServerResponse( + status = response.status, + code = error?.get("code")?.jsonPrimitive?.contentOrNull, + serverMessage = error?.get("error")?.jsonPrimitive?.contentOrNull, + ) + } + + private fun parseObject(body: String): JsonObject? = + runCatching { Json.parseToJsonElement(body) as? JsonObject }.getOrNull() + + private fun originOf(request: Request): ServerAddress = + ServerAddress(request.url.scheme, request.url.host, request.url.port) + + private companion object { + const val PATH = "api/worktrees" + val JSON_MEDIA_TYPE = "application/json".toMediaType() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/AndroidNotifier.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/AndroidNotifier.kt new file mode 100644 index 000000000..5abb5237a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/AndroidNotifier.kt @@ -0,0 +1,737 @@ +package io.github.kahme247.ompweb.notify + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.service.notification.StatusBarNotification +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput +import androidx.core.content.ContextCompat +import io.github.kahme247.ompweb.MainActivity +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.NOTIFICATION_TITLE_MAX +import io.github.kahme247.ompweb.domain.NotifyChannel +import io.github.kahme247.ompweb.domain.NotifyEvent +import io.github.kahme247.ompweb.domain.NotifyProject +import io.github.kahme247.ompweb.domain.RenderedNotification +import io.github.kahme247.ompweb.domain.ReplyTarget +import io.github.kahme247.ompweb.domain.androidChannelId +import io.github.kahme247.ompweb.domain.clipNotificationText +import io.github.kahme247.ompweb.domain.notificationActions +import io.github.kahme247.ompweb.domain.notifyChannelFor +import io.github.kahme247.ompweb.domain.quietId +import io.github.kahme247.ompweb.domain.renderNotification +import io.github.kahme247.ompweb.domain.renderTestNotification +import io.github.kahme247.ompweb.domain.supportsVibration +import io.github.kahme247.ompweb.domain.vibrationPattern +import io.github.kahme247.ompweb.domain.vibratingId +import java.util.concurrent.atomic.AtomicInteger + +/** + * Every system notification of the app, in one place: the live event + * renderer, the Phase-7 host-tool `notify` path, the local test notification + * and the "Sent"/"Already answered"/"Failed" result update. + * + * Tag `:` replaces instead of stacking; every notification + * carries a private public version for the lock screen, and its intents carry + * the origin they were minted for ([origin]). + * + * Event notifications join the Android group of their session's project and a + * per-project summary is refreshed after every change ("3 sessions in + * ompwebandroid"). A grouped child also carries a delete intent, and every + * foreground visit rebuilds all summaries, so tapping or swiping a child away + * cannot leave a stale count/title behind. Vibration lives in the channels + * (immutable after creation, so the vibrating `_v2` and quiet `_quiet` twins + * exist side by side); [vibrate] picks the twin on every post. + */ +class AndroidNotifier( + private val context: Context, + private val origin: () -> String?, + /** The current "Vibrate" setting; channels cannot be edited after creation. */ + private val vibrate: () -> Boolean = { true }, +) : Notifier { + + override fun ensureChannels() { + val manager = context.getSystemService(NotificationManager::class.java) ?: return + // A user who blocked a legacy channel keeps that decision on the new + // twins; `createNotificationChannels` never raises an importance. + val blocked = NotifyChannel.entries + .filter { it.supportsVibration } + .filter { manager.getNotificationChannel(it.id)?.importance == NotificationManager.IMPORTANCE_NONE } + .toSet() + manager.createNotificationChannels( + buildList { + addAll( + eventChannels( + NotifyChannel.NeedsInput, + R.string.notify_channel_needs_input, + R.string.notify_channel_needs_input_description, + NotificationManager.IMPORTANCE_HIGH, + blocked, + ), + ) + addAll( + eventChannels( + NotifyChannel.Errors, + R.string.notify_channel_errors, + R.string.notify_channel_errors_description, + NotificationManager.IMPORTANCE_HIGH, + blocked, + ), + ) + addAll( + eventChannels( + NotifyChannel.Completed, + R.string.notify_channel_completed, + R.string.notify_channel_completed_description, + NotificationManager.IMPORTANCE_DEFAULT, + blocked, + ), + ) + add( + channel( + NotifyChannel.ModelSwitch.id, + R.string.notify_channel_model_switch, + R.string.notify_channel_model_switch_description, + NotificationManager.IMPORTANCE_LOW, + ), + ) + add( + channel( + NotifyChannel.ConnectionStatus.id, + R.string.notify_channel_connection, + R.string.notify_channel_connection_description, + NotificationManager.IMPORTANCE_MIN, + ).apply { + setSound(null, null) + enableVibration(false) + setShowBadge(false) + lockscreenVisibility = Notification.VISIBILITY_SECRET + }, + ) + add( + channel( + NotifyChannel.AgentMessages.id, + R.string.notify_channel_agent_messages, + R.string.notify_channel_agent_messages_description, + NotificationManager.IMPORTANCE_DEFAULT, + ), + ) + }, + ) + // The legacy event channel ids have no vibration pattern and can never + // get one; the `_v2`/`_quiet` twins replace them. Deleting the old ids + // keeps the system list free of duplicates (posting no longer uses them). + NotifyChannel.entries + .filter { it.supportsVibration } + .forEach { manager.deleteNotificationChannel(it.id) } + } + + /** The vibrating and the quiet twin of one event channel. */ + private fun eventChannels( + channel: NotifyChannel, + name: Int, + description: Int, + importance: Int, + blocked: Set, + ): List { + val pattern = channel.vibrationPattern() ?: return emptyList() + val effectiveImportance = if (channel in blocked) NotificationManager.IMPORTANCE_NONE else importance + return listOf( + channel(channel.vibratingId, name, description, effectiveImportance).apply { + enableVibration(true) + vibrationPattern = pattern + }, + channel(channel.quietId, name, description, effectiveImportance).apply { + this.name = context.getString(name) + context.getString(R.string.notify_channel_quiet_suffix) + enableVibration(false) + }, + ) + } + + private fun channel( + id: String, + name: Int, + description: Int, + importance: Int, + ): NotificationChannel = NotificationChannel( + id, + context.getString(name), + importance, + ).apply { + this.description = context.getString(description) + } + + fun canPostNotifications(): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val granted = ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + if (!granted) return false + } + return NotificationManagerCompat.from(context).areNotificationsEnabled() + } + + /** + * Posts one live event. The tag replaces an older notification of the same + * session/type and carries the answerable dialog actions. + */ + @SuppressLint("MissingPermission", "NotificationPermission") + override fun post(event: NotifyEvent, project: NotifyProject?): Boolean { + if (!canPostNotifications()) return false + ensureChannels() + val rendered = renderNotification(event) + val plan = notificationActions(event, context.getString(R.string.notify_reply_label)) + val builder = baseBuilder(rendered, rendered.channel.androidChannelId(vibrate())) + .setContentIntent(openSessionIntent(event.sessionId, rendered.tag)) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .addExtras( + Bundle().apply { + event.answerRequestId?.let { putString(NotificationIntents.EXTRA_REQUEST_ID, it) } + putString(NotificationIntents.EXTRA_SESSION_ID, event.sessionId) + project?.let { putString(NotificationIntents.EXTRA_PROJECT_LABEL, it.label) } + }, + ) + project?.let { + builder + .setGroup(it.key) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setDeleteIntent(dismissIntent(rendered.tag, it.key)) + } + + plan.buttons.forEachIndexed { index, button -> + val intent = answerIntent( + tag = rendered.tag, + index = index, + sessionId = event.sessionId, + requestId = event.answerRequestId, + answerJson = button.answer.toJson().toString(), + replyKind = null, + askQuestionId = null, + title = rendered.title, + channelId = rendered.channel.id, + ) + builder.addAction( + actionBuilder(button.label, intent).build(), + ) + } + + plan.reply?.let { reply -> + val (kind, questionId) = when (val target = reply.target) { + ReplyTarget.Input -> NotificationIntents.REPLY_INPUT to null + is ReplyTarget.Ask -> NotificationIntents.REPLY_ASK to target.questionId + ReplyTarget.FollowUp -> NotificationIntents.REPLY_FOLLOW_UP to null + } + val intent = answerIntent( + tag = rendered.tag, + index = 10, + sessionId = event.sessionId, + requestId = event.answerRequestId, + answerJson = null, + replyKind = kind, + askQuestionId = questionId, + title = rendered.title, + channelId = rendered.channel.id, + remoteInput = true, + ) + val remoteInput = RemoteInput.Builder(NotificationIntents.REPLY_KEY) + .setLabel(context.getString(R.string.notify_reply_hint)) + .build() + builder.addAction( + actionBuilder(reply.label, intent) + .addRemoteInput(remoteInput) + .build(), + ) + } + + val posted = notify(rendered.tag, EVENT_NOTIFICATION_ID, builder.build()) + if (posted && project != null) { + refreshGroupSummary( + groupKey = project.key, + added = GroupChild( + tag = rendered.tag, + sessionId = event.sessionId, + title = rendered.title, + label = project.label, + channel = rendered.channel, + postedAt = System.currentTimeMillis(), + ), + ) + } + return posted + } + + /** + * The Phase-7 host-tool `notify` path: same builder, channel and private + * public version as every other notification, one tag per message so + * stacked messages stay individual. + */ + @SuppressLint("MissingPermission", "NotificationPermission") + fun postAgentMessage(title: String, message: String): Boolean { + if (!canPostNotifications()) return false + ensureChannels() + val number = agentMessageIds.getAndIncrement() % 1_000_000 + val rendered = RenderedNotification( + title = clipNotificationText(title, NOTIFICATION_TITLE_MAX), + body = clipNotificationText(message), + tag = "agent-message:$number", + channel = NotifyChannel.AgentMessages, + ) + val notification = baseBuilder(rendered, rendered.channel.androidChannelId(vibrate())) + .setContentIntent(openSessionIntent(null, rendered.tag)) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .build() + return notify(rendered.tag, AGENT_MESSAGE_ID_BASE + number, notification) + } + + /** Settings "Send test notification" without a push subscription. */ + @SuppressLint("MissingPermission", "NotificationPermission") + override fun postTest(): Boolean { + if (!canPostNotifications()) return false + ensureChannels() + val rendered = renderTestNotification( + title = context.getString(R.string.notify_status_title), + body = context.getString(R.string.notify_test_body), + ) + val notification = baseBuilder(rendered, rendered.channel.androidChannelId(vibrate())) + .setContentIntent(openSessionIntent(null, rendered.tag)) + .build() + return notify(rendered.tag, TEST_NOTIFICATION_ID, notification) + } + + /** + * Replaces an answered notification by its result ("Sent" / "Already + * answered" / "Failed — open app"). The request id is deliberately not + * part of the updated notification, so a dismiss frame for the answered + * request cannot close the result. + * + * The result never vibrates (answering/sending is not an event), so it + * moves to the channel's quiet twin. It keeps the group of the replaced + * notification, otherwise the project's summary would shrink by one. + */ + @SuppressLint("MissingPermission", "NotificationPermission") + override fun postResult(tag: String, title: String, channelId: String, text: String, sessionId: String): Boolean { + if (!canPostNotifications()) return false + ensureChannels() + val channel = notifyChannelFor(channelId) ?: NotifyChannel.Completed + val previous = activeNotification(tag) + val group = previous?.notification?.group + val label = previous?.notification?.extras?.getString(NotificationIntents.EXTRA_PROJECT_LABEL) + val builder = NotificationCompat.Builder(context, channel.quietId) + .setSmallIcon(iconFor(channel)) + .setColor(colorFor(channel)) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(openSessionIntent(sessionId.takeIf { it.isNotBlank() }, tag)) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setPublicVersion(privateVersion(NotifyChannel.Completed)) + builder.addExtras( + Bundle().apply { + if (sessionId.isNotBlank()) putString(NotificationIntents.EXTRA_SESSION_ID, sessionId) + label?.let { putString(NotificationIntents.EXTRA_PROJECT_LABEL, it) } + }, + ) + group?.let { + builder + .setGroup(it) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setDeleteIntent(dismissIntent(tag, it)) + } + val posted = notify(tag, EVENT_NOTIFICATION_ID, builder.build()) + if (posted && group != null) { + refreshGroupSummary( + groupKey = group, + added = GroupChild( + tag = tag, + sessionId = sessionId, + title = title, + label = label.orEmpty(), + channel = channel, + postedAt = System.currentTimeMillis(), + ), + ) + } + return posted + } + + /** + * The one builder every posted notification starts from: channel, icon, + * title/body, priority and the private lock-screen version. + */ + private fun baseBuilder(rendered: RenderedNotification, channelId: String): NotificationCompat.Builder = + NotificationCompat.Builder(context, channelId) + .setSmallIcon(iconFor(rendered.channel)) + .setColor(colorFor(rendered.channel)) + .setContentTitle(rendered.title) + .setContentText(rendered.body) + .setStyle(NotificationCompat.BigTextStyle().bigText(rendered.body)) + .setAutoCancel(true) + .setPriority(priority(rendered.channel)) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setPublicVersion(privateVersion(rendered.channel)) + + /** Closes every notification this app posted (sign-out/server switch). */ + override fun cancelAll() { + runCatching { NotificationManagerCompat.from(context).cancelAll() } + } + + /** + * Closes every notification that carries [sessionId] — all event types and + * the answered result — and repairs each affected group summary. Opening a + * chat makes its notifications read in the transcript, so the shade must + * not keep a second copy. + */ + @SuppressLint("MissingPermission") + override fun cancelSession(sessionId: String) { + val session = sessionId.takeIf { it.isNotBlank() } ?: return + val active = readActiveNotifications() ?: return + val manager = NotificationManagerCompat.from(context) + val groups = LinkedHashSet() + val cancelled = HashSet() + for (shown in active) { + val tag = shown.tag ?: continue + val notification = shown.notification + if (notification.flags and Notification.FLAG_GROUP_SUMMARY != 0) continue + if (notification.extras.getString(NotificationIntents.EXTRA_SESSION_ID) != session) continue + cancelled += tag + notification.group?.let(groups::add) + manager.cancel(tag, shown.id) + } + if (cancelled.isEmpty()) return + // Cancelling is asynchronous; the system's list still holds the closed + // entries, so the summaries are rebuilt from this snapshot minus them. + val remaining = active.filterNot { it.tag in cancelled } + groups.forEach { refreshGroupSummary(groupKey = it, active = remaining) } + } + + /** + * Closes the notification of [tag] only when it still belongs to + * [requestId]: a buffered dismiss must not close the next dialog's + * notification of the same tag. + */ + @SuppressLint("MissingPermission") + override fun cancelIfMatches(tag: String, requestId: String) { + val manager = NotificationManagerCompat.from(context) + val match = try { + manager.activeNotifications.firstOrNull { shown -> + shown.tag == tag && + shown.notification.extras.getString(NotificationIntents.EXTRA_REQUEST_ID) == requestId + } + } catch (_: SecurityException) { + null + } ?: return + val group = match.notification.group + manager.cancel(match.tag, match.id) + // Cancelling is asynchronous; excluding the tag keeps the summary exact. + group?.let { refreshGroupSummary(groupKey = it, removedTag = tag) } + } + + /** + * A child was swiped away from the shade. Its delete intent carries the + * group and tag because the system's own list still contains the child + * while the broadcast is delivered. + */ + override fun onGroupChildDismissed(groupKey: String, tag: String) { + refreshGroupSummary(groupKey = groupKey, removedTag = tag) + } + + /** + * Rebuilds every summary the shade currently shows. A tap auto-cancels a + * child without firing its delete intent, so the app repairs all summaries + * when it comes to the foreground; the group keys come from the system's + * list, which survives process death. + */ + override fun refreshGroupSummaries() { + val active = readActiveNotifications() ?: return + val groups = active.mapNotNullTo(LinkedHashSet()) { it.notification.group } + groups.forEach { refreshGroupSummary(groupKey = it, active = active) } + } + + /** One child of a project group: exactly what the summary needs. */ + private data class GroupChild( + val tag: String, + val sessionId: String, + val title: String, + val label: String, + val channel: NotifyChannel, + val postedAt: Long, + ) + + private fun activeNotification(tag: String): StatusBarNotification? = + readActiveNotifications()?.firstOrNull { it.tag == tag } + + private fun readActiveNotifications(): List? = try { + NotificationManagerCompat.from(context).activeNotifications + } catch (_: SecurityException) { + null + } + + /** + * Rebuilds the group's summary from the notifications that exist right + * now, plus the child just posted ([added]) or minus the one just closed + * ([removedTag]) — the system's own list lags one binder call behind. + * A group without children loses its summary. + */ + private fun refreshGroupSummary( + groupKey: String, + added: GroupChild? = null, + removedTag: String? = null, + active: List? = null, + ) { + // Without the system's list a summary built from [added] alone would + // undercount the group; leaving the old one is less wrong. + val shown = active ?: readActiveNotifications() ?: return + val children = groupChildren(shown, groupKey, added, removedTag) + val manager = NotificationManagerCompat.from(context) + if (children.isEmpty()) { + runCatching { manager.cancel(groupSummaryTag(groupKey), GROUP_SUMMARY_ID) } + return + } + val label = children.firstNotNullOfOrNull { it.label.takeIf(String::isNotBlank) } + ?: groupKey.substringAfterLast('/').takeIf { it.isNotBlank() } + ?: context.getString(R.string.notify_status_title) + val sessions = children.mapTo(HashSet()) { it.sessionId.ifBlank { it.tag } } + val titles = children.map { it.title }.filter { it.isNotBlank() } + val summaryChannel = children.map { it.channel }.maxByOrNull { it.summaryRank } ?: NotifyChannel.Completed + val style = NotificationCompat.InboxStyle() + titles.take(MAX_SUMMARY_LINES).forEach(style::addLine) + val summary = NotificationCompat.Builder(context, summaryChannel.androidChannelId(vibrate = false)) + .setSmallIcon(R.drawable.ic_notify_summary) + .setColor(ContextCompat.getColor(context, R.color.notify_accent_summary)) + .setContentTitle( + context.resources.getQuantityString(R.plurals.notify_group_summary, sessions.size, sessions.size, label), + ) + .setContentText(titles.firstOrNull() ?: label) + .setStyle(style) + .setContentIntent(openSessionIntent(null, groupSummaryTag(groupKey))) + .setGroup(groupKey) + .setGroupSummary(true) + // The children alert on their own; the summary must not repeat it. + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setSilent(true) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setPublicVersion(privateVersion(summaryChannel)) + .build() + notify(groupSummaryTag(groupKey), GROUP_SUMMARY_ID, summary) + } + + private fun groupChildren( + active: List?, + groupKey: String, + added: GroupChild?, + removedTag: String?, + ): List { + val fromSystem = active.orEmpty().mapNotNull { shown -> + val notification = shown.notification + val tag = shown.tag ?: return@mapNotNull null + if (tag == removedTag || tag == added?.tag) return@mapNotNull null + if (notification.group != groupKey) return@mapNotNull null + if (notification.flags and Notification.FLAG_GROUP_SUMMARY != 0) return@mapNotNull null + GroupChild( + tag = tag, + sessionId = notification.extras.getString(NotificationIntents.EXTRA_SESSION_ID).orEmpty(), + title = notification.extras.getCharSequence(Notification.EXTRA_TITLE)?.toString().orEmpty(), + label = notification.extras.getString(NotificationIntents.EXTRA_PROJECT_LABEL).orEmpty(), + channel = notifyChannelFor(notification.channelId) ?: NotifyChannel.Completed, + postedAt = shown.postTime, + ) + } + return (fromSystem + listOfNotNull(added)) + .sortedByDescending { it.postedAt } + } + + private fun groupSummaryTag(groupKey: String): String = "group:$groupKey" + + /** Which child's channel the summary borrows its importance from. */ + private val NotifyChannel.summaryRank: Int get() = when (this) { + NotifyChannel.NeedsInput -> 3 + NotifyChannel.Errors -> 2 + NotifyChannel.Completed -> 1 + NotifyChannel.ModelSwitch, NotifyChannel.AgentMessages -> 0 + NotifyChannel.ConnectionStatus -> -1 + } + + private fun notify(tag: String, id: Int, notification: Notification): Boolean = try { + NotificationManagerCompat.from(context).notify(tag, id, notification) + true + } catch (_: SecurityException) { + false + } + + /** + * Fired by the system when the user swipes a grouped child away (a tap + * auto-cancels without it, hence the foreground rebuild). The receiver + * re-derives the summary; without it the count/title would stay stale + * until the next post into the group. + */ + private fun dismissIntent(tag: String, groupKey: String): PendingIntent { + val intent = Intent(context, NotificationActionReceiver::class.java).apply { + action = NotificationIntents.ACTION_DISMISS + // Unique data per tag: PendingIntent equality would otherwise + // merge the delete intents of two children into one. + data = Uri.parse("ompweb://notify/${Uri.encode(tag)}/dismiss") + putExtra(NotificationIntents.EXTRA_TAG, tag) + putExtra(NotificationIntents.EXTRA_GROUP_KEY, groupKey) + } + return PendingIntent.getBroadcast( + context, + tag.hashCode() * 31 + DISMISS_REQUEST_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + + private fun openSessionIntent(sessionId: String?, tag: String): PendingIntent { + val session = sessionId?.takeIf { it.isNotBlank() } + val intent = Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_VIEW + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + session?.let { + putExtra(NotificationIntents.EXTRA_SESSION_ID, it) + // Only this app's own PendingIntent can carry a valid token, so + // the exported activity can tell a tap from a foreign intent. + putExtra(NotificationIntents.EXTRA_OPEN_TOKEN, NotificationOpenToken.sign(context, it)) + origin()?.let { server -> putExtra(NotificationIntents.EXTRA_ORIGIN, server) } + } + } + return PendingIntent.getActivity( + context, + tag.hashCode(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + + private fun answerIntent( + tag: String, + index: Int, + sessionId: String, + requestId: String?, + answerJson: String?, + replyKind: String?, + askQuestionId: String?, + title: String, + channelId: String, + remoteInput: Boolean = false, + ): PendingIntent { + val intent = Intent(context, NotificationActionReceiver::class.java).apply { + action = NotificationIntents.ACTION_ANSWER + // Unique data per tag/action: otherwise PendingIntent equality + // would merge the answer of one dialog into another. + data = Uri.parse("ompweb://notify/${Uri.encode(tag)}/$index") + putExtra(NotificationIntents.EXTRA_SESSION_ID, sessionId) + putExtra(NotificationIntents.EXTRA_TAG, tag) + putExtra(NotificationIntents.EXTRA_TITLE, title) + putExtra(NotificationIntents.EXTRA_CHANNEL_ID, channelId) + origin()?.let { putExtra(NotificationIntents.EXTRA_ORIGIN, it) } + requestId?.let { putExtra(NotificationIntents.EXTRA_REQUEST_ID, it) } + answerJson?.let { putExtra(NotificationIntents.EXTRA_ANSWER_JSON, it) } + replyKind?.let { putExtra(NotificationIntents.EXTRA_REPLY_KIND, it) } + askQuestionId?.let { putExtra(NotificationIntents.EXTRA_ASK_QUESTION_ID, it) } + } + // Android 12 refuses to post a notification whose RemoteInput action + // carries an immutable PendingIntent and ignores the fill-in intent that + // holds the typed text. Only the Reply action is mutable; buttons and + // content intents stay immutable. + val mutability = if (remoteInput && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_IMMUTABLE + } + return PendingIntent.getBroadcast( + context, + tag.hashCode() * 31 + index, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or mutability, + ) + } + + /** + * Every answer/reply action needs an unlocked device on API 31+ (the + * compat builder forwards this to the platform action). Below 31 the + * receiver's own keyguard check opens the app instead. + */ + private fun actionBuilder( + label: String, + intent: PendingIntent, + ): NotificationCompat.Action.Builder { + val builder = NotificationCompat.Action.Builder(R.drawable.ic_notification, label, intent) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAuthenticationRequired(true) + } + return builder + } + + private fun privateVersion(channel: NotifyChannel): Notification = + NotificationCompat.Builder(context, channel.androidChannelId(vibrate = false)) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(context.getString(R.string.app_name)) + .setContentText(context.getString(R.string.notify_private_body)) + .setContentIntent(openSessionIntent(null, channel.quietId)) + .setPriority(priority(channel)) + .build() + + private fun priority(channel: NotifyChannel): Int = when (channel) { + NotifyChannel.NeedsInput, NotifyChannel.Errors -> NotificationCompat.PRIORITY_HIGH + NotifyChannel.Completed, NotifyChannel.AgentMessages -> NotificationCompat.PRIORITY_DEFAULT + NotifyChannel.ModelSwitch -> NotificationCompat.PRIORITY_LOW + NotifyChannel.ConnectionStatus -> NotificationCompat.PRIORITY_MIN + } + + /** The monochrome 24 dp small icon of one notification type. */ + private fun iconFor(channel: NotifyChannel): Int = when (channel) { + NotifyChannel.Completed -> R.drawable.ic_notify_completed + NotifyChannel.NeedsInput -> R.drawable.ic_notify_input + NotifyChannel.Errors -> R.drawable.ic_notify_error + NotifyChannel.ModelSwitch -> R.drawable.ic_notify_model_switch + NotifyChannel.ConnectionStatus -> R.drawable.ic_notify_connection + NotifyChannel.AgentMessages -> R.drawable.ic_notify_message + } + + /** The accent the system tints the icon and app name with (theme status tokens). */ + private fun colorFor(channel: NotifyChannel): Int = ContextCompat.getColor( + context, + when (channel) { + NotifyChannel.Completed -> R.color.notify_accent_completed + NotifyChannel.NeedsInput -> R.color.notify_accent_input + NotifyChannel.Errors -> R.color.notify_accent_error + NotifyChannel.ModelSwitch -> R.color.notify_accent_model_switch + NotifyChannel.ConnectionStatus -> R.color.notify_accent_connection + NotifyChannel.AgentMessages -> R.color.notify_accent_message + }, + ) + + private companion object { + const val EVENT_NOTIFICATION_ID = 1 + const val TEST_NOTIFICATION_ID = 2 + const val GROUP_SUMMARY_ID = 3 + const val AGENT_MESSAGE_ID_BASE = 4_100 + + /** Separates the delete intent's request code from the answer actions'. */ + const val DISMISS_REQUEST_CODE = 6 + + /** The summary's inbox never needs more lines than the shade shows. */ + const val MAX_SUMMARY_LINES = 5 + + val agentMessageIds = AtomicInteger() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/BootReceiver.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/BootReceiver.kt new file mode 100644 index 000000000..e6ef430b1 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/BootReceiver.kt @@ -0,0 +1,32 @@ +package io.github.kahme247.ompweb.notify + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import io.github.kahme247.ompweb.OmpWebApplication +import kotlinx.coroutines.Job + +/** + * Brings the notification service back after a reboot and after the app is + * updated. The service is only started when notifications are enabled and a + * server/session is still configured; `NotificationCenter` re-checks both. + * + * `goAsync()` keeps the process alive until the stored settings are read and + * the service start has been issued; Android gives a manifest receiver about + * ten seconds for that. + */ +class BootReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if ( + intent.action != Intent.ACTION_BOOT_COMPLETED && + intent.action != Intent.ACTION_MY_PACKAGE_REPLACED + ) { + return + } + val application = context.applicationContext as? OmpWebApplication ?: return + val pending = goAsync() + val job: Job = application.container.notifications.onBoot() + job.invokeOnCompletion { pending.finish() } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/InAppHaptics.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/InAppHaptics.kt new file mode 100644 index 000000000..47ae1eae6 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/InAppHaptics.kt @@ -0,0 +1,92 @@ +package io.github.kahme247.ompweb.notify + +import android.app.NotificationManager +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioManager +import android.os.Build +import android.os.VibrationEffect +import android.os.VibrationAttributes +import android.os.Vibrator +import android.os.VibratorManager +import io.github.kahme247.ompweb.domain.NotifyType +import io.github.kahme247.ompweb.domain.vibrationPattern + +/** + * The pulse for a notice the app shows itself (foreground toast, or the + * in-app fallback when Android refused the system notification). System + * notifications vibrate through their channel on their own; only these + * in-app deliveries need a manual waveform. + */ +fun interface InAppHaptics { + /** Pulses [type]'s waveform; null/no pattern stays silent. Never called for sends. */ + fun pulse(type: NotifyType?) +} + +/** + * Whether an in-app event may vibrate: the user's "Vibrate" setting, a real + * vibrator, and the system's silent/Do-Not-Disturb state. The AudioAttributes + * of the actual vibration additionally let the platform apply its own policy, + * so this is the first, cheap gate rather than the only one. + */ +fun inAppVibrationAllowed( + enabled: Boolean, + hasVibrator: Boolean, + ringerSilent: Boolean, + dndActive: Boolean, +): Boolean = enabled && hasVibrator && !ringerSilent && !dndActive + +/** Vibration in the app's own notice, with the per-event waveform. */ +class AndroidHaptics( + private val context: Context, + /** The current `NotifyPrefs.vibrate`; read on every pulse, not once. */ + private val enabled: () -> Boolean, +) : InAppHaptics { + + override fun pulse(type: NotifyType?) { + val pattern = type?.vibrationPattern() ?: return + val vibrator = vibrator(context) ?: return + val audio = context.getSystemService(AudioManager::class.java) + val policy = context.getSystemService(NotificationManager::class.java) + val filter = runCatching { policy?.currentInterruptionFilter }.getOrNull() + if ( + !inAppVibrationAllowed( + enabled = enabled(), + hasVibrator = vibrator.hasVibrator(), + ringerSilent = audio?.ringerMode == AudioManager.RINGER_MODE_SILENT, + // UNKNOWN/ALL/PRIORITY do not block outright; NONE (total silence) + // and ALARMS do. + dndActive = filter == NotificationManager.INTERRUPTION_FILTER_NONE || + filter == NotificationManager.INTERRUPTION_FILTER_ALARMS, + ) + ) { + return + } + val effect = VibrationEffect.createWaveform(pattern, -1) + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + vibrator.vibrate( + effect, + VibrationAttributes.createForUsage(VibrationAttributes.USAGE_NOTIFICATION), + ) + } else { + @Suppress("DEPRECATION") + vibrator.vibrate( + effect, + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(), + ) + } + } + } + + private fun vibrator(context: Context): Vibrator? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + context.getSystemService(VibratorManager::class.java)?.defaultVibrator + } else { + @Suppress("DEPRECATION") + context.getSystemService(Vibrator::class.java) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationActionReceiver.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationActionReceiver.kt new file mode 100644 index 000000000..ac08504b0 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationActionReceiver.kt @@ -0,0 +1,224 @@ +package io.github.kahme247.ompweb.notify + +import android.app.KeyguardManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.app.RemoteInput +import io.github.kahme247.ompweb.MainActivity +import io.github.kahme247.ompweb.OmpWebApplication +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.NotifyAnswer +import io.github.kahme247.ompweb.domain.NotifyChannel +import io.github.kahme247.ompweb.domain.ReplyTarget +import io.github.kahme247.ompweb.domain.parseNotifyAnswer +import io.github.kahme247.ompweb.domain.replyAnswer +import io.github.kahme247.ompweb.domain.sanitizeReplyText +import io.github.kahme247.ompweb.net.AnswerOutcome +import io.github.kahme247.ompweb.net.captureApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout + +/** + * Answers a notification action without opening the app: `goAsync()` plus one + * short coroutine (no WorkManager). The server's answer route never spawns + * omp; a completed run's Reply is a regular follow-up prompt instead. + * + * Results replace the notification: "Sent", "Already answered" or "Failed — + * open app". On API < 31 the notification cannot require authentication, so a + * locked device opens the app instead of answering; API 31+ actions are + * marked `setAuthenticationRequired(true)` by the renderer. + */ +class NotificationActionReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == NotificationIntents.ACTION_DISMISS) { + // Rebuilding a summary only needs the NotificationManager; going + // through the app container would boot stores, network and the + // event service from a swipe on the shade. + dismiss( + intent, + AndroidNotifier(context.applicationContext, origin = { null }, vibrate = { false }), + ) + return + } + if (intent.action != NotificationIntents.ACTION_ANSWER) return + val sessionId = intent.getStringExtra(NotificationIntents.EXTRA_SESSION_ID) ?: return + val tag = intent.getStringExtra(NotificationIntents.EXTRA_TAG) ?: return + val application = context.applicationContext as? OmpWebApplication ?: return + + val keyguard = context.getSystemService(KeyguardManager::class.java) + if (keyguard?.isDeviceLocked == true) { + openApp(context, sessionId) + return + } + + val pending = goAsync() + CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { + try { + withTimeout(TIMEOUT_MILLIS) { + handle(application, intent, sessionId, tag) + } + } catch (_: TimeoutCancellationException) { + showResult(context, intent, tag, sessionId, R.string.notify_result_failed) + } catch (_: Exception) { + showResult(context, intent, tag, sessionId, R.string.notify_result_failed) + } finally { + pending.finish() + } + } + } + + private suspend fun handle( + application: OmpWebApplication, + intent: Intent, + sessionId: String, + tag: String, + ) { + val container = application.container + // A cold process (notification action after the app was swiped away) + // has no origin until the first DataStore emission; without this wait + // every request is NotConfigured and the answer is lost. + container.settings.awaitLoaded() + // A notification of a server the user left must never answer into the + // configured one (mirrors the origin-bound cookie): silently drop it. + if ( + !NotificationIntents.acceptsOrigin( + intent.getStringExtra(NotificationIntents.EXTRA_ORIGIN), + container.settings.originNow()?.origin, + ) + ) { + return + } + val replyKind = intent.getStringExtra(NotificationIntents.EXTRA_REPLY_KIND) + + if (replyKind != null) { + val text = sanitizeReplyText( + RemoteInput.getResultsFromIntent(intent)?.getCharSequence(NotificationIntents.REPLY_KEY), + ) + if (text == null) { + openApp(application, sessionId) + return + } + val target = when (replyKind) { + NotificationIntents.REPLY_INPUT -> ReplyTarget.Input + NotificationIntents.REPLY_FOLLOW_UP -> ReplyTarget.FollowUp + NotificationIntents.REPLY_ASK -> { + val questionId = intent.getStringExtra(NotificationIntents.EXTRA_ASK_QUESTION_ID) + if (questionId.isNullOrBlank()) { + showResult(application, intent, tag, sessionId, R.string.notify_result_failed) + return + } + ReplyTarget.Ask(questionId) + } + else -> { + showResult(application, intent, tag, sessionId, R.string.notify_result_failed) + return + } + } + val answer = replyAnswer(target, text) + if (answer == null) { + // `completed` Reply: a follow-up prompt, not a dialog answer. + val result = captureApi { container.notificationsApi.followUp(sessionId, text) } + result.fold( + onSuccess = { showResult(application, intent, tag, sessionId, R.string.notify_result_sent) }, + onFailure = { showResult(application, intent, tag, sessionId, R.string.notify_result_failed) }, + ) + } else { + answer(application, intent, tag, sessionId, answer) + } + return + } + + val requestId = intent.getStringExtra(NotificationIntents.EXTRA_REQUEST_ID) + val answer = parseNotifyAnswer(intent.getStringExtra(NotificationIntents.EXTRA_ANSWER_JSON)) + if (requestId.isNullOrBlank() || answer == null) { + showResult(application, intent, tag, sessionId, R.string.notify_result_failed) + return + } + answer(application, intent, tag, sessionId, answer) + } + + private suspend fun answer( + application: OmpWebApplication, + intent: Intent, + tag: String, + sessionId: String, + answer: NotifyAnswer, + ) { + val requestId = intent.getStringExtra(NotificationIntents.EXTRA_REQUEST_ID) + if (requestId.isNullOrBlank()) { + showResult(application, intent, tag, sessionId, R.string.notify_result_failed) + return + } + val outcome = captureApi { + application.container.notificationsApi.answer(sessionId, requestId, answer) + } + val text = outcome.fold( + onSuccess = { status -> + when (status) { + AnswerOutcome.Answered -> R.string.notify_result_sent + AnswerOutcome.AlreadySettled -> R.string.notify_result_already + AnswerOutcome.SessionGone -> R.string.notify_result_failed + } + }, + onFailure = { R.string.notify_result_failed }, + ) + showResult(application, intent, tag, sessionId, text) + } + + private fun showResult( + context: Context, + intent: Intent, + tag: String, + sessionId: String, + textRes: Int, + ) { + val application = context.applicationContext as? OmpWebApplication ?: return + val title = intent.getStringExtra(NotificationIntents.EXTRA_TITLE) + ?: context.getString(R.string.notify_status_title) + val channelId = intent.getStringExtra(NotificationIntents.EXTRA_CHANNEL_ID) + ?: NotifyChannel.Completed.id + application.container.notifications.notifier.postResult( + tag = tag, + title = title, + channelId = channelId, + text = context.getString(textRes), + sessionId = sessionId, + ) + } + + private fun openApp(context: Context, sessionId: String) { + val application = context.applicationContext as? OmpWebApplication ?: return + val launch = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtra(NotificationIntents.EXTRA_SESSION_ID, sessionId) + putExtra(NotificationIntents.EXTRA_OPEN_TOKEN, NotificationOpenToken.sign(application, sessionId)) + application.container.settings.originNow()?.origin?.let { + putExtra(NotificationIntents.EXTRA_ORIGIN, it) + } + } + runCatching { context.startActivity(launch) } + } + + internal companion object { + /** Long enough for a slow Tailscale round trip, short enough for a broadcast. */ + const val TIMEOUT_MILLIS = 20_000L + + /** + * A grouped child was swiped away: rebuild its project's summary. The + * tag is handed over as removed because the system's list still holds + * the child when the delete broadcast arrives. + */ + fun dismiss(intent: Intent, notifier: Notifier) { + if (intent.action != NotificationIntents.ACTION_DISMISS) return + val tag = intent.getStringExtra(NotificationIntents.EXTRA_TAG) ?: return + val groupKey = intent.getStringExtra(NotificationIntents.EXTRA_GROUP_KEY) ?: return + notifier.onGroupChildDismissed(groupKey, tag) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationCenter.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationCenter.kt new file mode 100644 index 000000000..35d50f473 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationCenter.kt @@ -0,0 +1,664 @@ +package io.github.kahme247.ompweb.notify + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import io.github.kahme247.ompweb.data.SettingsStore +import io.github.kahme247.ompweb.domain.InAppNotice +import io.github.kahme247.ompweb.domain.NoticeRoute +import io.github.kahme247.ompweb.domain.NotifyEvent +import io.github.kahme247.ompweb.domain.NotifyIdentity +import io.github.kahme247.ompweb.domain.NotifyPrefs +import io.github.kahme247.ompweb.domain.NotifyProject +import io.github.kahme247.ompweb.domain.NotifyRuntimeAction +import io.github.kahme247.ompweb.domain.NotifyType +import io.github.kahme247.ompweb.domain.NotifyWhenActive +import io.github.kahme247.ompweb.domain.PresenceReport +import io.github.kahme247.ompweb.domain.inAppNotice +import io.github.kahme247.ompweb.domain.isValidNotificationId +import io.github.kahme247.ompweb.domain.nextPresenceSeq +import io.github.kahme247.ompweb.domain.noticeRoute +import io.github.kahme247.ompweb.domain.noticeRouteAfterPost +import io.github.kahme247.ompweb.domain.notificationRuntimeAction +import io.github.kahme247.ompweb.domain.parseNotifyDismiss +import io.github.kahme247.ompweb.domain.parseNotifyEvent +import io.github.kahme247.ompweb.domain.resolvePresence +import io.github.kahme247.ompweb.domain.runningSessionCount +import io.github.kahme247.ompweb.events.ConnectionState +import io.github.kahme247.ompweb.events.EventChannel +import io.github.kahme247.ompweb.events.EventMux +import io.github.kahme247.ompweb.net.NotificationsApi +import io.github.kahme247.ompweb.net.TestPushResult +import io.github.kahme247.ompweb.net.captureApi +import io.github.kahme247.ompweb.net.userMessage +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive + +/** What the settings screen shows about the device's permissions. */ +data class NotificationPermissionStatus( + val appNotificationsEnabled: Boolean, + val postNotificationsGranted: Boolean, + val batteryUnrestricted: Boolean, + val canRequestPostNotifications: Boolean, +) + +/** Result of the settings "Send test notification" button. */ +sealed interface NotificationTestOutcome { + data object Sent : NotificationTestOutcome + data object LocalShown : NotificationTestOutcome + data object Blocked : NotificationTestOutcome + data class Failed(val message: String) : NotificationTestOutcome +} + +/** + * The app-level owner of notification state: identity and registration, + * prefs, presence, service control and frame delivery. + * + * The foreground service calls [activateRuntime]/[deactivateRuntime]; every + * other lifecycle point (sign-in, sign-out, prefs change, boot) goes through + * this class so there is exactly one place that decides whether the service + * and its single SSE subscription should run. + */ +class NotificationCenter( + private val context: Context, + private val settings: SettingsStore, + private val api: NotificationsApi, + private val mux: EventMux, + private val scope: CoroutineScope, + private val isSignedIn: () -> Boolean, + /** The answer receiver replaces a notification with its result text. */ + internal val notifier: Notifier = AndroidNotifier( + context.applicationContext, + origin = { settings.originNow()?.origin }, + vibrate = { settings.snapshot.value.notifyPrefs.vibrate }, + ), + /** The pulse for in-app notices; system notifications vibrate via their channel. */ + private val haptics: InAppHaptics = AndroidHaptics( + context.applicationContext, + enabled = { settings.snapshot.value.notifyPrefs.vibrate }, + ), +) { + + val connection: StateFlow = mux.state + + private val _runningCount = MutableStateFlow(0) + val runningCount: StateFlow = _runningCount.asStateFlow() + + private val _runtimeActive = MutableStateFlow(false) + val runtimeActive: StateFlow = _runtimeActive.asStateFlow() + + private val _registrationError = MutableStateFlow(null) + val registrationError: StateFlow = _registrationError.asStateFlow() + + private val _permissionRequests = MutableSharedFlow(extraBufferCapacity = 1) + val permissionRequests: SharedFlow = _permissionRequests.asSharedFlow() + + /** Bumped after a permission answer or a return from system settings. */ + private val _permissionRevision = MutableStateFlow(0) + val permissionRevision: StateFlow = _permissionRevision.asStateFlow() + + fun refreshPermissions() { + _permissionRevision.value += 1 + } + + /** A notification delivered live; B's history view model adds it without refetching. */ + @Volatile + var onDelivered: ((JsonObject) -> Unit)? = null + + private val _notices = MutableStateFlow>(emptyList()) + + /** + * In-app deliveries (web toast): `whenActive:"toast"` output and every + * system post the app was not allowed to make. The shell renders them and + * a tap opens the session and marks its history entry read. + */ + val notices: StateFlow> = _notices.asStateFlow() + + /** The user closed one notice; a newer notice of the same tag stays. */ + fun dismissNotice(notice: InAppNotice) { + _notices.update { current -> + current.filterNot { it.id == notice.id && it.requestId == notice.requestId } + } + } + + private val activationMutex = Mutex() + private val syncMutex = Mutex() + private val teardown = NotificationTeardown() + + /** + * Guards start/stop races: a service that is stopped and immediately + * restarted must not leave an activation job running after its + * deactivation (or the new service would have no subscription). + */ + private var runtimeEpoch = 0 + + private var notificationJob: Job? = null + private var runningJob: Job? = null + private var presenceJob: Job? = null + + private val presenceLock = Any() + private var presenceSeq = 0L + private var appForeground = false + private var visibleSession: String? = null + + init { + notifier.ensureChannels() + scope.launch { + settings.snapshot + .map { snapshot -> Triple(snapshot.loaded, snapshot.origin, snapshot.notifyPrefs) } + .distinctUntilChanged() + .collect { (loaded, origin, prefs) -> + when (val action = notificationRuntimeAction(loaded, origin != null, prefs.enabled, isSignedIn())) { + NotifyRuntimeAction.Idle -> Unit + NotifyRuntimeAction.Start -> { + NotificationService.start(context) + syncDevice() + } + is NotifyRuntimeAction.Stop -> { + NotificationService.stop(context) + // "Notifications off" must reach the server too, + // otherwise it keeps routing this device's events. + if (action.syncDevice) syncDevice() + } + } + } + } + } + + // ------------------------------------------------------------ lifecycle + + /** Signed in (or passwordless server ready): register and keep the service running. */ + fun onSignedIn() { + scope.launch { + settings.ensureNotifyIdentity() + syncDevice() + refreshService() + } + } + + /** + * `clearLocalUserData()`: stop the service, close this app's + * notifications, unregister the device while the old cookie and origin + * still exist, then forget the identity. + * + * Suspends until the unregister finished (bounded by + * [NotificationTeardown.DEFAULT_TIMEOUT_MILLIS]): the caller may only + * clear the cookie and change the origin afterwards, or the PUT would be + * unauthorized or go to the next installation. + */ + suspend fun clearDevice() { + val snapshot = settings.snapshot.value + val identity = snapshot.identityOrNull() + val origin = snapshot.origin + NotificationService.stop(context) + notifier.cancelAll() + _notices.value = emptyList() + teardown.run( + unregister = { + if (identity != null && origin != null) { + // Same lock as syncDeviceNow: an in-flight prefs PUT must + // not land after this one and re-enable the device. + syncMutex.withLock { + api.putDevice(identity.deviceId, snapshot.notifyPrefs.copy(enabled = false), origin) + } + } + }, + forgetIdentity = { settings.clearNotifyIdentity() }, + ) + } + + /** Boot or app update: restart the service only when it should be running. */ + fun onBoot(): Job = scope.launch { + val snapshot = settings.awaitLoaded() + if (snapshot.origin == null || !snapshot.notifyPrefs.enabled || !isSignedIn()) return@launch + settings.ensureNotifyIdentity() + syncDevice() + NotificationService.start(context) + } + + /** Start or stop the service according to prefs, sign-in and origin. */ + fun refreshService() { + val snapshot = settings.snapshot.value + if (!snapshot.loaded || snapshot.origin == null || !snapshot.notifyPrefs.enabled || !isSignedIn()) { + NotificationService.stop(context) + } else { + NotificationService.start(context) + } + } + + // ------------------------------------------------------------ service runtime + + /** Called once by [NotificationService]; the service holds this subscription. */ + fun activateRuntime() { + if (_runtimeActive.value) return + val epoch = ++runtimeEpoch + scope.launch { + activationMutex.withLock { + if (epoch != runtimeEpoch || _runtimeActive.value) return@withLock + // A restarted service runs before the first DataStore + // emission; judging prefs from an empty snapshot would stop a + // service that should keep running. + val snapshot = settings.awaitLoaded() + if (epoch != runtimeEpoch) return@withLock + if (!snapshot.notifyPrefs.enabled || !isSignedIn() || snapshot.origin == null) { + NotificationService.stop(context) + return@withLock + } + val identity = settings.ensureNotifyIdentity() + if (epoch != runtimeEpoch) return@withLock + _runtimeActive.value = true + // The service owns the one process-wide SSE connection now. + mux.setPersistent(true) + notificationJob = scope.launch { + try { + mux.subscribe(EventChannel.Notifications(identity.clientId)).collect { frame -> + deliverFrame(frame) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // EventMux reconnects; a refused channel only ends this flow. + } + } + runningJob = scope.launch { + try { + mux.subscribe(EventChannel.Running).collect { frame -> + runningSessionCount(frame)?.let { _runningCount.value = it } + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // Status only; the list screen has its own subscription. + } + } + presenceJob = scope.launch { + while (isActive) { + reportPresence() + delay(PRESENCE_INTERVAL_MILLIS) + } + } + reportPresence() + } + } + } + + /** Called from `NotificationService.onDestroy`. */ + fun deactivateRuntime() { + runtimeEpoch++ + if (!_runtimeActive.value) return + _runtimeActive.value = false + notificationJob?.cancel() + runningJob?.cancel() + presenceJob?.cancel() + notificationJob = null + runningJob = null + presenceJob = null + // Report the window state one last time: this is not a "gone" report, + // it carries whatever the app is showing (a `visible: true` report for + // the chat still on screen included), so the server keeps a fresh view + // of this device instead of a report that may be one interval old and + // lets that entry expire by its own TTL. + reportPresence(force = true) + mux.setPersistent(false) + } + + // ------------------------------------------------------------ presence + + fun setAppForeground(foreground: Boolean) { + val changed = synchronized(presenceLock) { + val before = appForeground + appForeground = foreground + before != foreground + } + if (foreground) { + // A tapped notification auto-cancels without a dismiss callback; + // only the next foreground visit can repair the group's count/title. + // Run this first: closing the visible chat's notifications rebuilds + // the affected summaries from its own snapshot, and that rebuild + // must be the last word on the group. + notifier.refreshGroupSummaries() + // Returning to a chat the user is looking at: its notifications + // belong to the transcript now, not the shade. + synchronized(presenceLock) { visibleSession }?.let(::clearSessionArtifacts) + } + if (changed) reportPresence() + } + + /** The chat currently on screen, or null on any other view. */ + fun setVisibleSession(sessionId: String?) { + val normalized = sessionId?.takeIf { it.isNotBlank() } + val changed = synchronized(presenceLock) { + val before = visibleSession + visibleSession = normalized + before != normalized + } + if (!changed) return + // Opening a chat closes what was already posted for it (web: the + // notification is read where the conversation is). A chat selected + // while the app is in the background keeps its notification until the + // user actually returns to it. + if (normalized != null && isAppForeground()) clearSessionArtifacts(normalized) + reportPresence() + } + + /** + * The app is "looking at" a session only in the foreground with exactly + * that chat's pane on screen (phone and two-pane). The server suppresses + * such events through the presence report; the local check covers the + * window between a state change and that report reaching it. + */ + private fun isSessionOnScreen(sessionId: String): Boolean = + synchronized(presenceLock) { appForeground && visibleSession == sessionId } + + /** Drops the shade notifications and in-app notices of one session. */ + private fun clearSessionArtifacts(sessionId: String) { + notifier.cancelSession(sessionId) + _notices.update { current -> current.filterNot { it.sessionId == sessionId } } + } + + private fun reportPresence(force: Boolean = false) { + if (!force && !_runtimeActive.value) return + val report = presenceReport() ?: return + scope.launch { api.reportPresence(report) } + } + + /** + * The report for the current window state, or null before an identity + * exists. [PresenceReport.seq] only grows, so the server never drops a + * newer visibility change because reports overtook each other. + */ + internal fun presenceReport(): PresenceReport? { + val identity = settings.snapshot.value.identityOrNull() ?: return null + val visibility = synchronized(presenceLock) { + resolvePresence(appForeground, visibleSession) + } + val seq = synchronized(presenceLock) { + presenceSeq = nextPresenceSeq(presenceSeq, System.currentTimeMillis()) + presenceSeq + } + return PresenceReport( + clientId = identity.clientId, + deviceId = identity.deviceId, + visible = visibility.visible, + sessionId = visibility.sessionId, + seq = seq, + ) + } + + // ------------------------------------------------------------ prefs + + fun setEnabled(enabled: Boolean) = updatePrefs { it.copy(enabled = enabled) } + + fun setTypeEnabled(type: NotifyType, enabled: Boolean) = updatePrefs { it.withType(type, enabled) } + + fun setWhenActive(whenActive: NotifyWhenActive) = updatePrefs { it.copy(whenActive = whenActive) } + + fun setVibrate(enabled: Boolean) = updatePrefs { it.copy(vibrate = enabled) } + + private fun updatePrefs(transform: (NotifyPrefs) -> NotifyPrefs) { + scope.launch { + val current = settings.snapshot.value.notifyPrefs + val next = transform(current) + if (next == current) return@launch + // The snapshot collector restarts/stops the service and registers + // the new prefs; doing it here too would double the PUT. + settings.setNotifyPrefs(next) + } + } + + // ------------------------------------------------------------ registration + + /** Sends the current prefs to the server; serialized and reads prefs when sent. */ + fun syncDevice() { + scope.launch { syncDeviceNow() } + } + + suspend fun syncDeviceNow() { + if (!isSignedIn() || settings.snapshot.value.origin == null) return + syncMutex.withLock { + var sent: NotifyPrefs? = null + while (true) { + val snapshot = settings.snapshot.value + if (snapshot.origin == null || !isSignedIn()) return@withLock + val prefs = snapshot.notifyPrefs + if (prefs == sent) return@withLock + sent = prefs + val identity = settings.ensureNotifyIdentity() + val result = runCatching { api.putDevice(identity.deviceId, prefs) } + _registrationError.value = result.exceptionOrNull()?.userMessage() + if (prefs == settings.snapshot.value.notifyPrefs) return@withLock + } + } + } + + // ------------------------------------------------------------ permissions + + fun permissionStatus(): NotificationPermissionStatus { + val appEnabled = NotificationManagerCompat.from(context).areNotificationsEnabled() + val granted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + } else { + true + } + val power = context.getSystemService(PowerManager::class.java) + return NotificationPermissionStatus( + appNotificationsEnabled = appEnabled, + postNotificationsGranted = granted, + batteryUnrestricted = power?.isIgnoringBatteryOptimizations(context.packageName) ?: true, + canRequestPostNotifications = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !granted, + ) + } + + /** The settings screen's fix button: the activity-level launcher collects this. */ + fun requestPostNotificationPermission() { + _permissionRequests.tryEmit(Unit) + } + + /** Opens the system dialog explaining and requesting the battery exemption. */ + fun requestBatteryExemption() { + val intent = Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:${context.packageName}"), + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + try { + context.startActivity(intent) + } catch (_: Exception) { + val fallback = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + runCatching { context.startActivity(fallback) } + } + } + + fun openSystemNotificationSettings() { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + runCatching { context.startActivity(intent) } + } + + // ------------------------------------------------------------ test + + /** + * `POST /api/notifications/test` for a subscribed device. Native devices + * have no Web Push subscription, so the normal path is a local + * notification through the same renderer; a 502 shows the push service's + * answer instead. + */ + suspend fun sendTest(): NotificationTestOutcome { + if (!isSignedIn() || settings.snapshot.value.origin == null) { + return NotificationTestOutcome.Failed("Not signed in") + } + val identity = settings.ensureNotifyIdentity() + syncDeviceNow() + val subscribed = captureApi { api.isSubscribed(identity.deviceId) }.getOrDefault(false) + if (!subscribed) { + return if (notifier.postTest()) { + NotificationTestOutcome.LocalShown + } else { + NotificationTestOutcome.Blocked + } + } + return try { + when (val result = api.sendTest(identity.deviceId)) { + TestPushResult.Sent -> NotificationTestOutcome.Sent + is TestPushResult.Failed -> NotificationTestOutcome.Failed(result.message) + } + } catch (error: Exception) { + NotificationTestOutcome.Failed(error.userMessage()) + } + } + + // ------------------------------------------------------------ delivery + + /** + * One frame of the notifications channel. One malformed or unexpected + * frame must never end the subscription: the collect loop keeps running + * and the next frame is still handled. + * + * Suspending on purpose: a notification may have to resolve its session's + * project before it is posted, and frames must keep their order (a dismiss + * for the same tag may follow immediately). + */ + internal suspend fun deliverFrame(frame: JsonObject) { + try { + onNotificationFrame(frame) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // Dropped frame; the channel stays subscribed. + } + } + + private suspend fun onNotificationFrame(frame: JsonObject) { + when (frame["type"]?.jsonPrimitive?.contentOrNull) { + "notification" -> { + val kind = frame["kind"]?.jsonPrimitive?.contentOrNull + val eventJson = frame["event"] as? JsonObject ?: return + val event = parseNotifyEvent(eventJson) ?: return + onDelivered?.invoke(eventJson) + // The server already keeps this device quiet while its presence + // says the chat is on screen; a report can still be in flight + // when the frame arrives. Enforce the rule locally as well: + // nothing is posted, shown or vibrated for the chat being read. + if (isSessionOnScreen(event.sessionId)) { + clearSessionArtifacts(event.sessionId) + return + } + // `whenActive:"toast"` stays in the app; a system post the app + // may not make (permission withdrawn, channel blocked) falls + // back to the same in-app notice instead of disappearing. + val route = noticeRoute(kind) + val posted = route == NoticeRoute.System && + notifier.post(event, projectFor(event.sessionId)) + if (noticeRouteAfterPost(route, posted) == NoticeRoute.InApp) { + showNotice(event) + // A notice the user sees in the app replaces the system + // notification, including its channel vibration. + if (isAppForeground()) haptics.pulse(event.type) + } + } + "notification-dismiss" -> { + parseNotifyDismiss(frame)?.let { dismiss -> + notifier.cancelIfMatches(dismiss.tag, dismiss.requestId) + // Only the notice standing for this request is retracted; a + // buffered dismiss must not close the next dialog's notice. + _notices.update { current -> + current.filterNot { it.matches(dismiss.tag, dismiss.requestId) } + } + } + } + } + } + + // ------------------------------------------------------------ grouping + + private val projectCache = mutableMapOf() + private var projectsFetchedAt = 0L + + /** Project lock; notification frames are serial, but lookups are also used by tests. */ + private val projectMutex = Mutex() + + /** + * The project a session's notifications are grouped under. The wire event + * carries no cwd, so the session list answers it (`projectKey`/`projectRoot`/ + * `cwd`). A lookup is cached for [PROJECT_CACHE_TTL]; when the server does + * not list the session (deleted, an old alias) the notification stays + * ungrouped instead of landing in a wrong group. + */ + private suspend fun projectFor(sessionId: String): NotifyProject? = projectMutex.withLock { + val now = System.currentTimeMillis() + if (projectCache.containsKey(sessionId) && now - projectsFetchedAt < PROJECT_CACHE_TTL) { + return@withLock projectCache[sessionId] + } + if (!projectCache.containsKey(sessionId) && now - projectsFetchedAt < PROJECT_LOOKUP_THROTTLE) { + return@withLock null + } + val fetched = runCatching { api.sessionProjects() }.getOrDefault(emptyMap()) + projectsFetchedAt = now + projectCache.putAll(fetched) + if (!projectCache.containsKey(sessionId)) projectCache[sessionId] = null + projectCache[sessionId] + } + + private fun isAppForeground(): Boolean = synchronized(presenceLock) { appForeground } + + /** Adds [event] as the newest in-app notice, replacing its tag's older one. */ + private fun showNotice(event: NotifyEvent) { + val notice = inAppNotice(event) + _notices.update { current -> notice.addTo(current, MAX_IN_APP_NOTICES) } + } + + private fun io.github.kahme247.ompweb.data.SettingsSnapshot.identityOrNull(): NotifyIdentity? { + val device = notifyDeviceId?.takeIf(::isValidNotificationId) ?: return null + val client = notifyClientId?.takeIf(::isValidNotificationId) ?: return null + return NotifyIdentity(device, client) + } + + companion object { + /** The server's presence TTL is 60 s; report at half of it. */ + const val PRESENCE_INTERVAL_MILLIS = 30_000L + + /** + * At most this many notices stay on screen; a new one pushes the + * oldest out instead of growing the overlay without bound. + */ + const val MAX_IN_APP_NOTICES = 3 + + /** How long a resolved session project is trusted before a refresh. */ + const val PROJECT_CACHE_TTL = 5 * 60_000L + + /** + * A session the list does not know is retried at most this often, so + * an unknown id cannot turn every event into a list request. + */ + const val PROJECT_LOOKUP_THROTTLE = 30_000L + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationIntents.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationIntents.kt new file mode 100644 index 000000000..3b466b65e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationIntents.kt @@ -0,0 +1,69 @@ +package io.github.kahme247.ompweb.notify + +/** + * Intent extras shared by the notification renderer, the answer receiver and + * the activity. Kept as strings because broadcasts and PendingIntents cannot + * carry Kotlin objects. + */ +object NotificationIntents { + const val EXTRA_SESSION_ID = "io.github.kahme247.ompweb.notify.sessionId" + const val EXTRA_TAG = "io.github.kahme247.ompweb.notify.tag" + const val EXTRA_REQUEST_ID = "io.github.kahme247.ompweb.notify.requestId" + const val EXTRA_ANSWER_JSON = "io.github.kahme247.ompweb.notify.answerJson" + const val EXTRA_REPLY_KIND = "io.github.kahme247.ompweb.notify.replyKind" + const val EXTRA_ASK_QUESTION_ID = "io.github.kahme247.ompweb.notify.askQuestionId" + const val EXTRA_TITLE = "io.github.kahme247.ompweb.notify.title" + const val EXTRA_CHANNEL_ID = "io.github.kahme247.ompweb.notify.channelId" + + /** + * Project display label stored on a posted notification (notification + * extras, not an intent): the group summary reads it back after a process + * restart instead of guessing from the group key. + */ + const val EXTRA_PROJECT_LABEL = "io.github.kahme247.ompweb.notify.projectLabel" + + /** + * `scheme://host:port` of the server the notification belongs to. The + * answer receiver drops an action whose origin is not the configured one, + * so a notification left over from another server can never answer into + * the current one. + */ + const val EXTRA_ORIGIN = "io.github.kahme247.ompweb.notify.origin" + + /** + * HMAC of the session id under a per-installation secret (see + * [NotificationOpenToken]). Only this app's own PendingIntents can set it, + * so the exported [io.github.kahme247.ompweb.MainActivity] refuses a + * foreign intent that tries to force a chat open. + */ + const val EXTRA_OPEN_TOKEN = "io.github.kahme247.ompweb.notify.openToken" + + /** The action of the answer broadcast. */ + const val ACTION_ANSWER = "io.github.kahme247.ompweb.notify.ANSWER" + + /** + * The action of a grouped child's delete intent: the user swiped the + * notification away and the project's summary must be rebuilt. + */ + const val ACTION_DISMISS = "io.github.kahme247.ompweb.notify.DISMISS" + + /** Project group of a dismissed child ([ACTION_DISMISS]). */ + const val EXTRA_GROUP_KEY = "io.github.kahme247.ompweb.notify.groupKey" + + /** RemoteInput result key of the Reply action. */ + const val REPLY_KEY = "io.github.kahme247.ompweb.notify.reply" + + /** Reply kinds of [EXTRA_REPLY_KIND]. */ + const val REPLY_INPUT = "input" + const val REPLY_ASK = "ask" + const val REPLY_FOLLOW_UP = "followUp" + + /** + * True when an action's [actionOrigin] may run against the install that is + * configured as [configuredOrigin]. An action without the configured + * origin — a notification of a server the user left, or a foreign + * broadcast — is dropped instead of answering into the current server. + */ + fun acceptsOrigin(actionOrigin: String?, configuredOrigin: String?): Boolean = + configuredOrigin != null && actionOrigin == configuredOrigin +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationOpenToken.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationOpenToken.kt new file mode 100644 index 000000000..365ccfdf9 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationOpenToken.kt @@ -0,0 +1,67 @@ +package io.github.kahme247.ompweb.notify + +import android.content.Context +import java.security.MessageDigest +import java.security.SecureRandom +import java.util.Base64 +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +/** + * Proof that a "open this session" intent came from one of our own + * notification PendingIntents. + * + * `MainActivity` has to stay exported (share target), so an explicit intent + * with `EXTRA_SESSION_ID` from any app would otherwise force a chat open. The + * token is an HMAC of the session id under a per-installation secret that only + * this app's private `SharedPreferences` holds: a foreign app can neither read + * the secret nor guess the token, and a leaked token is bound to that one + * session id. + * + * The HMAC itself takes the key as a parameter, so the rule is pinned by JVM + * tests; only [sign]/[verify] with a `Context` reach the private preferences. + */ +object NotificationOpenToken { + + private const val PREFS = "ompweb-notification-open" + private const val KEY_SECRET = "secret" + private const val ALGORITHM = "HmacSHA256" + private const val SECRET_BYTES = 32 + + /** The token that belongs into [NotificationIntents.EXTRA_OPEN_TOKEN]. */ + fun sign(context: Context, sessionId: String): String = sign(secret(context), sessionId) + + /** Constant-time check of a token that arrived with an exported intent. */ + fun verify(context: Context, sessionId: String, token: String?): Boolean = + verify(secret(context), sessionId, token) + + /** Token of [sessionId] under [secret]; an empty id can never be signed. */ + fun sign(secret: ByteArray, sessionId: String): String { + if (sessionId.isEmpty()) return "" + val mac = Mac.getInstance(ALGORITHM).apply { + init(SecretKeySpec(secret, ALGORITHM)) + } + return Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal(sessionId.toByteArray())) + } + + /** True only for the token [sign] produced for this secret and session id. */ + fun verify(secret: ByteArray, sessionId: String, token: String?): Boolean { + if (sessionId.isEmpty() || token.isNullOrEmpty()) return false + return MessageDigest.isEqual(sign(secret, sessionId).toByteArray(), token.toByteArray()) + } + + @Synchronized + private fun secret(context: Context): ByteArray { + val prefs = context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + decode(prefs.getString(KEY_SECRET, null))?.let { return it } + val created = ByteArray(SECRET_BYTES).also(SecureRandom()::nextBytes) + prefs.edit() + .putString(KEY_SECRET, Base64.getUrlEncoder().withoutPadding().encodeToString(created)) + .apply() + return created + } + + private fun decode(value: String?): ByteArray? = value + ?.let { encoded -> runCatching { Base64.getUrlDecoder().decode(encoded) }.getOrNull() } + ?.takeIf { it.size >= SECRET_BYTES } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationService.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationService.kt new file mode 100644 index 000000000..d78f5de08 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationService.kt @@ -0,0 +1,161 @@ +package io.github.kahme247.ompweb.notify + +import android.Manifest +import android.app.Notification +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat +import io.github.kahme247.ompweb.MainActivity +import io.github.kahme247.ompweb.OmpWebApplication +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.NotifyChannel +import io.github.kahme247.ompweb.events.ConnectionState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** + * The built-in delivery channel (PLAN §4): a `specialUse` foreground service + * that keeps the process alive and holds the notification service's single + * `/api/events` connection plus presence reports. Its persistent silent + * notification shows the connection status and the number of running agents. + * + * `START_STICKY` brings the service back after the app is swiped away; the + * boot receiver covers a reboot and app update. The service never opens a + * second socket — `EventMux` stays shared with the UI. + */ +class NotificationService : Service() { + + private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private lateinit var center: NotificationCenter + private var statusJob: Job? = null + + override fun onCreate() { + super.onCreate() + // Touching the container also arms the process-wide network callback + // that reconnects the shared stream; a service-only layout (the UI + // swiped away) must not lose that path. + center = (application as OmpWebApplication).container.notifications + promoteToForeground() + center.activateRuntime() + statusJob = serviceScope.launch { + combine(center.connection, center.runningCount) { state, running -> state to running } + .collect { (state, running) -> updateStatusNotification(state, running) } + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent?.action == ACTION_STOP) { + stopSelf() + return START_NOT_STICKY + } + // A restart (START_STICKY) arrives with a null intent; the notification + // must already be up before this method returns. + promoteToForeground() + center.activateRuntime() + return START_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onDestroy() { + statusJob?.cancel() + statusJob = null + center.deactivateRuntime() + serviceScope.cancel() + stopForeground(STOP_FOREGROUND_REMOVE) + super.onDestroy() + } + + private fun promoteToForeground() { + val notification = buildStatusNotification(center.connection.value, center.runningCount.value) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ServiceCompat.startForeground( + this, + STATUS_NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE, + ) + } else { + ServiceCompat.startForeground(this, STATUS_NOTIFICATION_ID, notification, 0) + } + } + + private fun updateStatusNotification(state: ConnectionState, running: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) return + val notification = buildStatusNotification(state, running) + runCatching { + NotificationManagerCompat.from(this).notify(STATUS_NOTIFICATION_ID, notification) + } + } + + private fun buildStatusNotification(state: ConnectionState, running: Int): Notification { + val text = when (state) { + ConnectionState.Connected -> + resources.getQuantityString(R.plurals.notify_status_connected, running, running) + ConnectionState.Reconnecting -> getString(R.string.notify_status_reconnecting) + ConnectionState.Offline -> getString(R.string.notify_status_unreachable) + } + val launch = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + val contentIntent = PendingIntent.getActivity( + this, + STATUS_NOTIFICATION_ID, + launch, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + return NotificationCompat.Builder(this, NotifyChannel.ConnectionStatus.id) + .setSmallIcon(R.drawable.ic_notify_connection) + .setColor(ContextCompat.getColor(this, R.color.notify_accent_connection)) + .setContentTitle(getString(R.string.notify_status_title)) + .setContentText(text) + .setContentIntent(contentIntent) + .setOngoing(true) + .setSilent(true) + .setOnlyAlertOnce(true) + .setShowWhen(false) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + .build() + } + + companion object { + const val ACTION_START = "io.github.kahme247.ompweb.notify.START" + const val ACTION_STOP = "io.github.kahme247.ompweb.notify.STOP" + private const val STATUS_NOTIFICATION_ID = 10 + + /** Called by [NotificationCenter]; a background start may be refused, which is not fatal. */ + fun start(context: Context) { + val intent = Intent(context, NotificationService::class.java).setAction(ACTION_START) + runCatching { ContextCompat.startForegroundService(context, intent) } + } + + fun stop(context: Context) { + runCatching { + context.stopService(Intent(context, NotificationService::class.java).setAction(ACTION_STOP)) + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationTeardown.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationTeardown.kt new file mode 100644 index 000000000..6634dd0c3 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/NotificationTeardown.kt @@ -0,0 +1,48 @@ +package io.github.kahme247.ompweb.notify + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.withTimeout + +/** + * Order of the sign-out / server-switch teardown for a registered notification + * device. + * + * The unregister PUT has to run while the old session cookie and origin still + * exist, and it must have finished (or timed out) *before* anything clears + * them — otherwise the server keeps pushing to a device whose answer endpoint + * no longer authenticates, or the PUT lands on the next installation. The + * local identity is forgotten afterwards. + * + * Kept free of Android types so JVM tests can assert the order. + */ +class NotificationTeardown( + private val timeoutMillis: Long = DEFAULT_TIMEOUT_MILLIS, +) { + + /** + * Runs [unregister] with a bounded wait, then [forgetIdentity]. A failed + * or timed-out unregister never blocks the sign-out: the device id is + * forgotten locally either way, so this installation cannot answer again. + */ + suspend fun run( + unregister: suspend () -> Unit, + forgetIdentity: suspend () -> Unit, + ) { + try { + withTimeout(timeoutMillis) { unregister() } + } catch (_: TimeoutCancellationException) { + // Offline server: sign-out must not hang on it. + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // Already unauthorized or offline: nothing left to unregister. + } + forgetIdentity() + } + + companion object { + /** Long enough for a slow Tailscale round trip, short enough for a sign-out. */ + const val DEFAULT_TIMEOUT_MILLIS = 3_000L + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/notify/Notifier.kt b/android/app/src/main/java/io/github/kahme247/ompweb/notify/Notifier.kt new file mode 100644 index 000000000..0dfb1ed95 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/notify/Notifier.kt @@ -0,0 +1,53 @@ +package io.github.kahme247.ompweb.notify + +import io.github.kahme247.ompweb.domain.NotifyEvent +import io.github.kahme247.ompweb.domain.NotifyProject + +/** + * Every system-notification operation [NotificationCenter] and the answer + * receiver need. [AndroidNotifier] posts for real; a JVM test supplies a fake + * to exercise the delivery rules (post refusal → in-app notice) without the + * NotificationManager. + */ +interface Notifier { + + fun ensureChannels() + + /** + * One live event; `false` when the app may not post (permission/channel). + * [project] is the session's project when known: the notification joins + * that project's group and the group summary is refreshed. + */ + fun post(event: NotifyEvent, project: NotifyProject?): Boolean + + /** Settings "Send test notification" without a push subscription. */ + fun postTest(): Boolean + + /** Replaces an answered dialog's notification with the result text. */ + fun postResult(tag: String, title: String, channelId: String, text: String, sessionId: String): Boolean + + fun cancelAll() + + /** + * Closes every posted notification of one session (all event types and + * results). The app calls this when the user opens that chat: what is on + * screen is read there, not in the shade. + */ + fun cancelSession(sessionId: String) + + /** Closes the notification of [tag] only while it still belongs to [requestId]. */ + fun cancelIfMatches(tag: String, requestId: String) + + /** + * A grouped child was dismissed from the shade; [tag] must be treated as + * gone even while the system's own list still contains it. + */ + fun onGroupChildDismissed(groupKey: String, tag: String) + + /** + * Rebuilds every group summary from the live notifications; a tap + * auto-cancels a child without a dismiss callback, so the app calls this + * when it comes to the foreground. + */ + fun refreshGroupSummaries() +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/AppViewModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/AppViewModel.kt new file mode 100644 index 000000000..00cda8d49 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/AppViewModel.kt @@ -0,0 +1,242 @@ +package io.github.kahme247.ompweb.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.kahme247.ompweb.AppContainer +import io.github.kahme247.ompweb.net.ApiError +import io.github.kahme247.ompweb.net.AppInfoProbe +import io.github.kahme247.ompweb.net.LoginOutcome +import io.github.kahme247.ompweb.net.ProbeResult +import io.github.kahme247.ompweb.net.ServerAddress +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +/** Which screen the app shows. */ +sealed interface AppRoute { + data object Loading : AppRoute + + /** The connect screen, optionally with the address and result of a failed re-check. */ + data class Connect( + val prefill: ServerAddress? = null, + val result: ProbeResult? = null, + ) : AppRoute + + data object Password : AppRoute + data class TooOld(val address: ServerAddress, val serverVersion: String?) : AppRoute + data object Main : AppRoute +} + +/** Why the last sign-in attempt failed; the screen owns the wording. */ +enum class PasswordError { + WrongPassword, + TooLarge, + Misconfigured, + Forbidden, + Network, + Server, +} + +/** + * Owns the onboarding state machine: connect → (password) → main. + * + * A stored origin plus a stored cookie goes straight to the main screen on + * launch — that is what makes a relaunch stay signed in; without a cookie the + * app asks for the password again (unless the server never had one). Any 401 + * for the configured origin clears the cookie and returns to the password + * screen with the origin kept; "Sign out" does the same on purpose, and + * "Change server" also forgets the origin. + */ +class AppViewModel(private val container: AppContainer) : ViewModel() { + + private val _route = MutableStateFlow(AppRoute.Loading) + val route: StateFlow = _route.asStateFlow() + + private val _signingIn = MutableStateFlow(false) + val signingIn: StateFlow = _signingIn.asStateFlow() + + private val _passwordError = MutableStateFlow(null) + val passwordError: StateFlow = _passwordError.asStateFlow() + + private var signInJob: Job? = null + + init { + viewModelScope.launch { + val snapshot = container.settings.snapshot.first { it.loaded } + _route.value = when { + snapshot.origin == null -> AppRoute.Connect() + // A stored address without a cookie is not a signed-in app; + // only a server that never asked for a password can skip it. + snapshot.needsPassword && !container.cookieJar.hasCookies() -> AppRoute.Password + else -> AppRoute.Main + } + } + viewModelScope.launch { + container.authEvents.collect { failedOrigin -> + // A late 401 from a server the user already left must not sign + // out the one that is configured now. + if (_route.value == AppRoute.Main && failedOrigin == container.settings.originNow()) { + // The unregister of the notification device still needs a + // session; a cleared cookie would make it a 401 again. + container.clearLocalUserData() + container.cookieJar.clear() + _passwordError.value = null + _route.value = AppRoute.Password + } + } + } + } + + /** The connect screen validated the address (and the server's capabilities). */ + fun acceptServer(address: ServerAddress, needsPassword: Boolean) { + viewModelScope.launch { + // Drafts, recordings and exports belong to the previous origin. + if (container.settings.originNow()?.origin != address.origin) { + container.clearLocalUserData() + } + container.settings.setOrigin(address.origin) + container.settings.setNeedsPassword(needsPassword) + _passwordError.value = null + _route.value = if (needsPassword) AppRoute.Password else AppRoute.Main + } + } + + /** The server answered, but without a usable `/api/app-info`. */ + fun reportTooOld(address: ServerAddress, serverVersion: String?) { + viewModelScope.launch { + // Same rule as acceptServer: pointing the app at another server + // must not keep that installation's cache, cookies or drafts. A + // too-old server is often reached after another one, and this path + // used to switch the origin without wiping. + if (container.settings.originNow()?.origin != address.origin) { + container.clearLocalUserData() + } + container.settings.setOrigin(address.origin) + _route.value = AppRoute.TooOld(address, serverVersion) + } + } + + fun signIn(password: String) { + if (_signingIn.value || password.isEmpty()) return + _signingIn.value = true + _passwordError.value = null + signInJob = viewModelScope.launch { + try { + when (container.api.login(password)) { + LoginOutcome.Success -> verifyCapabilitiesThenMain() + LoginOutcome.PasswordDisabled -> { + // The server does not use a password after all (the + // stored hint may be stale); remember it so "Sign out" + // does not lead into a password screen. + container.settings.setNeedsPassword(false) + _route.value = AppRoute.Main + } + LoginOutcome.WrongPassword -> _passwordError.value = PasswordError.WrongPassword + LoginOutcome.TooLarge -> _passwordError.value = PasswordError.TooLarge + } + } catch (error: ApiError) { + _passwordError.value = error.toPasswordError() + } finally { + _signingIn.value = false + } + } + } + + /** + * "Check again" on the too-old screen: after updating ompweb on the PC the + * user should not have to retype the address. + */ + fun retryServer() { + val address = (_route.value as? AppRoute.TooOld)?.address ?: return + _route.value = AppRoute.Loading + viewModelScope.launch { + val next: AppRoute = when (val probe = container.probe.probeAppInfo(address)) { + is AppInfoProbe.Supported -> AppRoute.Main + is AppInfoProbe.TooOld -> AppRoute.TooOld(address, probe.serverVersion) + AppInfoProbe.PasswordRequired -> AppRoute.Password + // Only a missing or unknown API means "too old"; a network, + // TLS or proxy problem belongs on the connect screen, where + // the user can retry and see what actually failed. + AppInfoProbe.Unreachable -> AppRoute.Connect(address, ProbeResult.UNREACHABLE) + AppInfoProbe.TlsError -> AppRoute.Connect(address, ProbeResult.TLS_ERROR) + AppInfoProbe.Forbidden -> AppRoute.Connect(address, ProbeResult.FORBIDDEN) + AppInfoProbe.Misconfigured -> AppRoute.Connect(address, ProbeResult.MISCONFIGURED) + AppInfoProbe.NotOmpweb -> AppRoute.Connect(address, ProbeResult.NOT_OMPWEB) + } + _route.value = next + } + } + + fun signOut() { + signInJob?.cancel() + _signingIn.value = false + val needsPassword = container.settings.snapshot.value.needsPassword + _passwordError.value = null + // An open server has no password to ask for; asking anyway would be a + // dead end where every input is accepted. + _route.value = if (needsPassword) AppRoute.Password else AppRoute.Main + viewModelScope.launch { + // Unregistrate first: it still needs the cookie and the origin. + // Drafts, recovered queue text and local recordings are the + // signed-in user's; they must not survive either. + container.clearLocalUserData() + container.cookieJar.clear() + } + } + + fun changeServer() { + signInJob?.cancel() + _signingIn.value = false + viewModelScope.launch { + // Same order as sign-out: the device unregister needs the old cookie. + container.clearLocalUserData() + container.cookieJar.clear() + container.settings.setOrigin(null) + _passwordError.value = null + _route.value = AppRoute.Connect() + } + } + + fun setTheme(themeId: String) { + viewModelScope.launch { container.settings.setTheme(themeId) } + } + + /** + * A password-protected server cannot answer `/api/app-info` before sign-in, + * so the "server too old" check runs once more with the session cookie. + */ + private suspend fun verifyCapabilitiesThenMain() { + val address = container.settings.originNow() + if (address == null) { + _route.value = AppRoute.Connect() + return + } + when (val probe = container.probe.probeAppInfo(address)) { + is AppInfoProbe.TooOld -> _route.value = AppRoute.TooOld(address, probe.serverVersion) + else -> _route.value = AppRoute.Main + } + } + + private fun ApiError.toPasswordError(): PasswordError = when (this) { + is ApiError.Unauthorized -> PasswordError.WrongPassword + is ApiError.Forbidden -> PasswordError.Forbidden + is ApiError.ServerMisconfigured -> PasswordError.Misconfigured + // "omp is busy" (503 session_slow): not a sign-in condition, but the + // server answered — retrying is a server-side wait, not a credentials one. + is ApiError.SessionSlow -> PasswordError.Server + // A foreign owner is not a sign-in condition either: the server + // answered, the session just belongs to another omp process. + is ApiError.SessionRunningElsewhere -> PasswordError.Server + is ApiError.Network -> PasswordError.Network + is ApiError.NotConfigured -> PasswordError.Network + is ApiError.Http -> PasswordError.Server + is ApiError.ServerResponse -> when (status) { + 401 -> PasswordError.WrongPassword + 403 -> PasswordError.Forbidden + else -> PasswordError.Server + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/OmpWebRoot.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/OmpWebRoot.kt new file mode 100644 index 000000000..daf541c69 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/OmpWebRoot.kt @@ -0,0 +1,520 @@ +package io.github.kahme247.ompweb.ui + +import android.Manifest +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.github.kahme247.ompweb.ui.theme.OmpDialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import io.github.kahme247.ompweb.AppContainer +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.chat.ChatViewModel +import io.github.kahme247.ompweb.ui.connect.ConnectScreen +import io.github.kahme247.ompweb.ui.connect.ConnectViewModel +import io.github.kahme247.ompweb.ui.hosttools.HostToolPromptHost +import io.github.kahme247.ompweb.ui.onboarding.PasswordScreen +import io.github.kahme247.ompweb.ui.notifications.NotificationBellButton +import io.github.kahme247.ompweb.ui.notifications.NotificationHistoryViewModel +import io.github.kahme247.ompweb.ui.notifications.NotificationNoticeHost +import io.github.kahme247.ompweb.ui.notifications.NotificationListPane +import io.github.kahme247.ompweb.ui.onboarding.ServerTooOldScreen +import io.github.kahme247.ompweb.ui.sessions.DirectoryPickerViewModel +import io.github.kahme247.ompweb.ui.sessions.NewSessionViewModel +import io.github.kahme247.ompweb.ui.sessions.SessionsScreen +import io.github.kahme247.ompweb.ui.sessions.SessionsViewModel +import io.github.kahme247.ompweb.ui.settings.LocalNativeSettingsDependencies +import io.github.kahme247.ompweb.ui.settings.NativeSettingsDependencies +import io.github.kahme247.ompweb.ui.settings.NativeSettingsViewModel +import io.github.kahme247.ompweb.ui.settings.SettingsAgentsHostViewModel +import io.github.kahme247.ompweb.ui.settings.SettingsHostViewModel +import io.github.kahme247.ompweb.ui.settings.SettingsProvidersHostViewModel +import io.github.kahme247.ompweb.ui.settings.SettingsScreen +import io.github.kahme247.ompweb.ui.settings.SettingsViewModel +import io.github.kahme247.ompweb.ui.settings.AndroidUpdateBanner +import io.github.kahme247.ompweb.ui.settings.UpdateNoticeBanner +import io.github.kahme247.ompweb.ui.hosttools.openExternalUrl +import io.github.kahme247.ompweb.ui.settings.agents.AgentsTab +import io.github.kahme247.ompweb.ui.settings.agents.agentsSearchEntries +import io.github.kahme247.ompweb.ui.settings.models.ProvidersModelsTab +import io.github.kahme247.ompweb.ui.settings.models.providersModelsSearchEntries +import io.github.kahme247.ompweb.ui.settings.native.NativeSettingsTab +import io.github.kahme247.ompweb.ui.settings.native.nativeSettingsSearchEntries +import io.github.kahme247.ompweb.domain.FontSizePreference +import io.github.kahme247.ompweb.domain.LOCAL_SETTINGS_SEARCH_ENTRIES +import io.github.kahme247.ompweb.domain.TouchTargetsPreference +import io.github.kahme247.ompweb.domain.settingsSearchEntries +import io.github.kahme247.ompweb.domain.UiScalePreference +import io.github.kahme247.ompweb.ui.theme.OmpWebTheme +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * Pause between the first painted content and the app's own update check, so + * the two optional checks never share the startup window with the list and the + * first transcript. + */ +private const val STARTUP_UPDATE_STAGGER_MILLIS = 1_500L + +@Composable +fun OmpWebRoot(container: AppContainer) { + val appViewModel: AppViewModel = viewModel(factory = container.appViewModelFactory) + val route by appViewModel.route.collectAsStateWithLifecycle() + val settings by container.settings.snapshot.collectAsStateWithLifecycle() + val passwordError by appViewModel.passwordError.collectAsStateWithLifecycle() + val signingIn by appViewModel.signingIn.collectAsStateWithLifecycle() + + // Every path into Main (cold start, password, passwordless) registers the + // notification device and starts the service when it is enabled. + LaunchedEffect(route) { + if (route == AppRoute.Main) container.notifications.onSignedIn() + } + + // Until DataStore has delivered the stored theme, keep the window + // background (the splash) instead of flashing the default dark palette. + if (!settings.loaded) return + + OmpWebTheme( + themeId = settings.themeId, + fontSize = FontSizePreference.fromWire(settings.fontSize), + uiScale = UiScalePreference.fromWire(settings.uiScale), + touchTargets = TouchTargetsPreference.fromWire(settings.touchTargets), + ) { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + when (val current = route) { + AppRoute.Loading -> LoadingScreen() + is AppRoute.Connect -> { + val connectViewModel: ConnectViewModel = viewModel(factory = container.connectViewModelFactory) + ConnectScreen( + viewModel = connectViewModel, + onAccepted = appViewModel::acceptServer, + onTooOld = appViewModel::reportTooOld, + initialAddress = current.prefill, + initialResult = current.result, + ) + } + AppRoute.Password -> PasswordScreen( + origin = settings.origin, + error = passwordError, + signingIn = signingIn, + onSubmit = appViewModel::signIn, + onChangeServer = appViewModel::changeServer, + ) + is AppRoute.TooOld -> ServerTooOldScreen( + address = current.address, + serverVersion = current.serverVersion, + onRetry = appViewModel::retryServer, + onChangeServer = appViewModel::changeServer, + ) + AppRoute.Main -> { + val sessionsViewModel: SessionsViewModel = viewModel(factory = container.sessionsViewModelFactory) + val newSessionViewModel: NewSessionViewModel = + viewModel(factory = container.newSessionViewModelFactory) + val directoryPickerViewModel: DirectoryPickerViewModel = + viewModel(factory = container.directoryPickerViewModelFactory) + val chatViewModel: ChatViewModel = viewModel(factory = container.chatViewModelFactory) + val notificationHistoryViewModel: NotificationHistoryViewModel = + container.notificationHistoryViewModel + val sessionsState by sessionsViewModel.state.collectAsStateWithLifecycle() + var notificationsOpen by rememberSaveable { mutableStateOf(false) } + var settingsOpen by rememberSaveable { mutableStateOf(false) } + var settingsCwd by rememberSaveable { mutableStateOf(null) } + val settingsHost: SettingsHostViewModel = + viewModel(factory = container.settingsHostViewModelFactory) + + DisposableEffect(notificationHistoryViewModel) { + container.notifications.onDelivered = notificationHistoryViewModel::onDelivered + onDispose { container.notifications.onDelivered = null } + } + + NotificationPermissionHost(container) + // `host_tool_call` frames arrive per session on the shared + // channel: the bridge must live above the chat screen so a + // consent prompt still works from the list or settings. + DisposableEffect(container.hostTools) { + container.hostTools.start() + onDispose { container.hostTools.stop() } + } + HostToolPromptHost( + controller = container.hostTools, + // The Files panel owns the viewer now: the tab is bound + // to the session that asked, so switching the chat + // cannot change which file is read. The path is passed + // through unchanged: a host tool names it relative to + // the workspace it ran in, and joining it with the cwd + // of whatever session is on screen would read the wrong + // directory (the server resolves it against its roots). + onOpenFile = { sessionId, path -> + container.filesViewModel.openTab( + path, + sourceSessionId = sessionId, + cwd = null, + ) + }, + ) + // D1: the update banner is shell content (web + // `UpdateNoticeBanner` below the chrome); it hides itself + // for null/dismissed/completed versions. + val settingsViewModel: SettingsViewModel = + viewModel(factory = container.settingsViewModelFactory) + val updateNotice by settingsViewModel.updateNotice.collectAsStateWithLifecycle() + // The app's own update notice: GitHub Releases, cached for + // six hours on a cold start and revalidated on a return to + // the foreground once the answer is older than 30 minutes. + // The shell checks once content is on screen; the container + // owns the foreground rule. It is independent of the + // connected server, so it keeps its own state. + val androidUpdate by container.androidUpdates.state.collectAsStateWithLifecycle() + val context = LocalContext.current + // Startup burst: both update checks are optional hints, so + // they wait until content is on screen (the cached list + // makes that immediate) and then run one after the other + // instead of competing with the first transcript. The + // forced checks in Settings are untouched. + var startupChecksStarted by remember { mutableStateOf(false) } + LaunchedEffect(sessionsState.loading) { + if (startupChecksStarted || sessionsState.loading) return@LaunchedEffect + startupChecksStarted = true + settingsViewModel.checkForUpdateNotices() + delay(STARTUP_UPDATE_STAGGER_MILLIS) + container.androidUpdates.check() + } + // Android keeps the process alive for days, so a cold-start + // check alone would rarely run again: every later return to + // the foreground rechecks the server's update notices, the + // same rule the container applies to the app's own update. + // The observer is only installed once the staggered start + // check is out of the way, so the first event it sees is a + // real resume; the view model throttles those to one per + // hour (the server memoizes both answers for an hour) and a + // check that reached nothing retries after a short backoff. + // The cold start itself never waits out a window. + if (startupChecksStarted) { + DisposableEffect(settingsViewModel) { + val lifecycle = ProcessLifecycleOwner.get().lifecycle + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + settingsViewModel.checkForUpdateNotices() + } + } + lifecycle.addObserver(observer) + onDispose { lifecycle.removeObserver(observer) } + } + } + // D2: the in-app notice cards (WP4 NotificationCenter) are + // the *last* layer above the shell. D1's banner is passed + // into the shell and renders inside the visible pane, below + // its top bar, so it never covers that bar. + MainShellLayers( + notices = { + NotificationNoticeHost( + notices = container.notifications.notices, + history = notificationHistoryViewModel, + onDismiss = container.notifications::dismissNotice, + onOpenSession = { sessionId -> + container.offerSessionOpen(sessionId, settings.origin?.origin) + }, + ) + }, + ) { + SessionsScreen( + appContainer = container, + viewModel = sessionsViewModel, + newSessionViewModel = newSessionViewModel, + directoryPickerViewModel = directoryPickerViewModel, + chatViewModel = chatViewModel, + themeId = settings.themeId, + onThemeSelected = appViewModel::setTheme, + onSignOut = appViewModel::signOut, + onChangeServer = appViewModel::changeServer, + notificationBell = { + NotificationBellButton( + vm = notificationHistoryViewModel, + onClick = { notificationsOpen = true }, + ) + }, + onOpenSettings = { cwd -> + settingsCwd = cwd + settingsOpen = true + }, + overlayOpen = notificationsOpen || settingsOpen, + restoreSessionId = settings.lastSessionId, + restoreNewSessionCwd = settings.lastNewSessionCwd, + // D1: the update banner is content, not an overlay: it + // sits below the active pane's top bar, so the bar + // stays visible, tappable and in the a11y tree. + updateBanner = { + Column { + UpdateNoticeBanner( + state = updateNotice, + onOpenSettings = { + settingsCwd = null + settingsOpen = true + }, + // The state the reader saw is the version that + // is dismissed, even if a check landed between + // the render and the tap. + onDismiss = { dismissed -> + settingsViewModel.dismissUpdateNotice(dismissed) + }, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), + ) + AndroidUpdateBanner( + notice = androidUpdate.notice, + install = androidUpdate.install, + onInstall = { container.androidUpdates.install() }, + onOpenInstallSettings = { + container.androidUpdates.openInstallPermissionSettings() + }, + onConfirmInstall = { container.androidUpdates.confirmInstall() }, + onCancelInstall = { container.androidUpdates.cancelInstall() }, + onOpenRelease = { notice -> + notice.releaseUrl?.let { url -> + openExternalUrl(context, url) + } + }, + onDismiss = { notice -> + container.androidUpdates.dismiss(notice.availableVersion) + }, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), + ) + } + }, + ) + } + if (notificationsOpen) { + NotificationPanelOverlay( + historyViewModel = notificationHistoryViewModel, + onOpenSession = { sessionId -> + notificationsOpen = false + container.offerSessionOpen(sessionId, settings.origin?.origin) + }, + onClose = { notificationsOpen = false }, + ) + } + if (settingsOpen) { + // Created on first open, retained across rotation in the + // settings store, dropped on close/sign-out (no API keys + // left in the heap). + val providersModelsHost: SettingsProvidersHostViewModel = viewModel( + viewModelStoreOwner = settingsHost.owner, + factory = container.settingsProvidersHostFactory, + ) + val agentsHost: SettingsAgentsHostViewModel = viewModel( + viewModelStoreOwner = settingsHost.owner, + factory = container.settingsAgentsHostFactory, + ) + val nativeSettingsViewModel: NativeSettingsViewModel = viewModel( + viewModelStoreOwner = settingsHost.owner, + factory = container.nativeSettingsViewModelFactory, + ) + val closeSettings = { + settingsOpen = false + settingsHost.clear() + } + OmpDialog( + onDismissRequest = closeSettings, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + CompositionLocalProvider( + LocalNativeSettingsDependencies provides NativeSettingsDependencies( + viewModel = nativeSettingsViewModel, + sessionId = sessionsState.selectedSessionId, + ), + ) { + SettingsScreen( + container = container, + cwd = settingsCwd, + viewModelStoreOwner = settingsHost.owner, + searchEntries = settingsSearchEntries( + LOCAL_SETTINGS_SEARCH_ENTRIES, + nativeSettingsSearchEntries(), + providersModelsSearchEntries(), + agentsSearchEntries(), + ), + onClose = closeSettings, + onSignOut = appViewModel::signOut, + onChangeServer = appViewModel::changeServer, + onThemeSelected = appViewModel::setTheme, + nativeContent = { category, cwdForTab, highlight -> + NativeSettingsTab(category, cwdForTab, highlight) + }, + providersContent = { highlight -> + ProvidersModelsTab(providersModelsHost.viewModel, highlight) + }, + agentsContent = { cwdForTab, highlight -> + AgentsTab(agentsHost.viewModel, cwdForTab, highlight) + }, + ) + } + } + } + } + } + } + } +} + +/** + * Full-screen notification history above the session shell. The dialog + * window keeps touches away from the list below; Settings is its own surface. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun NotificationPanelOverlay( + historyViewModel: NotificationHistoryViewModel, + onOpenSession: (String) -> Unit, + onClose: () -> Unit, +) { + OmpDialog( + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + Scaffold( + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = onClose) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.nav_back), + ) + } + }, + ) + }, + ) { innerPadding -> + NotificationListPane( + vm = historyViewModel, + onOpenSession = onOpenSession, + modifier = Modifier.padding(innerPadding), + ) + } + } + } +} + +/** + * The only place that may request POST_NOTIFICATIONS: a launcher needs the + * activity, so the settings screen emits `NotificationCenter.permissionRequests` + * and this host performs it. The first sign-in shows a short explanation once; + * after that the settings row reports the state and deep-links to the system. + */ +@Composable +private fun NotificationPermissionHost(container: AppContainer) { + val snapshot by container.settings.snapshot.collectAsStateWithLifecycle() + val scope = rememberCoroutineScope() + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { + container.notifications.refreshService() + } + LaunchedEffect(container) { + container.notifications.permissionRequests.collect { + if (container.notifications.permissionStatus().canRequestPostNotifications) { + launcher.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + container.notifications.openSystemNotificationSettings() + } + } + } + + var showOnboarding by remember { mutableStateOf(false) } + LaunchedEffect(snapshot.loaded, snapshot.notifyPermissionPrompted) { + if ( + snapshot.loaded && + !snapshot.notifyPermissionPrompted && + container.notifications.permissionStatus().canRequestPostNotifications + ) { + showOnboarding = true + } + } + if (!showOnboarding) return + + fun finish(allow: Boolean) { + showOnboarding = false + scope.launch { container.settings.setNotifyPermissionPrompted(true) } + if (allow) launcher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + + OmpAlertDialog( + onDismissRequest = { finish(false) }, + title = { Text(stringResource(R.string.notify_onboarding_title)) }, + text = { Text(stringResource(R.string.notify_onboarding_text)) }, + confirmButton = { + TextButton(onClick = { finish(true) }) { + Text(stringResource(R.string.notify_onboarding_allow)) + } + }, + dismissButton = { + TextButton(onClick = { finish(false) }) { + Text(stringResource(R.string.notify_onboarding_later)) + } + }, + ) +} + +@Composable +private fun LoadingScreen() { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } +} + +/** + * The signed-in shell's draw order: the session shell (list, chat, files + * panel) first, the in-app notice cards last, on top of it. + * + * Compose paints siblings in declaration order and hit-tests them in reverse, + * so a notice host composed *before* the opaque shell panes is both invisible + * and untappable (B1/A12). Keeping the order in one composable also makes it + * testable: the tests render this seam and click through the top layer. + */ +@Composable +internal fun MainShellLayers( + notices: @Composable () -> Unit, + shell: @Composable BoxScope.() -> Unit, +) { + Box(modifier = Modifier.fillMaxSize()) { + shell() + notices() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ActivityVisuals.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ActivityVisuals.kt new file mode 100644 index 000000000..611d15058 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ActivityVisuals.kt @@ -0,0 +1,174 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.Build +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Place +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.currentStateAsState +import io.github.kahme247.ompweb.chat.TranscriptFormat +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import kotlinx.coroutines.delay + +/** + * The pulse that marks a running activity (tool card, folded process row, + * subagent chip, composer status line). A static dot when the platform asks + * for reduced motion. + */ +@Composable +internal fun LivePulseDot( + modifier: Modifier = Modifier, + size: Dp = 7.dp, + color: Color = LocalOmpColors.current.accent, + animate: Boolean = LocalChatMotion.current, +) { + val alpha = if (animate) { + val transition = rememberInfiniteTransition(label = "live-pulse") + val pulse by transition.animateFloat( + initialValue = 0.3f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 900, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "live-pulse-alpha", + ) + pulse + } else { + 1f + } + Box( + modifier + .size(size) + .background(color.copy(alpha = alpha), CircleShape), + ) +} + +/** + * The tool family's icon and tint, mirroring the web `ToolCategoryIcon`: the + * same nine families, each in the palette token the web uses for it. Only + * `material-icons-core` ships with the app, so the nearest core glyph stands + * in for the web's lucide icon. + */ +@Composable +internal fun ToolCategoryIcon( + toolName: String, + modifier: Modifier = Modifier, + size: Dp = 13.dp, +) { + val colors = LocalOmpColors.current + val category = getToolCategory(toolName) + val (icon, tint) = when (category) { + ToolCategory.Read -> Icons.AutoMirrored.Filled.List to colors.statusRenamed + ToolCategory.Search -> Icons.Filled.Search to colors.accent + ToolCategory.Edit -> Icons.Filled.Edit to colors.statusModified + ToolCategory.Terminal -> Icons.Filled.PlayArrow to colors.statusSuccess + ToolCategory.Todo -> Icons.Filled.CheckCircle to colors.accent + ToolCategory.Task -> Icons.Filled.Person to (colors.secondaryAccent ?: colors.accent) + ToolCategory.Code -> Icons.Filled.Settings to colors.accent + ToolCategory.Web -> Icons.Filled.Place to (colors.secondaryAccent ?: colors.accent) + ToolCategory.Hub -> Icons.Filled.Email to (colors.secondaryAccent ?: colors.accent) + ToolCategory.Other -> Icons.Filled.Build to colors.textMuted + } + ToolIcon(icon = icon, tint = tint, modifier = modifier, size = size) +} + +@Composable +private fun ToolIcon(icon: ImageVector, tint: Color, modifier: Modifier, size: Dp) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = modifier.size(size), + tint = tint, + ) +} + +/** + * Elapsed time of a running activity, ticking once a second while [running]. + * Null while it is not running, so a settled row uses its recorded duration. + * The ticker only runs while the screen is STARTED: a card that stays composed + * in the background no longer wakes the compositor every second. + */ +@Composable +private fun rememberElapsedMs(startedAt: Long?, running: Boolean): Long? { + if (!running || startedAt == null) return null + val clock = LocalChatClock.current + val started = LocalLifecycleOwner.current.lifecycle + .currentStateAsState() + .value + .isAtLeast(Lifecycle.State.STARTED) + var now by remember(startedAt, clock) { mutableLongStateOf(clock()) } + LaunchedEffect(startedAt, clock, started) { + if (!started) return@LaunchedEffect + while (true) { + now = clock() + delay(1_000) + } + } + return (now - startedAt).coerceAtLeast(0) +} + +/** + * A duration that ticks while [running] and shows [settledMs] once settled. + * The tick is read inside this composable, so only this text recomposes every + * second — the tool card, fold or footer around it stays skippable. + */ +@Composable +internal fun ElapsedText( + startedAt: Long?, + running: Boolean, + settledMs: Long? = null, + modifier: Modifier = Modifier, + color: Color = LocalOmpColors.current.textDim, + fontSize: TextUnit = 10.sp, + textAlign: TextAlign? = null, + prefix: String = "", +) { + val elapsed = rememberElapsedMs(startedAt, running) + val text = when { + running -> elapsed?.let(TranscriptFormat::formatElapsedShort) + else -> settledMs?.let(TranscriptFormat::formatElapsedShort) + } ?: return + Text( + text = prefix + text, + modifier = modifier, + color = color, + fontSize = fontSize, + textAlign = textAlign, + maxLines = 1, + ) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/BranchNavigatorDialog.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/BranchNavigatorDialog.kt new file mode 100644 index 000000000..c65918bba --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/BranchNavigatorDialog.kt @@ -0,0 +1,280 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.chat.BranchRow +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily +import io.github.kahme247.ompweb.ui.theme.PickerDialogKey +import io.github.kahme247.ompweb.ui.theme.pickerDialogKeyFor +import io.github.kahme247.ompweb.ui.theme.pickerDialogTarget +import kotlinx.coroutines.launch + +/** Branch picker (web `BranchNavigator`): one selectable row per branch node. */ +@Composable +internal fun BranchNavigatorDialog( + state: BranchNavigatorState, + onSelect: (String) -> Unit, + onDismiss: () -> Unit, +) { + OmpAlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.chat_branches_title), fontSize = 17.sp) }, + text = { + BranchNavigatorContent( + state = state, + onSelect = onSelect, + onDismiss = onDismiss, + ) + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_close)) } + }, + ) +} + +/** + * The branch list with the web navigator's keyboard contract (A21): + * Up/Down/Home/End move the highlight (starting on the active branch), + * Enter/Space select it and Escape cancels. Focus leaving the picker closes it + * without switching, mirroring the web dropdown's blur/outside-click close. + */ +@Composable +internal fun BranchNavigatorContent( + state: BranchNavigatorState, + onSelect: (String) -> Unit, + onDismiss: () -> Unit, +) { + val colors = LocalOmpColors.current + val flat = remember(state.rows) { flatten(state.rows) } + val initialIndex = remember(flat) { flat.indexOfFirst { it.first.active }.coerceAtLeast(0) } + val listState = rememberLazyListState(initialFirstVisibleItemIndex = initialIndex) + val rootFocus = remember { FocusRequester() } + val scope = rememberCoroutineScope() + var hadFocus by remember { mutableStateOf(false) } + // A row that took focus (Tab/click) wins over the arrow-driven highlight. + var activeIndex by remember { mutableIntStateOf(initialIndex) } + var focusedIndex by remember { mutableStateOf(null) } + + fun choose(index: Int) { + val id = flat.getOrNull(index)?.first?.selectId ?: return + onSelect(id) + // The web navigator closes on a pick (`BranchNavigator.tsx: + // onLeafChange(id); setOpenInternal(false)`); without this the dialog + // stayed open over the reloaded transcript. + onDismiss() + } + + fun moveTo(index: Int) { + activeIndex = index + scope.launch { listState.scrollToItem(index) } + } + + LaunchedEffect(Unit) { rootFocus.requestFocus() } + + Column( + modifier = Modifier + .fillMaxWidth() + .testTag("branch-picker") + .focusRequester(rootFocus) + .onFocusChanged { focus -> + if (focus.hasFocus) hadFocus = true else if (hadFocus) onDismiss() + } + // Preview: the list would otherwise consume the arrows for scrolling. + .onPreviewKeyEvent { event -> + if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false + val key = pickerDialogKeyFor(event.key) + when (key) { + PickerDialogKey.CHOOSE -> { + if (flat.isEmpty()) return@onPreviewKeyEvent false + choose(focusedIndex ?: activeIndex) + true + } + PickerDialogKey.CANCEL -> { + onDismiss() + true + } + null -> false + else -> { + val target = pickerDialogTarget(activeIndex, key, flat.size) + if (target == null) false else { + moveTo(target) + true + } + } + } + } + .focusable(), + ) { + if (flat.isEmpty()) { + Text(stringResource(R.string.chat_no_branches), color = colors.textDim, fontSize = 12.sp) + } else { + LazyColumn( + state = listState, + modifier = Modifier.heightIn(max = 420.dp).fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + itemsIndexed( + items = flat, + key = { _, entry -> entry.first.selectId + "@" + entry.first.depth + ":" + entry.first.label }, + ) { index, entry -> + val (row, parentLines) = entry + BranchNodeRow( + row = row, + parentLines = parentLines, + modifier = Modifier.testTag("branch-row-$index"), + onFocus = { focusedIndex = index }, + onChoose = { choose(index) }, + ) + } + } + } + } +} + +@Composable +private fun BranchNodeRow( + row: BranchRow, + parentLines: List, + modifier: Modifier = Modifier, + onFocus: () -> Unit, + onChoose: () -> Unit, +) { + val colors = LocalOmpColors.current + Row( + modifier = modifier + .fillMaxWidth() + .onFocusChanged { if (it.isFocused) onFocus() } + .background( + if (row.active) colors.selected else androidx.compose.ui.graphics.Color.Transparent, + RoundedCornerShape(5.dp), + ) + .clickable(onClick = onChoose) + .padding(horizontal = 6.dp, vertical = 5.dp), + verticalAlignment = Alignment.Top, + ) { + parentLines.forEachIndexed { depth, drawLine -> + Box(Modifier.width(12.dp)) { + if (drawLine) { + Box( + Modifier + .padding(start = 6.dp) + .width(1.dp) + .heightIn(min = 20.dp) + .background(colors.border), + ) + } + if (depth == parentLines.lastIndex) { + Text( + text = if (row.isLast) "└" else "├", + color = colors.border, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + ) + } + } + } + Box(Modifier.width(6.dp)) + Column(Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + row.role?.let { role -> + Text( + text = role, + color = colors.accent, + fontSize = 9.5.sp, + fontFamily = OmpMonoFontFamily, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .background(colors.backgroundSubtle, RoundedCornerShape(3.dp)) + .padding(horizontal = 5.dp, vertical = 1.dp), + ) + Spacer(Modifier.width(5.dp)) + } + Text( + text = row.label.ifBlank { "(empty)" }, + color = if (row.active) colors.text else colors.textMuted, + fontSize = 11.5.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (row.active) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = stringResource(R.string.chat_active_branch), + modifier = Modifier.size(14.dp), + tint = colors.accent, + ) + } + } + if (row.skipped > 0) { + Text( + text = stringResource(R.string.chat_branch_more, row.skipped), + color = colors.textDim, + fontSize = 9.5.sp, + modifier = Modifier.padding(start = 2.dp, top = 1.dp), + ) + } + } + } +} + +/** Depth-first flattening with the ancestor-line flags each row draws. */ +private fun flatten(rows: List): List>> { + val out = ArrayList>>() + fun walk(nodes: List, parentLines: List) { + for (node in nodes) { + out += node to parentLines + walk(node.children, parentLines + !node.isLast) + } + } + walk(rows, emptyList()) + return out +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatLayout.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatLayout.kt new file mode 100644 index 000000000..5720a7d7c --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatLayout.kt @@ -0,0 +1,145 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * How the chat splits its height between transcript and composer for one + * measured pane. Computed by [ChatLayout.plan] *before* either side is + * measured, because both sides are children of the same column: whichever is + * measured first would otherwise eat the other's space. The composer is the + * bounded one (it scrolls its secondary rows), the transcript keeps the rest. + */ +internal data class ChatLayoutPlan( + /** Upper bound of the composer block (panels excluded — they are hidden or above them at all). */ + val composerMaxHeight: Dp, + val textFieldMaxHeight: Dp, + val textFieldMaxLines: Int, + /** The attached composer panels (todo / subagents / btw) are out while typing. */ + val hidePanels: Boolean, + /** The model/thinking/tools row under the field is out while typing. */ + val hideControlsRow: Boolean, +) + +/** + * The height budget of the chat surface with the keyboard open (user feedback + * 25.10.: the transcript shrank to four lines while typing). + * + * The rule is a fraction, not a fixed dp floor, so it holds at font scale 2.0 + * as well: the transcript keeps at least [TRANSCRIPT_MIN_FRACTION] of the pane + * the IME left over, the composer takes at most the rest, and the text field + * itself is capped at [TEXT_FIELD_MAX_LINES] lines / [TEXT_FIELD_MAX_FRACTION] + * of that space — minus the rows that must stay visible ([COMPOSER_FIXED_ROWS]) + * — and scrolls internally (the caret stays in view because `BasicTextField` + * scrolls to it). + * + * A wide pane (tablet / unfolded, >= [WIDE_SCREEN_DP] dp) that still has at + * least [WIDE_KEYBOARD_HEADROOM] dp left above the keyboard keeps the full + * composer: nothing is hidden when there is room for it. + */ +internal object ChatLayout { + + /** + * What the transcript needs to stay readable. Used as a fixed floor when + * the keyboard is closed (the composer only grows when the field grows) + * and replaced by [TRANSCRIPT_MIN_FRACTION] while the keyboard is open. + */ + val TRANSCRIPT_FLOOR = 120.dp + + /** The composer's own floor while the keyboard is closed. */ + val COMPOSER_FLOOR = 140.dp + + /** Below this the field would show less than its placeholder line. */ + val TEXT_FIELD_MIN = 40.dp + + /** Five to six lines: enough to re-read a prompt, small enough to keep the chat. */ + const val TEXT_FIELD_MAX_LINES = 6 + + /** Line height of the composer field (`14.sp` text, `20.sp` line height). */ + const val FIELD_LINE_HEIGHT_SP = 20f + + /** Share of the (IME-reduced) pane the transcript keeps while typing. */ + const val TRANSCRIPT_MIN_FRACTION = 0.45f + + /** Hard cap of the field relative to the (IME-reduced) pane. */ + const val TEXT_FIELD_MAX_FRACTION = 0.35f + + /** `compact` breakpoint used across the shell. */ + const val WIDE_SCREEN_DP = 600 + + /** + * Space above the keyboard that still fits the full composer on a wide + * pane. Below it the tablet behaves like the phone (hide, cap, scroll). + */ + val WIDE_KEYBOARD_HEADROOM = 480.dp + + /** + * The status line under the composer ([io.github.kahme247.ompweb.ui.chat.ChatFooter]) + * is not part of the composer's own cap, so the transcript's share is only + * honest when the plan reserves it: one `11.sp` line plus its padding, at + * the current font scale. + */ + const val CHROME_LINE_HEIGHT_SP = 11f + val CHROME_PADDING = 10.dp + + /** + * The composer box scrolls (`ChatScreen`: `heightIn(max).verticalScroll`), + * so a field cap that ignores the fixed rows around it would push the send + * row below the visible slice on a small pane / large font. These rows do + * not shrink and therefore belong to the budget first: + * + * - the minimize button above the field: a 48 dp touch target plus its + * 2 dp vertical padding (`MinimizeComposerButton`); + * - the composer surface's own 6 dp top/bottom padding; + * - the send row under the field: 4 dp top padding plus its 48 dp touch + * target (the speaker/add buttons set the row height, not the label). + */ + val MINIMIZE_ROW = 52.dp + val COMPOSER_SURFACE_PADDING = 12.dp + val SEND_ROW = 52.dp + + /** Rows of the composer box that must stay visible above and below the field. */ + val COMPOSER_FIXED_ROWS = MINIMIZE_ROW + COMPOSER_SURFACE_PADDING + SEND_ROW + + fun plan( + availableHeight: Dp, + imeVisible: Boolean, + wide: Boolean, + fontScale: Float, + /** + * Height of fixed chrome above the transcript that the plan does not + * own (the in-chat search bar). Reserved from the composer's share so + * the 45 % transcript promise also holds with the search open. + */ + reservedHeight: Dp = 0.dp, + ): ChatLayoutPlan { + val available = if (availableHeight.value.isNaN()) 0.dp else availableHeight.coerceAtLeast(0.dp) + val reserved = if (reservedHeight.value.isNaN()) 0.dp else reservedHeight.coerceAtLeast(0.dp) + val keyboardTight = imeVisible && !(wide && available >= WIDE_KEYBOARD_HEADROOM) + val lineHeight = (FIELD_LINE_HEIGHT_SP * fontScale.coerceAtLeast(0.5f)).dp + val chrome = (CHROME_LINE_HEIGHT_SP * fontScale.coerceAtLeast(0.5f)).dp + CHROME_PADDING + val composerMax = if (keyboardTight) { + (available * (1f - TRANSCRIPT_MIN_FRACTION) - chrome - reserved).coerceAtLeast(0.dp) + } else { + (available - TRANSCRIPT_FLOOR - reserved) + .coerceIn(minOf(COMPOSER_FLOOR, available), available) + } + // The send row and the minimize button never scroll out of the box: + // the field only gets what is left after them (Bug 13/#7). The 40 dp + // floor wins only on panes too small for both; there the box still has + // to scroll (ComposerBar cannot render a shorter field). + val fieldRoom = (composerMax - COMPOSER_FIXED_ROWS).coerceAtLeast(0.dp) + val fieldCap = minOf(lineHeight * TEXT_FIELD_MAX_LINES, available * TEXT_FIELD_MAX_FRACTION, fieldRoom) + .coerceAtLeast(TEXT_FIELD_MIN) + .coerceAtMost(available.coerceAtLeast(TEXT_FIELD_MIN)) + val lines = (fieldCap.value / lineHeight.value).toInt() + .coerceIn(1, TEXT_FIELD_MAX_LINES) + return ChatLayoutPlan( + composerMaxHeight = composerMax, + textFieldMaxHeight = fieldCap, + textFieldMaxLines = lines, + hidePanels = keyboardTight, + hideControlsRow = keyboardTight, + ) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatMinimap.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatMinimap.kt new file mode 100644 index 000000000..086960b53 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatMinimap.kt @@ -0,0 +1,130 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.drag +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import io.github.kahme247.ompweb.chat.TranscriptRow +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import kotlin.math.roundToInt +import kotlinx.coroutines.launch + +/** + * Fast-scroll rail (web `ChatMinimap`): a proportional thumb plus one tick per + * user turn. Dragging jumps straight to the matching row, so long sessions can + * be scrubbed without loading every earlier page. + */ +@Composable +internal fun ChatScrollRail( + listState: LazyListState, + totalItems: Int, + userTurns: List, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + val scope = rememberCoroutineScope() + val density = LocalDensity.current + val trackWidth = with(density) { 3.dp.toPx() } + val thumbMin = with(density) { 24.dp.toPx() } + + Box( + modifier = modifier + .width(16.dp) + .fillMaxHeight() + .pointerInput(totalItems) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + fun jump(y: Float) { + if (totalItems <= 1) return + val fraction = (y / size.height.toFloat()).coerceIn(0f, 1f) + val target = (fraction * (totalItems - 1)).roundToInt() + scope.launch { listState.scrollToItem(target) } + } + jump(down.position.y) + down.consume() + drag(down.id) { change -> + jump(change.position.y) + change.consume() + } + } + }, + ) { + val layout = listState.layoutInfo + val visible = layout.visibleItemsInfo + val first = visible.firstOrNull()?.index + val last = visible.lastOrNull()?.index + val drawState = remember(layout, totalItems) { + if (first == null || last == null || totalItems <= 1) { + null + } else { + Triple(first, last, totalItems) + } + } + Canvas(modifier = Modifier.fillMaxHeight().width(16.dp)) { + if (totalItems <= 1) return@Canvas + val height = size.height + val centerX = size.width / 2f + drawRoundRect( + color = colors.border, + topLeft = Offset(centerX - trackWidth / 2f, 0f), + size = Size(trackWidth, height), + cornerRadius = CornerRadius(trackWidth / 2f), + ) + userTurns.forEach { index -> + val y = (index.toFloat() / (totalItems - 1)) * height + drawCircle( + color = colors.textDim.copy(alpha = 0.65f), + radius = 1.6.dp.toPx(), + center = Offset(centerX, y), + ) + } + if (drawState != null) { + val (firstIndex, lastIndex, total) = drawState + val visibleCount = (lastIndex - firstIndex + 1).coerceAtLeast(1) + val thumbHeight = (height * visibleCount / total) + .coerceAtLeast(thumbMin) + .coerceAtMost(height) + val maxTop = height - thumbHeight + val top = if (total <= visibleCount) { + 0f + } else { + (firstIndex.toFloat() / (total - visibleCount)) * maxTop + } + drawRoundRect( + color = colors.accent.copy(alpha = 0.8f), + topLeft = Offset(centerX - 2.dp.toPx(), top), + size = Size(4.dp.toPx(), thumbHeight), + cornerRadius = CornerRadius(2.dp.toPx()), + ) + } + } + } +} + +/** Rendered LazyColumn indices of every row that starts a user turn. */ +internal fun userTurnIndices(rows: List, rowsToItem: Int = 1): List = + rows.mapIndexedNotNull { index, row -> + when (row) { + is TranscriptRow.Message -> if (row.message is io.github.kahme247.ompweb.chat.ChatMessage.User) { + index + rowsToItem + } else { + null + } + + is TranscriptRow.Group -> index + rowsToItem + } + } diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatRows.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatRows.kt new file mode 100644 index 000000000..8589ab2c2 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatRows.kt @@ -0,0 +1,2156 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.chat.AssistantBlock +import io.github.kahme247.ompweb.chat.ChatDisplayPrefs +import io.github.kahme247.ompweb.chat.ChatMessage +import io.github.kahme247.ompweb.chat.ChatState +import io.github.kahme247.ompweb.chat.formatGenerationRate +import io.github.kahme247.ompweb.chat.HeaderSlot +import io.github.kahme247.ompweb.chat.MOUNT_NOTICE +import io.github.kahme247.ompweb.chat.messageSpeed +import io.github.kahme247.ompweb.chat.PASSIVE_TOOL_CONTEXT +import io.github.kahme247.ompweb.chat.Speaker +import io.github.kahme247.ompweb.chat.SpeakerRole +import io.github.kahme247.ompweb.chat.TranscriptFormat +import io.github.kahme247.ompweb.chat.TurnSegment +import io.github.kahme247.ompweb.chat.TranscriptRow +import io.github.kahme247.ompweb.domain.formatCompactNumber +import io.github.kahme247.ompweb.domain.formatCost +import io.github.kahme247.ompweb.domain.formatDuration +import io.github.kahme247.ompweb.domain.formatTokens +import io.github.kahme247.ompweb.domain.shortModel +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.render.DiffView +import io.github.kahme247.ompweb.ui.render.GithubRepoRef +import io.github.kahme247.ompweb.ui.render.LinkTarget +import io.github.kahme247.ompweb.ui.render.MarkdownText +import io.github.kahme247.ompweb.ui.render.RemoteImage +import io.github.kahme247.ompweb.ui.render.ToolDiff +import io.github.kahme247.ompweb.ui.render.copyTextToClipboard +import io.github.kahme247.ompweb.ui.render.formatMessageSize +import io.github.kahme247.ompweb.ui.render.imageSourceFor +import io.github.kahme247.ompweb.ui.render.isLargeMessage +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import io.github.kahme247.ompweb.ui.theme.OmpCardRadius +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily +import kotlin.math.roundToInt +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull + +private const val RAW_VIEW_THRESHOLD = 12_000 + +/** Actions a transcript row can trigger (all owned by the chat screen/view model). */ +internal class ChatRowCallbacks( + val onLink: (LinkTarget) -> Unit, + val onFork: (String, Boolean) -> Unit, + val onLoadThinking: (String?, Int) -> Unit, + val onToggleSpeech: (String?, String, String) -> Unit, + val onStopSpeech: () -> Unit, + val onLoadBashOutput: (String) -> Unit, + val onDownloadBashOutput: (String) -> Unit, + /** "Edit from here": branch back to the previous assistant answer and prefill. */ + val onEditFromHere: (prevAssistantEntryId: String, text: String) -> Unit, +) + +/** One rendered LazyColumn item of the transcript. */ +@Composable +internal fun ChatRow( + row: TranscriptRow, + messages: List, + state: ChatUiState, + liveTail: Boolean, + indexByKey: Map, + callbacks: ChatRowCallbacks, + /** + * The transcript's last group while its turn still runs: its fold has to + * show live activity instead of a static summary. + */ + runningTurn: Boolean = false, + /** The in-chat search marks the rows its query matched. */ + highlight: ChatRowHighlight = ChatRowHighlight.None, + /** + * Speaker headers and day separators keyed by header block (see + * [TranscriptFormat.headerBlocks]): `row.key` for a row's anchor and + * `row.key + ":assistant"` for a turn's folded assistant block. + */ + headers: Map = emptyMap(), + /** + * Day separators keyed by the element that draws one ([TranscriptFormat + * .daySeparators]): row keys, the group's assistant header key and turn + * segment keys. A day change inside one turn is an own element, so it is + * never lost. + */ + dayBreaks: Map = emptyMap(), +) { + if (highlight == ChatRowHighlight.None) { + ChatRowContent(row, messages, state, liveTail, indexByKey, callbacks, runningTurn, headers, dayBreaks) + return + } + val colors = LocalOmpColors.current + val shape = RoundedCornerShape(6.dp) + val tint = if (highlight == ChatRowHighlight.Active) 0.16f else 0.08f + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 1.dp) + .background(colors.accent.copy(alpha = tint), shape) + .then( + if (highlight == ChatRowHighlight.Active) { + Modifier.border(1.dp, colors.accent.copy(alpha = 0.45f), shape) + } else { + Modifier + }, + ) + .padding(horizontal = 3.dp, vertical = 2.dp), + ) { + ChatRowContent(row, messages, state, liveTail, indexByKey, callbacks, runningTurn, headers, dayBreaks) + } +} + +@Composable +private fun ChatRowContent( + row: TranscriptRow, + messages: List, + state: ChatUiState, + liveTail: Boolean, + indexByKey: Map, + callbacks: ChatRowCallbacks, + runningTurn: Boolean, + headers: Map, + dayBreaks: Map, +) { + when (row) { + is TranscriptRow.Message -> { + val index = indexByKey[row.message.key] ?: -1 + MessageRow( + message = row.message, + state = state, + forkTarget = state.forkTargets.getOrNull(index), + header = headers[row.key], + streaming = false, + prevAssistantEntryId = previousAssistantEntryId(messages, index), + callbacks = callbacks, + dayBreak = dayBreaks[row.key], + ) + } + + is TranscriptRow.Group -> { + if (liveTail) { + // Flat live tail: same messages the fold would contain, so the + // running turn stays readable while its activity streams in. + // Its headers come from the group's own blocks, so the tail + // shows the same speaker runs as the folded view; the header + // hangs on the first assistant message that really renders, + // and a day change inside the tail draws its own separator. + val firstAssistantIndex = TranscriptFormat.firstVisibleAssistantIndex( + messages = messages, + startIndex = row.anchorIndex, + endIndex = row.endIndex, + hideThinking = state.preferences.hideThinking, + ) + val tailBreaks = remember(messages, row, state.preferences.hideThinking) { + TranscriptFormat.messageDaySeparators( + messages = messages, + startIndex = row.anchorIndex, + endIndex = row.endIndex, + hideThinking = state.preferences.hideThinking, + ) + } + for (index in row.anchorIndex until row.endIndex) { + val message = messages[index] + if (message is ChatMessage.ToolResult) continue + if (message is ChatMessage.Custom && + (message.customType == MOUNT_NOTICE || message.customType == PASSIVE_TOOL_CONTEXT) + ) { + continue + } + val header = when { + index == row.anchorIndex -> headers[row.key] + index == firstAssistantIndex -> headers[row.key + TranscriptFormat.ASSISTANT_HEADER_SUFFIX] + else -> null + } + MessageRow( + message = message, + state = state, + forkTarget = state.forkTargets.getOrNull(index), + header = header, + streaming = false, + prevAssistantEntryId = previousAssistantEntryId(messages, index), + callbacks = callbacks, + dayBreak = if (index == row.anchorIndex) { + dayBreaks[row.key] + } else { + tailBreaks[message.key] + }, + ) + } + return + } + val anchorIndex = row.anchorIndex + val anchor = row.anchor + MessageRow( + message = anchor, + state = state, + forkTarget = state.forkTargets.getOrNull(anchorIndex), + header = headers[row.key], + streaming = false, + prevAssistantEntryId = previousAssistantEntryId(messages, anchorIndex), + callbacks = callbacks, + dayBreak = dayBreaks[row.key], + ) + var attached = false + val lastActivity = row.segments.indexOfLast { it is TurnSegment.Activity } + val live = if (runningTurn) liveActivity(state.engine) else null + // The last timestamp of the turn: the fold shows the settled + // duration once the turn is over (nothing ticks then). + val finishedAt = row.segments + .flatMap { segment -> + when (segment) { + is TurnSegment.Text -> listOfNotNull(segment.message.timestamp) + is TurnSegment.Activity -> segment.pieces.mapNotNull { it.message.timestamp } + } + } + .maxOrNull() + row.segments.forEachIndexed { segmentIndex, segment -> + // One header for the whole assistant turn, above its first + // segment (text or the folded activity). A day change is drawn + // above that header, where the change really starts. + if (segmentIndex == 0) { + val segmentKey = when (segment) { + is TurnSegment.Text -> segment.key + is TurnSegment.Activity -> segment.key + } + val headerBreak = dayBreaks[row.key + TranscriptFormat.ASSISTANT_HEADER_SUFFIX] + ?: dayBreaks[segmentKey] + if (headerBreak != null) DaySeparatorRow(headerBreak) + BlockHeader(headers[row.key + TranscriptFormat.ASSISTANT_HEADER_SUFFIX]) + } + when (segment) { + is TurnSegment.Text -> { + val message = segment.message + val copy = message.copy( + blocks = segment.blocks, + durationMs = if (segment.last) message.durationMs else null, + usageOutput = if (segment.last) message.usageOutput else null, + ttftMs = if (segment.last) message.ttftMs else null, + completedAt = if (segment.last) message.completedAt else null, + errorMessage = if (segment.last) message.errorMessage else null, + ) + MessageRow( + message = copy, + state = state, + forkTarget = if (attached) null else state.forkTargets.getOrNull(segment.messageIndex), + header = null, + streaming = false, + callbacks = callbacks, + dayBreak = if (segmentIndex == 0) null else dayBreaks[segment.key], + ) + attached = true + } + + is TurnSegment.Activity -> { + if (segmentIndex != 0) { + dayBreaks[segment.key]?.let { DaySeparatorRow(it) } + } + ProcessDetailsGroup( + key = segment.key, + messageCount = segment.stepCount, + toolCallCount = segment.toolCallCount, + live = if (segmentIndex == lastActivity) live else null, + startedAt = row.anchor.timestamp, + finishedAt = finishedAt, + ) { + segment.pieces.forEachIndexed { pieceIndex, piece -> + val message = piece.message + when (message) { + is ChatMessage.Assistant -> { + val blocks = piece.blocks ?: message.blocks + AssistantBlockColumn( + message = message, + blocks = blocks, + state = state, + streaming = false, + callbacks = callbacks, + ) + } + + is ChatMessage.Custom -> CustomRow(message, state, callbacks) + is ChatMessage.BashExecution -> BashRow(message, state, callbacks) + is ChatMessage.Other -> Unit + else -> Unit + } + // Avoid collapsing the last folded row into the anchor. + if (pieceIndex != segment.pieces.lastIndex) Spacer(Modifier.height(2.dp)) + } + } + attached = true + } + } + if (segmentIndex != row.segments.lastIndex) Spacer(Modifier.height(2.dp)) + } + } + } +} + +@Composable +private fun MessageRow( + message: ChatMessage, + state: ChatUiState, + forkTarget: io.github.kahme247.ompweb.chat.ForkTarget?, + header: HeaderSlot?, + streaming: Boolean, + prevAssistantEntryId: String? = null, + callbacks: ChatRowCallbacks, + /** Timestamp of the day separator this message draws, if it starts a day. */ + dayBreak: Long? = null, +) { + Column(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { + if (dayBreak != null) DaySeparatorRow(dayBreak) + BlockHeader(header) + when (message) { + is ChatMessage.User -> UserRow( + message = message, + state = state, + forkTarget = forkTarget, + prevAssistantEntryId = prevAssistantEntryId, + callbacks = callbacks, + ) + is ChatMessage.Assistant -> AssistantRow( + message = message, + state = state, + forkTarget = forkTarget, + streaming = streaming, + callbacks = callbacks, + ) + + is ChatMessage.ToolResult -> Unit + is ChatMessage.BashExecution -> BashRow(message, state, callbacks) + is ChatMessage.Custom -> CustomRow(message, state, callbacks) + is ChatMessage.Other -> Unit + } + } +} + +// --------------------------------------------------------------------- user + +@Composable +private fun UserRow( + message: ChatMessage.User, + state: ChatUiState, + forkTarget: io.github.kahme247.ompweb.chat.ForkTarget?, + prevAssistantEntryId: String?, + callbacks: ChatRowCallbacks, +) { + val colors = LocalOmpColors.current + if (message.branchSummary) { + BranchSummaryRow(message) + return + } + val showFork = forkTarget != null && !state.busy + val editTarget = prevAssistantEntryId?.takeIf { !state.busy && message.text.isNotBlank() } + val scrollState = rememberScrollState() + var expanded by remember(message.key) { mutableStateOf(false) } + var overflows by remember(message.key) { mutableStateOf(false) } + LaunchedEffect(message.text) { + // The cap only scrolls once the content actually overflows it. + snapshotFlow { scrollState.maxValue }.collect { overflows = it > 0 } + } + Surface( + color = colors.userBackground, + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp)) { + if (message.text.isNotBlank()) { + // A long paste is capped like the web bubble: the body scrolls + // inside a fixed height and "Show full" lifts the cap. + Box( + modifier = if (expanded) { + Modifier.fillMaxWidth() + } else { + Modifier + .fillMaxWidth() + .heightIn(max = MAX_USER_BUBBLE_HEIGHT) + .verticalScroll(scrollState) + }, + ) { + SafeMarkdownText( + markdown = message.text, + githubRepo = state.githubRepo, + onLink = callbacks.onLink, + ) + } + if (overflows || expanded) { + TextButton(onClick = { expanded = !expanded }) { + Text( + text = stringResource( + if (expanded) R.string.chat_collapse else R.string.chat_show_full, + ), + fontSize = 11.sp, + color = colors.textDim, + ) + } + } + } + if (message.images.isNotEmpty()) { + FlowRow( + modifier = Modifier.padding(top = 6.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + message.images.forEach { image -> + val source = chatImageSource(image, thumbnail = true) ?: return@forEach + RemoteImage( + source = source, + contentDescription = stringResource(R.string.chat_attached_image), + modifier = Modifier.widthIn(max = 220.dp).heightIn(max = 220.dp), + ) + } + } + } + Row( + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CopyAction(text = message.text) + if (editTarget != null) { + TextButton(onClick = { callbacks.onEditFromHere(editTarget, message.text) }) { + Text( + text = stringResource(R.string.chat_edit_from_here), + fontSize = 11.sp, + color = colors.textDim, + ) + } + } + if (showFork && forkTarget != null) { + ForkAction(state, forkTarget, callbacks) + } + } + } + } +} + +/** Height a user bubble clamps to before "Show full" appears (web 300px). */ +private val MAX_USER_BUBBLE_HEIGHT = 300.dp + +/** The assistant answer before [index], the branch target of "edit from here". */ +private fun previousAssistantEntryId(messages: List, index: Int): String? = + (messages.getOrNull(index - 1) as? ChatMessage.Assistant)?.entryId + +/** + * Markdown text with the web's two size guards: past [MAX_MARKDOWN_CHARS] the + * body is not parsed at all until the user asks for it, and past + * [RAW_VIEW_THRESHOLD] the rendered/raw toggle stays available. + */ +@Composable +private fun SafeMarkdownText( + markdown: String, + githubRepo: GithubRepoRef?, + onLink: (LinkTarget) -> Unit, + modifier: Modifier = Modifier, +) { + if (isLargeMessage(markdown)) { + var revealed by remember(markdown) { mutableStateOf(false) } + if (!revealed) { + TextButton(onClick = { revealed = true }) { + Text( + text = stringResource( + R.string.chat_large_message_reveal, + formatMessageSize(markdown.length), + ), + fontSize = 11.sp, + ) + } + return + } + val colors = LocalOmpColors.current + Box( + modifier = modifier + .fillMaxWidth() + .heightIn(max = 420.dp) + .border(1.dp, colors.border, RoundedCornerShape(6.dp)) + .verticalScroll(rememberScrollState()) + .padding(8.dp), + ) { + SelectionContainer { + Text( + text = markdown, + color = colors.text, + fontSize = 12.sp, + lineHeight = 18.sp, + fontFamily = OmpMonoFontFamily, + modifier = Modifier.fillMaxWidth(), + ) + } + } + return + } + if (markdown.length > RAW_VIEW_THRESHOLD) { + var raw by remember(markdown) { mutableStateOf(false) } + Column { + if (raw) { + SelectionContainer { + Text( + text = markdown, + fontSize = 12.sp, + fontFamily = OmpMonoFontFamily, + ) + } + } else { + MarkdownText( + markdown = markdown, + githubRepo = githubRepo, + onLink = onLink, + modifier = modifier, + ) + } + TextButton(onClick = { raw = !raw }) { + Text( + stringResource(if (raw) R.string.chat_show_rendered else R.string.chat_show_all), + fontSize = 11.sp, + ) + } + } + return + } + MarkdownText( + markdown = markdown, + githubRepo = githubRepo, + onLink = onLink, + modifier = modifier, + ) +} + +@Composable +private fun BranchSummaryRow(message: ChatMessage.User) { + val colors = LocalOmpColors.current + Column(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.weight(1f).height(1.dp).background(colors.border)) + Text( + text = stringResource(R.string.chat_branch_summary), + modifier = Modifier.padding(horizontal = 8.dp), + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + ) + Box(Modifier.weight(1f).height(1.dp).background(colors.border)) + } + if (message.text.isNotBlank()) { + SafeMarkdownText( + markdown = message.text, + githubRepo = null, + onLink = {}, + modifier = Modifier.padding(top = 6.dp), + ) + } + } +} + +// ---------------------------------------------------------------- assistant + +@Composable +internal fun AssistantRow( + message: ChatMessage.Assistant, + state: ChatUiState, + forkTarget: io.github.kahme247.ompweb.chat.ForkTarget?, + streaming: Boolean, + callbacks: ChatRowCallbacks, + /** Header of a standalone (streaming) reply; transcript rows pass theirs to [MessageRow]. */ + header: HeaderSlot? = null, +) { + val blocks = displayedBlocks(message, state) + if (blocks.isEmpty() && message.errorMessage == null && !streaming) return + val colors = LocalOmpColors.current + Column(modifier = Modifier.fillMaxWidth()) { + BlockHeader(header) + if (!blocks.any { it is AssistantBlock.Thinking || it is AssistantBlock.ToolCall } && streaming) { + // While the reply streams, only the throughput stays next to the + // text; the model name now lives in the speaker header. + Row( + modifier = Modifier.padding(bottom = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + StreamingMeta(message, state.engine.liveTokensPerSecond) + } + } + AssistantBlockColumn( + message = message, + blocks = blocks, + state = state, + streaming = streaming, + callbacks = callbacks, + ) + message.errorMessage?.takeIf { it.isNotBlank() }?.let { error -> + ErrorBanner(error, interrupted = isInterruptedMessage(error, message.stopReason)) + } + val text = blocks.filterIsInstance().joinToString("\n\n") { it.text } + // Tool-only answers and split segments without usage carry no rate. + val speed = if (!streaming && text.isNotBlank()) messageSpeed(message) else null + val canFork = forkTarget != null && !state.busy && !streaming + val speakId = message.entryId ?: message.key + if (!streaming && (text.isNotBlank() || canFork || speed != null)) { + Row( + modifier = Modifier.fillMaxWidth().padding(top = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (text.isNotBlank()) CopyAction(text = text) + if (text.isNotBlank()) { + val speaking = state.speakingId == speakId + TextButton(onClick = { + if (speaking) callbacks.onStopSpeech() + else callbacks.onToggleSpeech(message.entryId, speakId, text) + }) { + Icon( + imageVector = if (speaking) Icons.Filled.Close else Icons.Filled.PlayArrow, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = if (speaking) colors.accent else colors.textDim, + ) + Spacer(Modifier.width(3.dp)) + Text( + text = stringResource(if (speaking) R.string.chat_stop else R.string.chat_read), + fontSize = 11.sp, + color = if (speaking) colors.accent else colors.textDim, + ) + } + } + if (canFork && forkTarget != null) { + ForkAction(state, forkTarget, callbacks) + } + if (speed != null) { + Spacer(Modifier.weight(1f)) + Text( + text = stringResource( + R.string.chat_tokens_per_second, + formatGenerationRate(speed.tokensPerSecond), + ), + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + ) + } + } + } + } +} + +@Composable +private fun StreamingMeta(message: ChatMessage.Assistant, liveTokensPerSecond: Double?) { + val colors = LocalOmpColors.current + val chars = message.blocks.sumOf { block -> + when (block) { + is AssistantBlock.Text -> block.text.length + is AssistantBlock.Thinking -> block.thinking.length + else -> 0 + } + } + val estimate = (chars / 4.0).roundToInt() + if (estimate <= 0) return + Text(text = "$estimate", color = colors.text, fontSize = 11.sp) + if (liveTokensPerSecond != null) { + val tone = when { + liveTokensPerSecond >= 50 -> colors.statusSuccess + liveTokensPerSecond >= 30 -> colors.statusRenamed + liveTokensPerSecond >= 15 -> colors.statusWarning + else -> colors.statusError + } + Surface( + color = tone.copy(alpha = 0.14f), + contentColor = tone, + shape = RoundedCornerShape(4.dp), + ) { + Text( + text = stringResource( + R.string.chat_tokens_per_second, + formatGenerationRate(liveTokensPerSecond), + ), + fontSize = 11.sp, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 1.dp), + ) + } + } +} + +@Composable +private fun AssistantBlockColumn( + message: ChatMessage.Assistant, + blocks: List, + state: ChatUiState, + streaming: Boolean, + callbacks: ChatRowCallbacks, +) { + Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { + blocks.forEachIndexed { position, block -> + when (block) { + is AssistantBlock.Text -> { + if (block.text.isNotBlank()) { + SafeMarkdownText( + markdown = block.text, + githubRepo = state.githubRepo, + onLink = callbacks.onLink, + ) + } + } + + is AssistantBlock.Thinking -> ThinkingRow( + message = message, + block = block, + blockIndex = originalBlockIndex(message, block), + state = state, + streaming = streaming, + callbacks = callbacks, + ) + + is AssistantBlock.ToolCall -> ToolCallCard( + block = block, + result = state.toolResults[block.toolCallId], + state = state, + callbacks = callbacks, + startedAt = message.timestamp, + liveRun = isCurrentRunMessage(state.engine, message.key), + ) + + is AssistantBlock.Image -> { + val source = chatImageSource(block.image, thumbnail = true) ?: return@forEachIndexed + RemoteImage( + source = source, + contentDescription = null, + modifier = Modifier.widthIn(max = 320.dp).heightIn(max = 320.dp), + ) + } + + is AssistantBlock.Other -> Unit + } + if (position != blocks.lastIndex) Spacer(Modifier.height(1.dp)) + } + } +} + +private fun displayedBlocks( + message: ChatMessage.Assistant, + state: ChatUiState, +): List = message.blocks.filter { block -> + if (state.preferences.hideThinking && block is AssistantBlock.Thinking) return@filter false + if (block is AssistantBlock.Thinking && !block.deferred && !message.streaming && block.thinking.isBlank()) { + return@filter false + } + true +} + +private fun originalBlockIndex( + message: ChatMessage.Assistant, + block: AssistantBlock.Thinking, +): Int = message.blocks.indexOf(block).takeIf { it >= 0 } ?: block.blockIndex + +// -------------------------------------------------------------- thinking row + +@Composable +private fun ThinkingRow( + message: ChatMessage.Assistant, + block: AssistantBlock.Thinking, + blockIndex: Int, + state: ChatUiState, + streaming: Boolean, + callbacks: ChatRowCallbacks, +) { + val colors = LocalOmpColors.current + var expanded by remember(message.entryId, blockIndex) { + mutableStateOf(ChatDisplayPrefs.thinkingExpanded(state.preferences.expandThinking)) + } + val key = "${message.entryId ?: message.key}:$blockIndex" + val fetched = state.thinking[key] + LaunchedEffect(expanded, block.deferred, message.entryId) { + if (expanded && block.deferred) callbacks.onLoadThinking(message.entryId, blockIndex) + } + Column( + modifier = Modifier.fillMaxWidth().border(1.dp, colors.border, RoundedCornerShape(OmpCardRadius)), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded } + .padding(horizontal = 9.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = stringResource(R.string.chat_thinking), + color = colors.textMuted, + fontSize = 11.5.sp, + fontFamily = OmpMonoFontFamily, + fontWeight = FontWeight.SemiBold, + ) + if (message.durationMs != null) { + Text( + text = "${(message.durationMs / 1000).roundToInt()}s", + color = colors.textDim, + fontSize = 10.sp, + ) + } + Spacer(Modifier.weight(1f)) + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource(if (expanded) R.string.chat_collapse else R.string.chat_expand), + modifier = Modifier.size(16.dp), + tint = colors.textDim, + ) + } + if (expanded) { + val text = when (val entry = fetched) { + ThinkingEntry.Loading -> stringResource(R.string.chat_loading_block) + is ThinkingEntry.Error -> entry.message + is ThinkingEntry.Text -> entry.text + null -> if (block.deferred) stringResource(R.string.chat_loading_block) else block.thinking + } + Text( + text = text, + modifier = Modifier.fillMaxWidth().padding(horizontal = 9.dp, vertical = 7.dp), + color = if (fetched is ThinkingEntry.Error) colors.statusError else colors.textMuted, + fontSize = 12.sp, + fontFamily = OmpMonoFontFamily, + lineHeight = 18.sp, + ) + } + } + if (streaming && !expanded) { + // A streaming thinking block stays discoverable without opening it. + Text( + text = (block.thinking.takeLast(60).replace("\n", " ") + if (block.thinking.length > 60) "…" else ""), + color = colors.textDim, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 4.dp, top = 1.dp), + ) + } +} + +// --------------------------------------------------------------- tool cards + +/** + * Whether [key] names the message the running turn is working on right now: + * the streamed reply while it streams, else the transcript's newest message. + * A tool call found in an older message belongs to an earlier run and must not + * read as running. + */ +private fun isCurrentRunMessage(engine: ChatState, key: String): Boolean { + val streaming = engine.streaming + return if (streaming != null) streaming.key == key else engine.messages.lastOrNull()?.key == key +} + +@Composable +private fun ToolCallCard( + block: AssistantBlock.ToolCall, + result: ChatMessage.ToolResult?, + state: ChatUiState, + callbacks: ChatRowCallbacks, + /** The assistant message's timestamp: when this call started running. */ + startedAt: Long? = null, + /** + * Whether this call belongs to the run that is streaming right now. Only + * such a call without a result may read as running; a call left without a + * result by an earlier, aborted run must not pulse or tick in every later + * stream. + */ + liveRun: Boolean = false, +) { + val colors = LocalOmpColors.current + val isRunning = result?.partial == true || + state.engine.liveTools.any { it.toolCallId == block.toolCallId && it.endedAt == null } || + (result == null && liveRun && state.engine.streamingActive) + val isError = result?.isError == true + var expanded by remember(block.toolCallId) { + mutableStateOf(ChatDisplayPrefs.toolCallExpanded(state.preferences.toolCallsCollapsed, isRunning)) + } + var inputExpanded by remember(block.toolCallId) { mutableStateOf(false) } + val category = getToolCategory(block.toolName) + val hubSend = if (category == ToolCategory.Hub) getHubSendSummary(block.input) else null + val hubJobs = if (category == ToolCategory.Hub) getHubJobs(result?.details) else null + val receipts = hubReceipts(result?.details) + val hubReceiptOutcome = receipts + .map { it.second } + .takeIf { outcomes -> outcomes.isNotEmpty() && outcomes.all { it == outcomes.first() } } + ?.firstOrNull() + val title = when { + hubSend != null -> "IRC → ${hubSend.to.joinToString(", ")}" + (hubReceiptOutcome?.let { " $it" } ?: "") + hubJobs != null -> getHubJobsHeader(hubJobs) + else -> block.toolName + } + val preview = when { + hubSend != null -> hubSend.snippet.ifEmpty { hubSend.to.joinToString(", ") } + hubJobs != null -> hubJobs.joinToString(" · ") { it.label } + else -> getToolPreview(block) + } + val semantic = getSemanticToolLabel(block) + val resultText = result?.text + val resultEmpty = resultText != null && resultText.trim().let { it == "(no output)" || it.isEmpty() } + val diff = if (expanded && result != null && !isError) { + ToolDiff.resolve( + patch = result.details?.str("patch"), + diff = result.details?.str("diff"), + ) + } else { + null + } + val meta = getToolResultMeta(result?.details) + // The live tool frame knows the real start; the message timestamp is the + // fallback for a call whose frame has not arrived (or a finished one). + val liveStart = state.engine.liveTools + .firstOrNull { it.toolCallId == block.toolCallId && it.endedAt == null } + ?.startedAt + ?: startedAt + val reportedDuration = result?.details?.get("durationMs") + ?.let { (it as? JsonPrimitive)?.contentOrNull?.toDoubleOrNull() } + ?.toLong() + val settledDuration = reportedDuration ?: run { + val finished = result?.timestamp + if (finished != null && startedAt != null && finished > startedAt) finished - startedAt else null + } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(colors.toolBackground, RoundedCornerShape(OmpCardRadius)) + .border(1.dp, colors.border, RoundedCornerShape(OmpCardRadius)), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .testTag("chat-tool-card-header") + .clickable( + onClickLabel = stringResource(if (expanded) R.string.chat_collapse else R.string.chat_expand), + ) { expanded = !expanded } + // TalkBack reads the tool name, preview and state as one button. + .semantics(mergeDescendants = true) {} + .padding(horizontal = 10.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + ToolCategoryIcon(toolName = block.toolName, size = 14.dp) + Text( + text = title, + color = if (isError) colors.statusError else colors.text, + fontSize = 11.5.sp, + fontFamily = OmpMonoFontFamily, + fontWeight = FontWeight.SemiBold, + ) + if (preview.isNotEmpty()) { + val clickableFile = semantic.isFile && block.input.str("path") != null + Text( + text = preview, + color = colors.textDim, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + .then( + if (clickableFile) { + Modifier.clickable { callbacks.onLink(LinkTarget.File(block.input.str("path")!!, null)) } + } else { + Modifier + }, + ), + ) + } else { + Spacer(Modifier.weight(1f)) + } + // The duration slot is always laid out, empty before a call has run, + // so switching between running and finished never shifts the row. + // Only this text recomposes per tick. + Box(Modifier.widthIn(min = 34.dp), contentAlignment = Alignment.CenterEnd) { + ElapsedText( + startedAt = liveStart, + running = isRunning, + settledMs = settledDuration, + color = if (isRunning) colors.accent else colors.textDim, + fontSize = 10.sp, + textAlign = TextAlign.End, + ) + } + ToolStateIcon(isError = isError, running = isRunning, done = result != null) + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource(if (expanded) R.string.chat_collapse else R.string.chat_expand), + modifier = Modifier.size(16.dp), + tint = colors.textDim, + ) + } + if (meta != null) { + Text( + text = meta, + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + modifier = Modifier.padding(start = 30.dp, bottom = 5.dp), + ) + } + result?.passiveContext?.let { context -> + if (!expanded) { + Text( + text = "↳ $context", + color = colors.textDim, + fontSize = 10.5.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 30.dp, end = 9.dp, bottom = 5.dp), + ) + } + } + if (expanded) { + Column(modifier = Modifier.padding(start = 9.dp, end = 9.dp, top = 2.dp, bottom = 7.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("$", color = colors.textDim, fontFamily = OmpMonoFontFamily, fontSize = 11.sp) + Spacer(Modifier.width(5.dp)) + Text( + text = formatToolCommand(block), + color = colors.textMuted, + fontFamily = OmpMonoFontFamily, + fontSize = 11.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (block.input.isNotEmpty()) { + TextButton(onClick = { inputExpanded = !inputExpanded }) { + Text( + text = if (inputExpanded) stringResource(R.string.chat_collapse_input) else stringResource(R.string.chat_show_full_input), + fontSize = 10.sp, + color = colors.accent, + ) + } + } + } + if (inputExpanded) { + CodeSurface(block.input.toString()) + } + val todo = if (category == ToolCategory.Todo) getTodoSummary(block.input) else null + if (todo != null) { + Row( + modifier = Modifier.padding(vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = todo.action, + color = colors.accent, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + ) + Text(todo.task ?: todo.label, fontSize = 11.sp, color = colors.text) + } + } + TaskResultPanel(result?.details) + HubResultPanel(block.input, result) + when { + isRunning && resultText.isNullOrBlank() -> Text( + text = stringResource(R.string.chat_running), + color = colors.textDim, + fontSize = 11.sp, + modifier = Modifier.padding(top = 4.dp), + ) + diff != null -> DiffView( + path = null, + oldText = null, + newText = null, + unifiedPatch = diff, + modifier = Modifier.padding(top = 4.dp), + ) + result != null -> { + if (result.images.isNotEmpty()) { + ToolImages(result.images) + } + if (!(hubJobs != null || (hubSend != null && !isError)) && + !(resultEmpty && result.images.isNotEmpty()) + ) { + CodeSurface( + if (resultEmpty) stringResource(R.string.chat_no_output) + else formatToolOutput(resultText.orEmpty(), block.toolName), + error = isError, + ) + } + } + } + result?.passiveContext?.let { context -> + Text( + text = "↳ $context", + color = colors.textDim, + fontSize = 10.5.sp, + modifier = Modifier.padding(top = 6.dp), + ) + } + } + } + } +} + +@Composable +private fun ToolImages(images: List) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + images.forEach { image -> + val source = chatImageSource(image, thumbnail = true) ?: return@forEach + RemoteImage( + source = source, + contentDescription = null, + modifier = Modifier.widthIn(max = 240.dp).heightIn(max = 240.dp), + ) + } + } +} + +/** + * The state slot of a tool row: a pulsing accent dot while the call runs, a + * muted check once it settles and an alert when it failed. + */ +@Composable +private fun ToolStateIcon(isError: Boolean, running: Boolean, done: Boolean) { + val colors = LocalOmpColors.current + Box(Modifier.size(14.dp), contentAlignment = Alignment.Center) { + when { + isError -> Icon( + imageVector = Icons.Filled.Warning, + contentDescription = stringResource(R.string.chat_failed), + modifier = Modifier.size(13.dp), + tint = colors.statusError, + ) + running -> LivePulseDot(size = 8.dp) + done -> Icon( + imageVector = Icons.Filled.Check, + contentDescription = stringResource(R.string.chat_done), + modifier = Modifier.size(13.dp), + tint = colors.textDim, + ) + } + } +} + +@Composable +private fun CodeSurface(text: String, error: Boolean = false) { + val colors = LocalOmpColors.current + Surface( + color = colors.background, + shape = RoundedCornerShape(6.dp), + modifier = Modifier + .fillMaxWidth() + .padding(top = 3.dp) + .border(1.dp, if (error) colors.statusError.copy(alpha = 0.4f) else colors.border, RoundedCornerShape(6.dp)), + ) { + Text( + text = text, + modifier = Modifier + .heightIn(max = 360.dp) + .padding(horizontal = 9.dp, vertical = 7.dp), + color = if (error) colors.statusError else colors.textMuted, + fontFamily = OmpMonoFontFamily, + fontSize = 11.sp, + lineHeight = 16.sp, + ) + } +} + +// ------------------------------------------------------------------- panels + +@Composable +private fun TaskResultPanel(details: JsonObject?) { + if (details == null) return + val colors = LocalOmpColors.current + val results = details.arr("results")?.filterIsInstance().orEmpty() + val progress = details.arr("progress")?.filterIsInstance().orEmpty() + val async = details.obj("async") + if (results.isEmpty() && progress.isEmpty() && async == null) return + val rows = when { + results.isNotEmpty() -> results + progress.isNotEmpty() -> progress + async?.str("jobId") != null -> listOf( + JsonObject( + mapOf( + "id" to JsonPrimitive(async.str("jobId")!!), + "agent" to JsonPrimitive("task"), + "status" to JsonPrimitive("started"), + "task" to JsonPrimitive(async.str("jobId")!!), + ), + ), + ) + else -> emptyList() + } + val totalTokens = rows.sumOf { it.int("tokens") ?: 0 } + val totalCost = rows.sumOf { it.num("cost") ?: 0.0 } + val totalParts = listOfNotNull( + stringResource(R.string.chat_task_subagent_count, rows.size), + formatTokens(totalTokens)?.let { "$it tokens" }, + formatCost(totalCost), + formatDuration(details.num("totalDurationMs")?.toLong()), + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + .background(colors.backgroundSubtle, RoundedCornerShape(6.dp)) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(3.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(stringResource(R.string.chat_task_subagents), color = colors.text, fontSize = 11.sp, fontWeight = FontWeight.SemiBold) + Spacer(Modifier.weight(1f)) + Text(totalParts.joinToString(" · "), color = colors.textDim, fontSize = 10.sp, fontFamily = OmpMonoFontFamily) + } + rows.forEachIndexed { index, row -> + val status = taskRowStatus(row) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = when (status) { + "completed" -> "✓" + "failed" -> "✗" + "aborted" -> "⊘" + else -> "•" + }, + color = when (status) { + "completed" -> colors.statusSuccess + "failed" -> colors.statusError + else -> colors.accent + }, + fontSize = 11.sp, + ) + Text( + text = row.str("agent") ?: "subagent", + color = colors.accent, + fontSize = 10.5.sp, + fontFamily = OmpMonoFontFamily, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = row.str("task") ?: row.str("assignment") ?: "", + color = colors.text, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + val rowParts = listOfNotNull( + formatTokens(row.int("tokens"))?.let { "$it tokens" }, + formatCost(row.num("cost")), + if (status != "started") formatDuration(row.num("durationMs")?.toLong()) else null, + shortModel(row.str("resolvedModel")), + ) + if (rowParts.isNotEmpty()) { + Text( + text = rowParts.joinToString(" · "), + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + ) + } + } + if (index == rows.lastIndex) Spacer(Modifier.height(1.dp)) + } + } +} + +private fun taskRowStatus(row: JsonObject): String { + if (row.bool("aborted") == true) return "aborted" + if (!row.str("error").isNullOrEmpty()) return "failed" + val exitCode = row.int("exitCode") + if (exitCode != null) return if (exitCode == 0) "completed" else "failed" + return when (row.str("status")) { + "completed" -> "completed" + "failed" -> "failed" + "aborted" -> "aborted" + else -> "started" + } +} + +@Composable +private fun HubResultPanel(input: JsonObject, result: ChatMessage.ToolResult?) { + val colors = LocalOmpColors.current + when (input.str("op")) { + "send" -> { + val send = getHubSendSummary(input) ?: return + val receipts = hubReceipts(result?.details) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + .background(colors.backgroundSubtle, RoundedCornerShape(6.dp)) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "IRC → ${send.to.joinToString(", ")}", + color = colors.text, + fontSize = 11.sp, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.weight(1f)) + if (receipts.isNotEmpty()) { + Text( + text = receipts.joinToString(" · ") { it.second }, + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + ) + } + } + if (send.message.isNotBlank()) { + MarkdownText(markdown = send.message, onLink = {}, modifier = Modifier.fillMaxWidth()) + } + } + } + + "jobs" -> { + val jobs = getHubJobs(result?.details) ?: return + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + .background(colors.backgroundSubtle, RoundedCornerShape(6.dp)) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(3.dp), + ) { + Text(getHubJobsHeader(jobs), color = colors.text, fontSize = 11.sp, fontWeight = FontWeight.SemiBold) + jobs.forEach { job -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = job.status, + color = when (job.status) { + "completed" -> colors.statusSuccess + "failed" -> colors.statusError + else -> colors.accent + }, + fontSize = 10.sp, + ) + Text( + text = "[${job.type}]", + color = colors.accent, + fontSize = 10.5.sp, + fontFamily = OmpMonoFontFamily, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = job.label, + color = colors.text, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + formatJobDuration(job.durationMs)?.let { + Text(it, color = colors.textDim, fontSize = 10.sp, fontFamily = OmpMonoFontFamily) + } + } + } + } + } + } +} + +private fun hubReceipts(details: JsonObject?): List> { + val receipts = details?.arr("receipts") ?: return emptyList() + return receipts.filterIsInstance().mapNotNull { raw -> + val to = raw.str("to") ?: return@mapNotNull null + to to (raw.str("outcome") ?: "sent") + } +} + +// ----------------------------------------------------------- bash & custom + +@Composable +private fun BashRow( + message: ChatMessage.BashExecution, + state: ChatUiState, + callbacks: ChatRowCallbacks, +) { + val colors = LocalOmpColors.current + val pending = message.output.isNullOrEmpty() && message.exitCode == null && !message.cancelled + val isError = message.cancelled || (message.exitCode != null && message.exitCode != 0) + val toolName = "bash" + val block = AssistantBlock.ToolCall( + toolCallId = "bash-${message.key}", + toolName = toolName, + input = JsonObject(mapOf("command" to JsonPrimitive(message.command))), + ) + val result = if (pending) { + null + } else { + ChatMessage.ToolResult( + key = message.key, + timestamp = message.timestamp, + entryId = message.entryId, + toolCallId = block.toolCallId, + toolName = toolName, + text = message.output.orEmpty(), + isError = isError, + ) + } + ToolCallCard( + block = block, + result = result, + state = state, + callbacks = callbacks, + startedAt = message.timestamp, + liveRun = isCurrentRunMessage(state.engine, message.key), + ) + message.fullOutputPath?.let { path -> + val output = state.bashOutputs[path] + Column(modifier = Modifier.padding(start = 6.dp, top = 4.dp)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { + if (output !is BashOutputUi.Ready) { + TextButton( + enabled = output !is BashOutputUi.Loading, + onClick = { callbacks.onLoadBashOutput(path) }, + ) { + Text( + text = stringResource(if (output is BashOutputUi.Loading) R.string.chat_loading_full_output else R.string.chat_view_full_output), + fontSize = 11.sp, + color = colors.accent, + ) + } + } + Text( + text = stringResource(R.string.chat_download), + color = colors.textDim, + fontSize = 11.sp, + modifier = Modifier.clickable { callbacks.onDownloadBashOutput(path) }, + ) + } + when (output) { + is BashOutputUi.Ready -> CodeSurface(output.text) + is BashOutputUi.Error -> Text( + text = output.message, + color = colors.statusError, + fontSize = 11.sp, + modifier = Modifier.padding(vertical = 4.dp), + ) + else -> Unit + } + } + } +} + +@Composable +private fun CustomRow( + message: ChatMessage.Custom, + state: ChatUiState, + callbacks: ChatRowCallbacks, +) { + if (message.customType == "compaction") { + CompactionRow(message) + return + } + if (message.customType == PASSIVE_TOOL_CONTEXT || message.customType == MOUNT_NOTICE) return + if (!message.display) return + val colors = LocalOmpColors.current + val isDeveloper = message.customType == "developer" + val isNotice = isDeveloper || message.customType == "async-result" || message.customType == "lsp-late-diagnostic" + var contentExpanded by remember(message.key) { mutableStateOf(!isNotice) } + var detailsExpanded by remember(message.key) { mutableStateOf(false) } + val isPlainText = message.customType == "async-result" || message.customType == "lsp-late-diagnostic" + val displayText = if (isDeveloper) stripHiddenWrappers(message.text) else message.text + val firstLine = displayText.lineSequence().firstOrNull { it.trim().isNotEmpty() }?.trim().orEmpty() + val collapsedPreview = if (firstLine.isNotEmpty() && firstLine != displayText.trim()) "$firstLine …" else firstLine + val title = when { + isDeveloper -> leadingWrapperTitle(message.text) ?: "developer" + message.customType == "advisor" -> stringResource(R.string.chat_advisor) + else -> formatCustomType(message.customType) + } + Column( + modifier = Modifier + .fillMaxWidth() + .border(1.dp, colors.border, RoundedCornerShape(OmpCardRadius)), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .then(if (isNotice) Modifier.clickable { contentExpanded = !contentExpanded } else Modifier) + .padding(horizontal = 9.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (isNotice) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = colors.textDim, + ) + } + Text( + text = title, + color = colors.textMuted, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.weight(1f)) + TimestampLabel(message.timestamp) + } + if (contentExpanded) { + Column(modifier = Modifier.padding(horizontal = 9.dp, vertical = 6.dp)) { + if (displayText.isBlank()) { + Text(stringResource(R.string.chat_no_message), color = colors.textDim, fontSize = 11.sp) + } else if (isPlainText) { + Text( + text = displayText, + color = colors.textMuted, + fontFamily = OmpMonoFontFamily, + fontSize = 11.sp, + lineHeight = 16.sp, + modifier = Modifier.heightIn(max = 420.dp), + ) + } else { + MarkdownText( + markdown = displayText, + githubRepo = state.githubRepo, + onLink = callbacks.onLink, + ) + } + } + } else { + Text( + text = collapsedPreview.ifEmpty { displayText.take(120) }, + color = colors.textDim, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .clickable { contentExpanded = true } + .padding(horizontal = 9.dp, vertical = 6.dp), + ) + } + Row( + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CopyAction(text = displayText.ifBlank { message.details?.toString().orEmpty() }) + message.details?.let { details -> + TextButton(onClick = { detailsExpanded = !detailsExpanded }) { + Text( + text = if (detailsExpanded) stringResource(R.string.chat_hide_details) else stringResource(R.string.chat_details), + fontSize = 10.sp, + color = colors.textDim, + ) + } + } + Spacer(Modifier.weight(1f)) + } + if (detailsExpanded) { + CodeSurface(message.details.toString()) + } + } +} + +@Composable +private fun CompactionRow(message: ChatMessage.Custom) { + val colors = LocalOmpColors.current + val parsed = remember(message.text) { parseCompactionSummary(message.text) } + val tokensBefore = message.details?.int("tokensBefore") + val tokensAfter = message.details?.int("tokensAfter") + val method = message.details?.str("method")?.takeIf { it.isNotBlank() } + var filesExpanded by remember(message.key) { mutableStateOf(false) } + Column( + modifier = Modifier + .fillMaxWidth() + .border(1.dp, colors.border, RoundedCornerShape(OmpCardRadius)), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(colors.panel) + .padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.chat_compaction), + color = colors.textMuted, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.weight(1f)) + TimestampLabel(message.timestamp) + } + Column(modifier = Modifier.padding(horizontal = 11.dp, vertical = 9.dp)) { + Text( + text = stringResource(R.string.chat_conversation_compacted), + color = colors.text, + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + ) + if (method != null || (tokensBefore != null && tokensAfter != null)) { + Row( + modifier = Modifier.padding(top = 4.dp, bottom = 6.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + method?.let { + Text( + text = it, + color = colors.textMuted, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + modifier = Modifier + .background(colors.backgroundSubtle, RoundedCornerShape(4.dp)) + .padding(horizontal = 7.dp, vertical = 1.dp), + ) + } + if (tokensBefore != null && tokensAfter != null) { + Text( + text = "${formatCompactNumber(tokensBefore.toDouble())} → ${formatCompactNumber(tokensAfter.toDouble())} tokens", + color = colors.textMuted, + fontSize = 11.sp, + ) + } + } + } + Text( + text = stringResource(R.string.chat_compaction_hint), + color = colors.text, + fontSize = 13.sp, + ) + if (parsed.body.isNotBlank()) { + MarkdownText( + markdown = parsed.body, + modifier = Modifier.padding(top = 6.dp), + onLink = {}, + ) + } else { + Text(stringResource(R.string.chat_no_summary), color = colors.textDim, fontSize = 11.sp) + } + val total = parsed.readFiles.size + parsed.modifiedFiles.size + if (total > 0) { + TextButton(onClick = { filesExpanded = !filesExpanded }) { + val parts = buildList { + if (parsed.readFiles.isNotEmpty()) add(stringResource(R.string.chat_file_context_read, parsed.readFiles.size)) + if (parsed.modifiedFiles.isNotEmpty()) add(stringResource(R.string.chat_file_context_modified, parsed.modifiedFiles.size)) + } + Text( + text = stringResource(R.string.chat_file_context, parts.joinToString(", ")), + fontSize = 11.sp, + color = colors.accent, + ) + } + if (filesExpanded) { + if (parsed.modifiedFiles.isNotEmpty()) CompactionFiles(stringResource(R.string.chat_modified_files), parsed.modifiedFiles) + if (parsed.readFiles.isNotEmpty()) CompactionFiles(stringResource(R.string.chat_read_files), parsed.readFiles) + } + } + } + } +} + +@Composable +private fun CompactionFiles(title: String, files: List) { + val colors = LocalOmpColors.current + Text( + text = title, + color = colors.textMuted, + fontSize = 10.5.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(top = 4.dp), + ) + files.forEach { file -> + Text( + text = "• $file", + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + ) + } +} + +// ------------------------------------------------------------------- pieces + +@Composable +private fun ProcessDetailsGroup( + key: String, + messageCount: Int, + toolCallCount: Int, + live: LiveActivity? = null, + startedAt: Long? = null, + /** The turn's last timestamp; the settled duration is derived from it. */ + finishedAt: Long? = null, + content: @Composable () -> Unit, +) { + val colors = LocalOmpColors.current + var expanded by remember(key) { mutableStateOf(false) } + val settled = if ( + live == null && startedAt != null && finishedAt != null && finishedAt > startedAt + ) { + finishedAt - startedAt + } else { + null + } + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded } + .padding(horizontal = 4.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource(if (expanded) R.string.chat_collapse else R.string.chat_expand), + modifier = Modifier.size(14.dp), + tint = if (live != null) colors.accent else colors.textDim, + ) + if (live == null) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = stringResource(R.string.chat_done), + modifier = Modifier.size(12.dp), + tint = colors.textDim, + ) + } + val parts = buildList { + add(stringResource(R.string.chat_process_details)) + add(stringResource(if (messageCount == 1) R.string.chat_process_message else R.string.chat_process_messages, messageCount)) + if (toolCallCount > 0) { + add(stringResource(if (toolCallCount == 1) R.string.chat_process_tool_call else R.string.chat_process_tool_calls, toolCallCount)) + } + } + // The duration ticks in its own composable, so a running fold + // recomposes only this text every second, not the whole row. + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = parts.joinToString(" · "), + color = if (live != null) colors.textMuted else colors.textDim, + fontSize = 11.sp, + ) + ElapsedText( + startedAt = startedAt, + running = live != null, + settledMs = settled, + prefix = " · ", + color = if (live != null) colors.textMuted else colors.textDim, + fontSize = 11.sp, + ) + } + } + // The collapsed row of the running turn must still show that work is in + // progress: what it waits on right now, with a live indicator. The + // static summary returns as soon as the turn ends (live == null). + if (live != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 24.dp, end = 4.dp, bottom = 4.dp) + .semantics(mergeDescendants = true) { liveRegion = LiveRegionMode.Polite }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (live is LiveActivity.Tool) { + ToolCategoryIcon(toolName = live.name, size = 12.dp) + } + LivePulseDot() + Text( + text = liveActivityText(live), + color = colors.textMuted, + fontSize = 11.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + if (expanded) { + Column( + modifier = Modifier.padding(start = 6.dp, top = 2.dp, bottom = 2.dp), + verticalArrangement = Arrangement.spacedBy(3.dp), + ) { + content() + } + } + } +} + +/** The live activity line: current tool and its intent, or the phase. */ +@Composable +private fun liveActivityText(live: LiveActivity): String = when (live) { + is LiveActivity.Tool -> buildString { + append(live.name) + if (live.intent.isNotBlank()) append(" · ").append(live.intent) + if (live.more > 0) append(' ').append(stringResource(R.string.chat_live_more, live.more)) + } + + LiveActivity.Thinking -> stringResource(R.string.chat_live_thinking) + LiveActivity.Writing -> stringResource(R.string.chat_live_writing) + LiveActivity.RunningCommand -> stringResource(R.string.chat_phase_running_command) + LiveActivity.Compacting -> stringResource(R.string.chat_status_compacting) + LiveActivity.Working -> stringResource(R.string.chat_status_working) +} + +// ---------------------------------------------------------------- utilities + +@Composable +private fun CopyAction(text: String) { + if (text.isBlank()) return + val colors = LocalOmpColors.current + val context = LocalContext.current + TextButton(onClick = { copyTextToClipboard(context, "message", text) }) { + Text(stringResource(R.string.chat_copy), fontSize = 11.sp, color = colors.textDim) + } +} + +/** + * Fork button of a message row. Spawning omp for a large session or branching a + * long history is not instant, so the in-flight request renders as a spinner: + * without it a slow fork looks exactly like a dead button, and only a transient + * system Toast told the user that nothing was created. + */ +@Composable +private fun ForkAction( + state: ChatUiState, + forkTarget: io.github.kahme247.ompweb.chat.ForkTarget, + callbacks: ChatRowCallbacks, +) { + val colors = LocalOmpColors.current + val forking = state.forkingEntryId == forkTarget.entryId + // One fork at a time (the view model ignores extra taps); disabling the + // other rows makes that rule visible instead of silently swallowing taps. + TextButton( + onClick = { callbacks.onFork(forkTarget.entryId, forkTarget.editPrompt) }, + enabled = state.forkingEntryId == null, + ) { + if (forking) { + CircularProgressIndicator( + modifier = Modifier.size(12.dp), + strokeWidth = 1.5.dp, + color = colors.textDim, + ) + Spacer(Modifier.width(5.dp)) + Text(stringResource(R.string.chat_forking), fontSize = 11.sp, color = colors.textDim) + } else { + Text(stringResource(R.string.chat_new_session), fontSize = 11.sp, color = colors.textDim) + } + } +} + +/** + * A block's speaker header. Absent while the same speaker continues on the + * same day, so a run of messages reads as one block; a day separator is a + * separate element of the rendered transcript ([TranscriptFormat.daySeparators]). + */ +@Composable +internal fun BlockHeader(slot: HeaderSlot?) { + if (slot == null) return + val speaker = slot.speaker + if (slot.showHeader && speaker != null) { + SpeakerHeaderRow(speaker = speaker, timestamp = slot.timestamp) + } +} + +/** The line between two transcript days: `Today`, `Yesterday`, or a date. */ +@Composable +private fun DaySeparatorRow(timestamp: Long?) { + if (timestamp == null) return + val colors = LocalOmpColors.current + val formatter = rememberChatTimeFormatter() + val clock = LocalChatClock.current + val now = LocalChatNow.current ?: clock() + val label = remember(timestamp, now, formatter) { formatter.daySeparator(timestamp, now) } + Row( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(Modifier.weight(1f).height(1.dp).background(colors.border)) + Text( + text = label, + modifier = Modifier.padding(horizontal = 8.dp), + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + ) + Box(Modifier.weight(1f).height(1.dp).background(colors.border)) + } +} + +/** + * The compact "who is speaking" header: a round role badge with the speaker's + * initial, its name and the block's time. The user's badge is filled with the + * accent and the assistant's is outlined, so the two never read alike. + */ +@Composable +internal fun SpeakerHeaderRow(speaker: Speaker, timestamp: Long?) { + val colors = LocalOmpColors.current + val name = when (speaker.role) { + SpeakerRole.You -> stringResource(R.string.chat_speaker_you) + SpeakerRole.Assistant -> speaker.name ?: stringResource(R.string.chat_speaker_assistant) + SpeakerRole.Subagent -> speaker.name ?: stringResource(R.string.chat_speaker_subagent) + } + val badgeColor = when (speaker.role) { + SpeakerRole.You -> colors.accent + SpeakerRole.Subagent -> colors.secondaryAccent ?: colors.accent + SpeakerRole.Assistant -> colors.backgroundSubtle + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 2.dp, end = 2.dp, top = 6.dp, bottom = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Box( + modifier = Modifier + .size(18.dp) + .background(badgeColor, CircleShape) + .then( + if (speaker.role == SpeakerRole.Assistant) { + Modifier.border(1.dp, colors.border, CircleShape) + } else { + Modifier + }, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = speaker.initial.toString(), + color = if (speaker.role == SpeakerRole.Assistant) colors.textMuted else colors.onAccent, + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = OmpMonoFontFamily, + ) + } + Text( + text = name, + color = if (speaker.role == SpeakerRole.You) colors.text else colors.textMuted, + fontSize = 11.sp, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.weight(1f)) + TimestampLabel(timestamp) + } +} + +/** + * A timestamp in the transcript. Tapping it shows the full date and time: the + * header uses the short shape (today `HH:mm`, yesterday `Yesterday HH:mm`, this + * year `d MMM, HH:mm`), which cannot carry a weekday or a year. + */ +@Composable +private fun TimestampLabel(timestamp: Long?) { + if (timestamp == null) return + val colors = LocalOmpColors.current + val formatter = rememberChatTimeFormatter() + val clock = LocalChatClock.current + val now = LocalChatNow.current ?: clock() + val formatted = remember(timestamp, now, formatter) { formatter.header(timestamp, now) } + var full by remember(timestamp) { mutableStateOf(false) } + Text( + text = formatted, + modifier = Modifier + .clickable(onClickLabel = stringResource(R.string.chat_timestamp_full_title)) { full = true } + .padding(horizontal = 4.dp, vertical = 3.dp), + color = colors.textDim, + fontSize = 10.sp, + ) + if (full) { + OmpAlertDialog( + onDismissRequest = { full = false }, + confirmButton = { + TextButton(onClick = { full = false }) { + Text( + text = stringResource(R.string.common_close), + fontSize = 12.sp, + color = colors.accent, + ) + } + }, + title = { + Text( + text = stringResource(R.string.chat_timestamp_full_title), + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + ) + }, + text = { + Text( + text = formatter.full(timestamp), + fontFamily = OmpMonoFontFamily, + fontSize = 12.sp, + ) + }, + ) + } +} + +@Composable +private fun ErrorBanner(message: String, interrupted: Boolean) { + val colors = LocalOmpColors.current + val tone = if (interrupted) colors.textMuted else colors.statusError + Surface( + color = tone.copy(alpha = 0.08f), + contentColor = tone, + shape = RoundedCornerShape(6.dp), + modifier = Modifier + .fillMaxWidth() + .padding(top = 2.dp) + .border(1.dp, tone.copy(alpha = 0.35f), RoundedCornerShape(6.dp)), + ) { + Row(modifier = Modifier.padding(7.dp), verticalAlignment = Alignment.Top) { + if (!interrupted) { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + modifier = Modifier.size(14.dp), + ) + Spacer(Modifier.width(6.dp)) + } + Text( + text = if (interrupted) stringResource(R.string.chat_interrupted) else message, + fontSize = 12.sp, + lineHeight = 17.sp, + ) + } + } +} + +@Composable +internal fun PendingUserBubble(text: String, header: HeaderSlot? = null) { + val colors = LocalOmpColors.current + Column(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { + BlockHeader(header) + Surface( + color = colors.userBackground, + shape = RoundedCornerShape(OmpCardRadius), + modifier = Modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp)) { + Text(text, color = colors.text, fontSize = 14.sp) + Text( + text = stringResource(R.string.chat_sending), + color = colors.textDim, + fontSize = 10.sp, + modifier = Modifier.padding(top = 2.dp), + ) + } + } + } +} + +internal fun isInterruptedMessage(errorMessage: String?, stopReason: String?): Boolean { + if (stopReason == "aborted") return true + if (errorMessage.isNullOrBlank()) return false + val lower = errorMessage.lowercase().trim() + return lower == "interrupted by user" || + lower == "interrupted" || + lower == "generation stopped by user" || + lower.startsWith("interrupted by user") || + lower.startsWith("interrupted:") || + lower == "aborted" || + lower == "cancelled" +} + +private fun chatImageSource( + image: io.github.kahme247.ompweb.chat.ChatImage, + thumbnail: Boolean, +): io.github.kahme247.ompweb.ui.render.ImageSource? { + image.url?.takeIf { it.isNotBlank() }?.let { url -> + val withThumb = if (thumbnail && !url.contains("thumb=")) { + if (url.contains('?')) "$url&thumb=1" else "$url?thumb=1" + } else { + url + } + return io.github.kahme247.ompweb.ui.render.ImageSource.Url(withThumb) + } + image.base64?.takeIf { it.isNotBlank() }?.let { + return imageSourceFor("data:${image.mimeType ?: "image/png"};base64,$it") + } + return null +} + +private fun stripHiddenWrappers(text: String): String { + var value = text.trim() + value = value.replace(Regex("^\\s*"), "").trim() + val outer = Regex("^<([a-zA-Z0-9_-]+)(?:\\s(?:[^>\"]|\"[^\"]*\")*)?>([\\s\\S]*)$").find(value) + return outer?.groups?.get(2)?.value?.trim() ?: value +} + +private fun leadingWrapperTitle(text: String): String? { + val match = Regex("""^<(\w+)((?:\s+[\w-]+="[^"]*")*)\s*>""").find(text.trim()) ?: return null + val tag = match.groups[1]!!.value + val attrs = Regex("""([\w-]+)="([^"]*)"""") + .findAll(match.groups[2]!!.value) + .map { it.groups[1]!!.value to it.groups[2]!!.value } + .filter { it.second.isNotEmpty() } + .map { (key, value) -> "$key=$value" } + return (listOf(tag) + attrs).joinToString(" · ") +} + +private fun formatCustomType(type: String): String { + if (type.isBlank()) return "Extension" + return type.replace('-', ' ').replace('_', ' ') + .split(' ') + .joinToString(" ") { word -> word.replaceFirstChar { it.uppercase() } } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatScreen.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatScreen.kt new file mode 100644 index 000000000..863543be1 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatScreen.kt @@ -0,0 +1,1793 @@ +@file:OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class) + +package io.github.kahme247.ompweb.ui.chat + +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.List +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.chat.ChatDisplayPrefs +import io.github.kahme247.ompweb.chat.ChatNotice +import io.github.kahme247.ompweb.chat.ContentionCopy +import io.github.kahme247.ompweb.chat.formatGenerationRate +import io.github.kahme247.ompweb.chat.HeaderBlock +import io.github.kahme247.ompweb.chat.LoadEarlierState +import io.github.kahme247.ompweb.chat.NoticeKind +import io.github.kahme247.ompweb.chat.ScrollLogic +import io.github.kahme247.ompweb.chat.Speaker +import io.github.kahme247.ompweb.chat.SpeakerRole +import io.github.kahme247.ompweb.chat.STREAM_RATE_PUBLISH_MS +import io.github.kahme247.ompweb.chat.TailMeasure +import io.github.kahme247.ompweb.chat.TranscriptFormat +import io.github.kahme247.ompweb.chat.TranscriptRow +import io.github.kahme247.ompweb.events.ConnectionState +import io.github.kahme247.ompweb.net.ElsewhereSession +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.chat.CompactResult +import io.github.kahme247.ompweb.domain.ActiveGoal +import io.github.kahme247.ompweb.domain.ElsewhereKind +import io.github.kahme247.ompweb.domain.elsewhereKind +import io.github.kahme247.ompweb.domain.ExtensionDialogState +import io.github.kahme247.ompweb.domain.ExtensionWidgetPlacement +import io.github.kahme247.ompweb.domain.chatShowsConnecting +import io.github.kahme247.ompweb.domain.formatGoalElapsed +import io.github.kahme247.ompweb.ui.composer.ComposerBar +import io.github.kahme247.ompweb.ui.composer.MinimizedComposerBar +import io.github.kahme247.ompweb.ui.composer.MinimizeComposerButton +import io.github.kahme247.ompweb.ui.panels.ComposerPanels +import io.github.kahme247.ompweb.ui.panels.SubagentTranscriptDialog +import io.github.kahme247.ompweb.ui.panels.btw.BtwFailureKind +import io.github.kahme247.ompweb.ui.panels.btw.BtwHistoryDialog +import io.github.kahme247.ompweb.ui.panels.btw.BtwPanel +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionDialogHost +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionOpenUrlNotice +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionStatusBar +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionTerminalPanel +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionWidgets +import io.github.kahme247.ompweb.ui.render.LinkTarget +import io.github.kahme247.ompweb.ui.render.LocalFileLinkBaseDir +import io.github.kahme247.ompweb.ui.render.copyTextToClipboard +import io.github.kahme247.ompweb.ui.hosttools.openExternalUrl +import io.github.kahme247.ompweb.ui.sessions.SessionExport +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +private const val TRANSCRIPT_WINDOW = 60 +private const val NEAR_BOTTOM_DP = 96 + +/** + * Height of the transcript's tail anchor. Exactly on the pixel grid at every + * density, so it never reads as a gap, but visible to the lazy list, which is + * what lets the follow scroll target it (a zero-height item is not measured + * into the visible range). + */ +private val TRANSCRIPT_TAIL_ANCHOR = 1.dp + +/** Phase 5 chat surface inside the session detail pane. */ +@Composable +fun ChatScreen( + sessionId: String, + cwd: String?, + viewModel: ChatViewModel, + onForked: (String) -> Unit, + onError: (String) -> Unit, + /** A `File` link in the transcript: the shell's Files panel opens the tab. */ + onOpenFile: (path: String, line: Int?) -> Unit, + /** + * Open another session by id: the "continued in a copy" banner links the + * original chat after omp moved this conversation to a copy. + */ + onOpenSession: (String) -> Unit, + modifier: Modifier = Modifier, + /** + * The session list's owner report for this session (`elsewhereSessions`). + * The list hears the server's `running` frames, so it can know the chat is + * run elsewhere before this screen's own live-state read answers; the seed + * is handed to [ChatViewModel.open]. + */ + elsewhere: ElsewhereSession? = null, +) { + val ui by viewModel.ui.collectAsStateWithLifecycle() + val composerState by viewModel.composer.state.collectAsStateWithLifecycle() + val searchState by viewModel.search.collectAsStateWithLifecycle() + val focusManager = LocalFocusManager.current + // Web `ChatWindow`: the chevron collapses the composer into a one-line + // pill; an extension dialog (and a new session) restores it. + var composerMinimized by rememberSaveable { mutableStateOf(false) } + var composerFocusRequested by remember { mutableStateOf(false) } + val composerPlaceholder = stringResource(R.string.composer_placeholder) + val minimizedStatusText = phaseStatusText(phaseLabel(ui.engine)) + val context = LocalContext.current + val btwState by viewModel.btw.state.collectAsStateWithLifecycle() + val extensionDialog by viewModel.extensionUi.dialog.collectAsStateWithLifecycle() + LaunchedEffect(extensionDialog) { + if (extensionDialog != null) composerMinimized = false + } + val extensionStatuses by viewModel.extensionUi.statusItems.collectAsStateWithLifecycle() + val extensionWidgets by viewModel.extensionUi.widgets.collectAsStateWithLifecycle() + val btwVisible = btwState.visible + // Side-question failures are one-shot toasts (web `toastBtwError`). + val btwUnsupportedText = stringResource(R.string.btw_unsupported) + val btwFailedText = stringResource(R.string.btw_failed) + // Resolved during composition: the clipboard label must not read resources + // off `LocalContext` inside a lambda (configuration-aware query lint), and + // the copy result is acknowledged like the web (`btw.copied`/`copyFailed`). + val btwCopyLabel = stringResource(R.string.btw_copy) + val btwCopiedText = stringResource(R.string.btw_copied) + val btwCopyFailedText = stringResource(R.string.btw_copy_failed) + val copyBtwAnswer: (String) -> Unit = { text -> + val copied = copyTextToClipboard(context, btwCopyLabel, text) + Toast.makeText( + context, + if (copied) btwCopiedText else btwCopyFailedText, + Toast.LENGTH_SHORT, + ).show() + } + LaunchedEffect(viewModel, btwUnsupportedText, btwFailedText) { + viewModel.btw.errors.collect { failure -> + val text = when (failure.kind) { + BtwFailureKind.UNSUPPORTED -> btwUnsupportedText + BtwFailureKind.FAILED -> failure.detail ?: btwFailedText + } + Toast.makeText(context, text, Toast.LENGTH_LONG).show() + } + } + val colors = LocalOmpColors.current + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + val lifecycleOwner = LocalLifecycleOwner.current + // Resolved during composition: reading resources from the context inside a + // coroutine can return a stale configuration. + val forkCancelledText = stringResource(R.string.chat_fork_cancelled) + val forkFailedText = stringResource(R.string.chat_fork_failed) + // Resolved here: the reveal effect must not read resources in a coroutine. + val revealMissingText = stringResource(R.string.chat_search_reveal_missing) + + // The list's owner report is part of the key: an owner that appears while + // the chat is open (a `running` frame) seeds the banner without a send. + LaunchedEffect(sessionId, cwd, elsewhere) { viewModel.open(sessionId, cwd, elsewhere) } + // Leaving the chat closes its host/extension consent scopes; [open] + // re-arms them when the surface comes back. + DisposableEffect(viewModel) { onDispose { viewModel.onChatHidden() } } + LaunchedEffect(Unit) { viewModel.prepareSpeech() } + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) viewModel.onForeground() + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + viewModel.stopSpeech() + } + } + LaunchedEffect(viewModel) { + viewModel.events.collect { event -> + when (event) { + is ChatEvent.Forked -> onForked(event.sessionId) + // A refused or failed fork must never look like a dead button. + is ChatEvent.ForkCancelled -> onError(forkCancelledText) + is ChatEvent.ForkFailed -> onError(forkFailedText) + is ChatEvent.Toast -> onError(event.message) + is ChatEvent.CopyText -> { + val copied = copyTextToClipboard(context, "assistant message", event.text, sensitive = false) + if (!copied) onError(event.text) + } + } + } + } + + var pendingDownload by remember { mutableStateOf(null) } + val outputSavedText = stringResource(R.string.chat_output_saved) + val outputSaveFailedText = stringResource(R.string.chat_output_save_failed) + val downloadLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("text/plain"), + ) { uri -> + val path = pendingDownload + pendingDownload = null + if (uri != null && path != null) { + scope.launch { + val bytes = viewModel.bashOutputBytes(path) + if (bytes != null) { + val saved = SessionExport.writeToUri(context, uri, bytes) + Toast.makeText( + context, + if (saved) outputSavedText else outputSaveFailedText, + Toast.LENGTH_SHORT, + ).show() + } + } + } + } + + LaunchedEffect(ui.prefill) { viewModel.adoptPrefill() } + // Live tokens/s is derived from the newest frame; without a tick a stalled + // stream would keep its last number until the next frame. Web runs the same + // 500 ms interval (useStreamRate) and hides the value after 2 s of silence. + LaunchedEffect(viewModel, ui.engine.streamingActive) { + if (!ui.engine.streamingActive) return@LaunchedEffect + while (true) { + delay(STREAM_RATE_PUBLISH_MS) + viewModel.refreshLiveRate() + } + } + LaunchedEffect(ui.busy, ui.engine.bashRunning, ui.sessionId) { + viewModel.composer.setRunning(ui.busy || ui.engine.bashRunning) + } + LaunchedEffect(ui.engine.remoteCommands) { + viewModel.composer.setRemoteCommands(ui.engine.remoteCommands) + } + + val linkHandler: (LinkTarget) -> Unit = { target -> + when (target) { + is LinkTarget.File -> onOpenFile(target.path, target.line) + is LinkTarget.Agent -> viewModel.openSubagent(target.handle) + is LinkTarget.External -> openExternalUrl(context, target.url) + // Unsupported scheme: the row rendered it as plain text. + is LinkTarget.Inert -> Unit + is LinkTarget.GithubIssue -> { + val repo = ui.githubRepo + val owner = if (target.owner.isNotEmpty()) target.owner else repo?.owner + val name = if (target.repo.isNotEmpty()) target.repo else repo?.repo + if (owner != null && name != null) { + openExternalUrl(context, "https://github.com/$owner/$name/issues/${target.number}") + } + } + } + } + val callbacks = remember(linkHandler) { + ChatRowCallbacks( + onLink = linkHandler, + onFork = { entryId, editPrompt -> viewModel.fork(entryId, editPrompt) }, + onLoadThinking = { entryId, blockIndex -> viewModel.loadThinking(entryId, blockIndex) }, + onToggleSpeech = { entryId, key, text -> viewModel.toggleSpeech(entryId, key, text) }, + onStopSpeech = { viewModel.stopSpeech() }, + onLoadBashOutput = { path -> viewModel.loadBashOutput(path) }, + onDownloadBashOutput = { path -> + pendingDownload = path + downloadLauncher.launch(path.substringAfterLast('/').ifBlank { "bash-output.txt" }) + }, + onEditFromHere = { prevAssistantEntryId, text -> viewModel.editFromHere(prevAssistantEntryId, text) }, + ) + } + + val rows = ui.rows + // Day labels and header shapes follow the shared minute clock; the time + // settings revision rebuilds the cached formatters (zone, 12/24-hour) + // without a process restart. + val timeRevision = rememberTimeFormatRevision() + val minuteNow = rememberMinuteNow() + val endAnchored = (rows.size - TRANSCRIPT_WINDOW).coerceAtLeast(0) + // First rendered transcript row; the window is pinned to the tail while the + // reader is at the bottom and frozen (or extended upwards) otherwise. + // + // The window, the follow latch and the first-scroll latch are saveable: + // a configuration change (rotation, fold, resize) recreates the composable + // and the lazy list restores its first item + offset from the saved + // instance state. Restoring the window with them keeps that item pointing + // at the same transcript row; resetting them (as before) re-pinned the + // window to the tail and jumped to the newest row. + var windowStart by rememberSaveable(sessionId) { mutableStateOf(0) } + var paging by remember(sessionId) { mutableStateOf(LoadEarlierState()) } + var pendingReveal by remember(sessionId) { mutableStateOf(null) } + var initialScrollDone by rememberSaveable(sessionId) { mutableStateOf(false) } + var followEnabled by rememberSaveable(sessionId) { mutableStateOf(true) } + val nearBottomThreshold = with(LocalDensity.current) { NEAR_BOTTOM_DP.dp.roundToPx() } + + /** The transcript tail as the list really laid it out, in pixels. */ + fun tailMeasure(): TailMeasure = listState.tailMeasure() + val nearBottom by remember(listState, nearBottomThreshold) { + derivedStateOf { ScrollLogic.isNearBottom(tailMeasure(), nearBottomThreshold) } + } + /** + * A list that has not laid out yet reports "near bottom" (an empty layout + * counts as the tail). Scroll decisions must only trust a measured layout: + * a recreated surface (rotation, fold) would otherwise pin itself to the + * tail before the restored position is measured, which is exactly the jump + * this screen must not make. + */ + fun laidOutNearBottom(): Boolean = + listState.layoutInfo.totalItemsCount > 0 && nearBottom + // The status bar and above-editor widgets ride as the transcript's first + // item (web `ChatWindow`: they scroll with the messages, not as an overlay). + val extensionHeaderVisible = extensionStatuses.isNotEmpty() || + extensionWidgets.any { it.placement == ExtensionWidgetPlacement.AboveEditor } + // The compacted-history notice is the next transcript item (web + // `ChatWindow` renders it inside the scroll container, above the first + // message), so it scrolls away instead of pinning above the composer. + val compactionNoticeVisible = ui.hasCompaction + // The cache-first note is another lead item; the scroll math (rail, anchor + // restore) counts every item above the first row. + val historyUpdatingVisible = ui.engine.historyStale + val transcriptLeadItems = (if (extensionHeaderVisible) 1 else 0) + + (if (compactionNoticeVisible) 1 else 0) + + (if (historyUpdatingVisible) 1 else 0) + val headerOffset = transcriptLeadItems + (if (windowStart > 0) 1 else 0) + + // One more window of older rows, anchored on the row the reader was looking + // at (web `chat-lazy-load.ts` + the `ChatWindow` scroll restore). + fun loadEarlier(revealFirstLoaded: Boolean) { + if (windowStart <= 0) return + val visible = listState.firstVisibleItemIndex + val anchorRow = ScrollLogic.rowIndexForItem(visible, windowStart, transcriptLeadItems) + windowStart = (windowStart - TRANSCRIPT_WINDOW).coerceAtLeast(0) + pendingReveal = if (revealFirstLoaded) { + ScrollLogic.revealLoadedItem(windowStart, transcriptLeadItems) + } else { + ScrollLogic.itemIndexForRow(anchorRow, windowStart, transcriptLeadItems) + } + } + + // Pin the window to the tail whenever the reader is at the bottom. A saved + // reading position restores its own window first, a pending reveal owns + // it, and a surface that has not laid out yet has no position to judge. + // The reveal id is deliberately not a key: its *consume* (right after the + // jump) must not re-pin the window over the row the jump just opened. + LaunchedEffect(endAnchored, nearBottom, ui.restoreScrollIndex) { + if (ui.restoreScrollIndex != null || searchState.revealEntryId != null) return@LaunchedEffect + if (!laidOutNearBottom()) return@LaunchedEffect + windowStart = endAnchored + } + + // A window saved for a longer transcript (branch switch, compaction) must + // not render past the tail on the recreated surface. + LaunchedEffect(rows.size, windowStart) { + val clamped = ScrollLogic.clampWindow(windowStart, rows.size) + if (clamped != windowStart) windowStart = clamped + } + + val messageIndex = remember(ui.engine.messages) { + ui.engine.messages.mapIndexed { index, message -> message.key to index }.toMap() + } + val displayedRows = remember(rows, windowStart) { + if (rows.isEmpty()) emptyList() else rows.subList(windowStart.coerceIn(0, rows.size), rows.size) + } + // Day separators of the rendered window, derived from the rendered message + // sequence (not the header blocks), so a day change inside one turn is + // drawn too. Recomputed when the zone/time settings change. + val dayBreaks = remember(displayedRows, ui.preferences.hideThinking, timeRevision) { + TranscriptFormat.daySeparators(displayedRows, ui.preferences.hideThinking) + } + val userTurns = remember(displayedRows, windowStart) { + userTurnIndices(displayedRows, headerOffset) + } + // The fast-scroll rail is a tablet affordance; phones keep the plain thumb. + val showScrollRail = LocalConfiguration.current.screenWidthDp >= 600 + val streaming = ui.engine.streaming + val pending = ui.engine.pendingUser + // Speaker headers and day dividers: one plan for the rendered window plus + // the optimistic prompt and the streaming reply, so a run of blocks by the + // same speaker shares a single header across items. + val transcriptHeaders = remember( + displayedRows, + pending, + streaming?.key, + streaming?.model, + streaming?.timestamp, + ) { + val blocks = TranscriptFormat.headerBlocks(displayedRows).toMutableList() + if (pending != null) { + blocks += HeaderBlock("pending", Speaker(SpeakerRole.You), null) + } + if (streaming != null) { + blocks += HeaderBlock("streaming", TranscriptFormat.speakerOf(streaming), streaming.timestamp) + } + TranscriptFormat.headerSlots(blocks) + } + // The tail anchor is the last item: a hairline spacer, so scrolling to it + // lands on the very bottom whatever the height of the rows above it. + val totalItems = displayedRows.size + headerOffset + (if (pending != null) 1 else 0) + + (if (streaming != null) 1 else 0) + 1 + + // Follow the tail while the reader has not scrolled away from it; sending a + // prompt re-enables following (web `useAgentSession` send scroll intent). + // + // The trigger is the *measured* layout, not the message list: text, tool + // output and folding grow inside existing rows, and the keyboard or a panel + // shrinks the viewport — none of that changes a row count. A content-keyed + // effect also ran before the list had measured the new content, so it + // scrolled to the previous bottom and left the newest lines below the fold. + // The layout snapshot is emitted after a measure pass, which is what makes + // the follow land on the newest text. + LaunchedEffect(listState) { + snapshotFlow { listState.tailMeasure() }.collect { measure -> + if (!followEnabled) return@collect + // A saved reading position owns the scroll until the reader + // decides otherwise; following it would yank them to the tail. A + // pending reveal of a server-side search hit owns it too. + if (ui.restoreScrollIndex != null || searchState.revealEntryId != null) return@collect + if (!ScrollLogic.needsTailScroll(measure)) return@collect + listState.scrollToTail() + } + } + + // A manual drag leaves the tail (the reader wins over the stream). Flinging + // continues after the finger lifts, so following is only re-enabled once + // the scroll settled — and only when it settled at the tail. + LaunchedEffect(listState) { + var settle: Job? = null + listState.interactionSource.interactions.collect { interaction -> + when (interaction) { + is DragInteraction.Start -> { + settle?.cancel() + followEnabled = false + } + is DragInteraction.Stop, is DragInteraction.Cancel -> { + settle?.cancel() + settle = scope.launch { + snapshotFlow { listState.isScrollInProgress }.first { !it } + // The release is reported before the list measures the + // new offset: judge the tail only after that pass, or a + // drag away from the bottom would re-arm following + // on the stale (still at bottom) layout. + withFrameNanos { } + withFrameNanos { } + followEnabled = ScrollLogic.isNearBottom(listState.tailMeasure(), nearBottomThreshold) + } + } + } + } + } + + // Open a session at its newest message unless a reading position is saved + // or a server-side search hit is waiting to be revealed: the reveal owns + // the scroll then (it may land two frames before this effect's tail jump). + LaunchedEffect(rows.size, ui.engine.historyLoaded, ui.restoreScrollIndex) { + if (initialScrollDone || rows.isEmpty() || ui.restoreScrollIndex != null) return@LaunchedEffect + if (searchState.revealEntryId != null) { + initialScrollDone = true + return@LaunchedEffect + } + initialScrollDone = true + withFrameNanos { } + withFrameNanos { } + listState.scrollToTail() + } + + // Restore a saved reading position once the transcript has content. A + // pending reveal of a server-side search hit wins: the reader asked for + // that entry, not for where they stopped reading last time. + LaunchedEffect(ui.restoreScrollIndex, rows.size, searchState.revealEntryId) { + val restore = ui.restoreScrollIndex ?: return@LaunchedEffect + if (rows.isEmpty()) return@LaunchedEffect + initialScrollDone = true + if (searchState.revealEntryId != null) { + viewModel.consumeRestoreScroll() + return@LaunchedEffect + } + val target = restore.coerceIn(0, rows.size - 1) + windowStart = minOf(target, endAnchored) + // Let the wider window compose before scrolling to the row. + withFrameNanos { } + withFrameNanos { } + val item = ScrollLogic.itemIndexForRow(target, windowStart, transcriptLeadItems) + .coerceIn(0, (listState.layoutInfo.totalItemsCount - 1).coerceAtLeast(0)) + listState.scrollToItem(item) + // The restored position decides whether following resumes: once the + // restore flag clears, the measured follow effect would otherwise drag + // a saved mid-transcript position down to the tail. + withFrameNanos { } + followEnabled = ScrollLogic.followAfterRestore( + restoredRow = target, + rowCount = rows.size, + measure = listState.tailMeasure(), + thresholdPx = nearBottomThreshold, + ) + viewModel.consumeRestoreScroll() + } + + // Keep a freshly prepended window from moving the reader's row. + LaunchedEffect(pendingReveal, windowStart) { + val row = pendingReveal ?: return@LaunchedEffect + pendingReveal = null + withFrameNanos { } + val total = listState.layoutInfo.totalItemsCount + if (total == 0) return@LaunchedEffect + listState.scrollToItem(row.coerceIn(0, total - 1)) + } + + // Persist the reading position (row index, not the lazy item index). + LaunchedEffect(listState, windowStart, transcriptLeadItems) { + snapshotFlow { listState.firstVisibleItemIndex }.collect { item -> + val rowIndex = ScrollLogic.rowIndexForItem(item, windowStart, transcriptLeadItems) + viewModel.onScrollPosition(rowIndex, ScrollLogic.isNearBottom(listState.tailMeasure(), nearBottomThreshold)) + } + } + + // Load one more window when the reader reaches the top. The latch arms only + // after the tail was seen and a non-zero first item was visible, so a + // prepended window cannot re-trigger itself. + LaunchedEffect(listState, windowStart) { + snapshotFlow { listState.firstVisibleItemIndex }.collect { item -> + val decision = ScrollLogic.decideLoadEarlier( + firstVisibleItemIndex = item, + windowStart = windowStart, + nearBottom = ScrollLogic.isNearBottom(listState.tailMeasure(), nearBottomThreshold), + state = paging, + ) + paging = decision.state + if (decision.expand) loadEarlier(revealFirstLoaded = false) + } + } + + // ------------------------------------------------------------- chat search + + // The search marks must describe the transcript they were typed against: + // a streamed reply or a reloaded history changes the hit list, and so does + // hiding the thinking text the query used to match. The query itself is + // not a key: `setSearchQuery` already refreshes, so keying it would scan + // the whole transcript twice per keystroke. + LaunchedEffect(searchState.open, ui.rows.size, ui.engine.messages.size, ui.preferences.hideThinking) { + if (searchState.open) viewModel.refreshSearchHits() + } + + /** + * Show [rowIndex] in the transcript, even when it sits above the rendered + * window, and stop following the tail so the jump is not undone by a + * running turn. Marks it as the search's active hit. + */ + fun revealRow(rowIndex: Int) { + if (rows.isEmpty()) return + val target = rowIndex.coerceIn(0, rows.lastIndex) + if (target < windowStart) { + windowStart = (target / TRANSCRIPT_WINDOW) * TRANSCRIPT_WINDOW + } + followEnabled = false + pendingReveal = ScrollLogic.itemIndexForRow(target, windowStart, transcriptLeadItems) + } + + // ↑/↓ and the first hit own the scroll while the field is open. + val activeHitKey = searchState.active?.rowKey + LaunchedEffect(searchState.open, activeHitKey, ui.rows.size) { + if (!searchState.open) return@LaunchedEffect + val key = activeHitKey ?: return@LaunchedEffect + val index = ChatSearch.rowIndexForRowKey(ui.rows, key) ?: return@LaunchedEffect + revealRow(index) + } + + // Hand-off of the server-side search (`ui/sessions`): scroll to the entry + // and flash its row. A request that arrives before the rows exist stays + // queued in the view model and is applied by this effect once they do. + // The hit may also live in history the compaction view hides; that history + // is loaded instead of dropping the jump, and a hit that is really absent + // is reported rather than swallowed. + var flashRowKey by remember(sessionId) { mutableStateOf(null) } + LaunchedEffect( + searchState.revealEntryId, + ui.rows.size, + ui.engine.historyLoaded, + ui.engine.historyLoading, + ui.engine.pagesLoading, + ui.engine.historyError, + ui.hasCompaction, + ui.engine.includePreCompaction, + ) { + val entry = searchState.revealEntryId ?: return@LaunchedEffect + val index = ChatSearch.rowIndexForEntry(ui.rows, ui.engine.messages, entry) + if (index != null) { + revealRow(index) + flashRowKey = ui.rows[index].key + viewModel.consumeRevealEntry() + return@LaunchedEffect + } + if (ui.hasCompaction && !ui.engine.includePreCompaction) { + viewModel.togglePreCompactionHistory() + return@LaunchedEffect + } + val failure = ui.engine.historyError + if (failure != null) { + viewModel.consumeRevealEntry() + Toast.makeText(context, failure, Toast.LENGTH_LONG).show() + return@LaunchedEffect + } + if (!ui.engine.historyLoaded || ui.engine.historyLoading || ui.engine.pagesLoading) { + return@LaunchedEffect + } + viewModel.consumeRevealEntry() + Toast.makeText(context, revealMissingText, Toast.LENGTH_SHORT).show() + } + LaunchedEffect(flashRowKey) { + val key = flashRowKey ?: return@LaunchedEffect + delay(2_500L) + if (flashRowKey == key) flashRowKey = null + } + val matchedRowKeys = remember(searchState.hits) { + searchState.hits.mapTo(HashSet()) { it.rowKey } + } + // Measured height of the in-chat search bar above the transcript. The plan + // does not compose that bar, so it reserves the measured height to keep the + // 45 % transcript share while the search is open. + var searchBarHeight by remember { mutableStateOf(0.dp) } + val density = LocalDensity.current + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .imePadding(), + ) { + // The keyboard shrinks the pane to the space above the IME; the plan + // splits what is left (user feedback 25.10.: the transcript shrank to + // four lines while typing). The transcript keeps at least 45 % of it, + // the composer caps there and scrolls its secondary rows, and on a + // phone the panels stack and the controls row step aside while the + // keyboard covers the screen. They come back when the IME closes; the + // controls stay reachable before the field takes focus. + val fontScale = LocalDensity.current.fontScale + val wide = LocalConfiguration.current.screenWidthDp >= ChatLayout.WIDE_SCREEN_DP + val plan = ChatLayout.plan( + availableHeight = maxHeight, + imeVisible = WindowInsets.isImeVisible, + wide = wide, + fontScale = fontScale, + reservedHeight = if (searchState.open) searchBarHeight else 0.dp, + ) + val composerMaxHeight = plan.composerMaxHeight + val motionEnabled = rememberSystemMotionEnabled() + CompositionLocalProvider( + LocalFileLinkBaseDir provides cwd, + LocalChatMotion provides motionEnabled, + LocalChatNow provides minuteNow, + LocalTimeFormatRevision provides timeRevision, + ) { + Surface(color = colors.background, modifier = Modifier.fillMaxSize()) { + Column(Modifier.fillMaxSize()) { + if (searchState.open) { + ChatSearchBar( + state = searchState, + // Pre-compaction entries are not part of the loaded + // transcript, so the local search cannot find them. + olderHistoryHidden = ui.hasCompaction && !ui.engine.includePreCompaction, + fullHistoryBusy = ui.engine.pagesLoading || ui.engine.historyLoading, + onQueryChange = viewModel::setSearchQuery, + onStep = viewModel::stepSearch, + onClose = { + focusManager.clearFocus() + viewModel.closeSearch() + }, + // Loading the full history is what makes the older + // entries searchable; the query re-runs by itself. + onSearchFullHistory = { viewModel.togglePreCompactionHistory() }, + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 5.dp) + .onSizeChanged { size -> + searchBarHeight = with(density) { size.height.toDp() } + }, + ) + } + Box(Modifier.weight(1f).fillMaxWidth()) { + val compact = LocalConfiguration.current.screenWidthDp < 600 + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxHeight() + .widthIn(max = 900.dp) + .fillMaxWidth() + .align(Alignment.TopCenter), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + start = 10.dp, + end = 10.dp, + top = 8.dp, + bottom = 10.dp, + ), + ) { + if (extensionHeaderVisible) { + item(key = "extension-header") { + ExtensionStatusBar(controller = viewModel.extensionUi) + } + } + if (compactionNoticeVisible) { + item(key = "compaction-notice") { + CompactionNotice( + includePreCompaction = ui.engine.includePreCompaction, + onToggle = viewModel::togglePreCompactionHistory, + ) + } + } + // Cache-first: the transcript on screen came from + // disk and the server answer is still on its way. + // One muted line, no spinner — the cached rows are + // readable and the live load replaces them. + if (ui.engine.historyStale) { + item(key = "history-updating") { + Text( + text = stringResource(R.string.chat_history_updating), + color = colors.textDim, + fontSize = 11.sp, + modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), + ) + } + } + if (rows.isEmpty() && streaming == null && pending == null) { + item(key = "state") { + TranscriptEmptyState( + loading = ui.engine.historyLoading && !ui.engine.historyLoaded, + error = ui.engine.historyError, + onRetry = viewModel::retryHistory, + modifier = Modifier.fillParentMaxSize(), + ) + } + } + if (windowStart > 0) { + item(key = "earlier") { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { loadEarlier(revealFirstLoaded = true) } + .padding(vertical = 10.dp), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = if (ui.engine.pagesLoading) stringResource(R.string.chat_earlier_loading) + else stringResource(R.string.chat_earlier), + color = colors.accent, + fontSize = 12.sp, + ) + } + } + } + items( + count = displayedRows.size, + key = { index -> displayedRows[index].key }, + ) { index -> + val row = displayedRows[index] + val absoluteIndex = windowStart + index + val liveTail = ChatDisplayPrefs.liveTail( + hideToolActivity = ui.preferences.hideToolActivity, + streaming = ui.engine.streamingActive, + isLastGroup = absoluteIndex == rows.lastIndex && + row is TranscriptRow.Group, + ) + ChatRow( + row = row, + messages = ui.engine.messages, + state = ui, + liveTail = liveTail, + indexByKey = messageIndex, + callbacks = callbacks, + // The last group of a turn that still runs + // keeps the fold informed (live activity, + // counters, elapsed) until it ends. + runningTurn = ui.busy && absoluteIndex == rows.lastIndex && + row is TranscriptRow.Group, + highlight = when { + row.key == activeHitKey || row.key == flashRowKey -> + ChatRowHighlight.Active + row.key in matchedRowKeys -> ChatRowHighlight.Match + else -> ChatRowHighlight.None + }, + headers = transcriptHeaders, + dayBreaks = dayBreaks, + ) + } + pending?.let { pendingUser -> + item(key = "pending") { + PendingUserBubble( + text = pendingUser.text, + header = transcriptHeaders["pending"], + ) + } + } + streaming?.let { message -> + item(key = "streaming") { + AssistantRow( + message = message, + state = ui, + forkTarget = null, + streaming = true, + callbacks = callbacks, + header = transcriptHeaders["streaming"], + ) + } + } + // The tail anchor: the follow scroll targets this + // last, hairline item, so it lands on the very + // bottom regardless of the height of the rows above + // it (web `messagesEndRef` + `scrollIntoView`). + item(key = "tail-anchor") { + Spacer(Modifier.height(TRANSCRIPT_TAIL_ANCHOR)) + } + } + + if (totalItems > 0 && showScrollRail) { + ChatScrollRail( + listState = listState, + totalItems = listState.layoutInfo.totalItemsCount, + userTurns = userTurns, + modifier = Modifier + .align(Alignment.CenterEnd) + .fillMaxHeight() + .padding(end = 1.dp), + ) + } + + if (rows.isNotEmpty() && !nearBottom) { + FloatingActionButton( + onClick = { + followEnabled = true + scope.launch { listState.scrollToTail() } + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = if (compact) 20.dp else 26.dp, bottom = 14.dp), + containerColor = colors.panel, + contentColor = colors.textMuted, + shape = CircleShape, + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource(R.string.chat_jump_latest), + ) + } + } + } + + Box(Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .widthIn(max = 900.dp) + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 5.dp), + verticalArrangement = Arrangement.spacedBy(5.dp), + ) { + ChatBanners(ui = ui, onRetryHistory = viewModel::retryHistory) + ForeignRunBanner( + elsewhere = ui.engine.foreignRun, + sendAnywayEnabled = composerState.hasContent, + onSendAnyway = viewModel::requestForeignSend, + ) + ContentionForkBanner( + copy = ui.contentionCopy, + onOpen = { id -> + onOpenSession(id) + viewModel.dismissContentionFork() + }, + onDismiss = viewModel::dismissContentionFork, + ) + ExtensionOpenUrlNotice(viewModel.extensionUi) + if (ui.notices.isNotEmpty()) { + Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { + ui.notices.takeLast(5).forEach { notice -> + NoticeBar(notice = notice, onDismiss = { viewModel.dismissNotice(notice) }) + } + } + } + ComposerModeStatus(goal = ui.activeGoal, plan = ui.planObjective) + CompactBanner(ui.compactResult, ui.compactError) + // The attached extension dialog sits above the + // panels, inside the composer column (web places it + // in the panel stack, not as an overlay). + ExtensionDialogHost(viewModel.extensionUi) + // Web `ChatWindow`: startup skill-conflict notice + // sits above the panels, in normal chat and in the + // empty new chat alike. + SkillDiagnosticsNotice( + snapshot = ui.engine.skillDiagnostics, + saving = ui.skillDiagnosticsSaving, + failure = ui.skillDiagnosticsFailure, + onDisable = { viewModel.setSkillStartupDiagnostics(false) }, + ) + // While the keyboard is open on a phone the attached + // panels (todo / subagents / btw) step aside: they + // are a reading surface, and the transcript needs + // the height. Their collapsed state is kept, so they + // are back exactly as they were when the IME closes. + if (!plan.hidePanels) { + ComposerPanels( + todoPhases = ui.engine.todoPhases, + subagents = ui.engine.subagents, + todoCollapsed = ui.todoCollapsed, + subagentsCollapsed = ui.subagentsCollapsed, + onTodoCollapsedChange = viewModel::setTodoCollapsed, + onSubagentsCollapsedChange = viewModel::setSubagentsCollapsed, + onSelectSubagent = { info -> viewModel.openSubagent(info) }, + leading = if (btwVisible) { + { + BtwPanel( + vm = viewModel.btw, + onCopy = copyBtwAnswer, + onLink = linkHandler, + ) + } + } else { + null + }, + ) + } + // A modal extension dialog owns the screen; the + // composer stays composed (draft/IME) but takes no + // space (web hides the input, keeps its state). + val composerHidden = + composerMinimized || extensionDialog is ExtensionDialogState.Ask + // The composer stays composed while minimized (draft, + // attachments and a recording survive) but takes no + // space, like the web's `display: none`. + Box( + modifier = if (composerHidden) { + Modifier + .fillMaxWidth() + .height(0.dp) + // TalkBack must not reach controls that are + // visually hidden (web `display: none`). + .clearAndSetSemantics {} + } else { + Modifier + .fillMaxWidth() + .heightIn(max = composerMaxHeight) + .verticalScroll(rememberScrollState()) + }, + ) { + Column(Modifier.fillMaxWidth()) { + if (!composerHidden) { + MinimizeComposerButton( + onMinimize = { composerMinimized = true }, + modifier = Modifier.padding(vertical = 2.dp), + ) + } + ComposerBar( + composer = viewModel.composer, + controls = viewModel.controls, + dictation = viewModel.dictation, + ghost = viewModel.ghost.collectAsStateWithLifecycle().value, + placeholder = composerPlaceholder, + streaming = ui.busy, + stopping = ui.stopping, + bashRunning = ui.engine.bashRunning, + queue = ui.engine.queue, + autoRetry = ui.engine.autoRetry, + submitDuringRun = ui.submitDuringRun, + submitting = ui.submitPending, + // While the submit waits for the live + // state the field is locked, so the + // routed text cannot be edited away. + inputEnabled = !ui.submitPending, + onSend = { + followEnabled = true + scope.launch { listState.scrollToTail() } + viewModel.submit() + }, + onQueue = { + followEnabled = true + viewModel.submit(forceQueue = true) + }, + onStop = viewModel::stop, + onAbortRetry = viewModel::abortRetry, + onAcceptGhost = { viewModel.acceptGhost() }, + onHistory = viewModel::openHistory, + onQueueEdit = viewModel::editQueued, + onQueueDelete = viewModel::deleteQueued, + onQueueSteer = viewModel::steerQueued, + autoFocus = composerFocusRequested, + onMinimize = { composerMinimized = true }, + textFieldMaxHeight = plan.textFieldMaxHeight, + textFieldMaxLines = plan.textFieldMaxLines, + showControlsRow = !plan.hideControlsRow, + ) + } + } + if (composerMinimized && extensionDialog !is ExtensionDialogState.Ask) { + MinimizedComposerBar( + draftPreview = composerState.text, + hasAttachments = composerState.images.isNotEmpty() || + composerState.files.isNotEmpty(), + placeholder = composerPlaceholder, + statusText = minimizedStatusText, + streaming = ui.busy, + onExpand = { + composerMinimized = false + composerFocusRequested = true + }, + onStop = viewModel::stop, + ) + } + ExtensionWidgets( + controller = viewModel.extensionUi, + placement = ExtensionWidgetPlacement.BelowEditor, + ) + ChatFooter(ui = ui) + } + } + } + } + } + + ui.branch?.let { branch -> + BranchNavigatorDialog( + state = branch, + onSelect = { leaf -> viewModel.selectLeaf(leaf) }, + onDismiss = viewModel::closeBranchNavigator, + ) + } + ui.subagent?.let { dialog -> + SubagentTranscriptDialog( + state = dialog, + githubRepo = ui.githubRepo, + onLink = linkHandler, + onToggleTranscript = viewModel::toggleSubagentTranscript, + onLoadMore = viewModel::loadMoreSubagentTranscript, + onDismiss = viewModel::closeSubagent, + ) + } + BtwHistoryDialog( + vm = viewModel.btw, + onCopy = copyBtwAnswer, + onLink = linkHandler, + ) + // GAP 1: read-only system prompt panel, opened from the top bar. + SystemPromptSheet(state = ui.systemPrompt, onDismiss = viewModel::closeSystemPrompt) + ui.foreignSend?.let { + ForeignSendDialog( + onSendAnyway = viewModel::confirmForeignSend, + onCancel = viewModel::cancelForeignSend, + ) + } + // Slice C: the terminal sheet (renders nothing while the controller + // has no terminal). The dialog and status band live in the layout above. + ExtensionTerminalPanel(viewModel.extensionUi) + } +} + +// ------------------------------------------------------------------ helpers + +/** The transcript tail in pixels; see [TailMeasure]. */ +private fun LazyListState.tailMeasure(): TailMeasure { + val info = layoutInfo + val last = info.visibleItemsInfo.lastOrNull() + return TailMeasure( + totalItems = info.totalItemsCount, + lastVisibleIndex = last?.index ?: -1, + lastVisibleEnd = last?.let { it.offset + it.size } ?: 0, + viewportEnd = info.viewportEndOffset, + ) +} + +/** + * Jump to the transcript's tail anchor (the hairline spacer that is always the + * last item). Its item start is the end of the content, so the request is + * clamped to the maximum scroll offset — the list lands on the very bottom of + * whatever the rows above it measured to, with no separate delta pass that + * would read a stale layout. + */ +private suspend fun LazyListState.scrollToTail() { + val total = layoutInfo.totalItemsCount + if (total == 0) return + scrollToItem(total - 1) +} + +// ------------------------------------------------------------------ pieces + +@Composable +private fun TranscriptEmptyState( + loading: Boolean, + error: String?, + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(24.dp), + ) { + when { + loading -> { + CircularProgressIndicator(strokeWidth = 2.dp, color = colors.accent) + Text(stringResource(R.string.chat_loading), color = colors.textMuted, fontSize = 13.sp) + } + + error != null -> { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + tint = colors.statusError, + ) + Text(stringResource(R.string.chat_load_failed), color = colors.text, fontSize = 14.sp) + Text( + text = error, + color = colors.textDim, + fontSize = 12.sp, + textAlign = TextAlign.Center, + ) + TextButton(onClick = onRetry) { Text(stringResource(R.string.chat_retry)) } + } + + else -> { + Text(stringResource(R.string.chat_empty_title), color = colors.text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold) + Text( + text = stringResource(R.string.chat_empty_hint), + color = colors.textDim, + fontSize = 12.5.sp, + textAlign = TextAlign.Center, + ) + } + } + } + } +} + +@Composable +private fun NoticeBar(notice: ChatNotice, onDismiss: () -> Unit) { + val colors = LocalOmpColors.current + val tone = when (notice.kind) { + NoticeKind.Error -> colors.statusError + NoticeKind.Warning -> colors.statusWarning + NoticeKind.Info -> colors.accent + } + LaunchedEffect(notice) { + kotlinx.coroutines.delay(if (notice.kind == NoticeKind.Error) 30_000L else 5_000L) + onDismiss() + } + Surface( + color = tone.copy(alpha = 0.10f), + contentColor = tone, + shape = RoundedCornerShape(7.dp), + modifier = Modifier + .fillMaxWidth() + .semantics { liveRegion = LiveRegionMode.Polite } + .border(1.dp, tone.copy(alpha = 0.35f), RoundedCornerShape(7.dp)), + ) { + Row( + modifier = Modifier.padding(start = 9.dp, end = 2.dp, top = 2.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = notice.message, + fontSize = 11.5.sp, + modifier = Modifier.weight(1f), + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + IconButton(onClick = onDismiss) { + Icon(Icons.Filled.Close, contentDescription = stringResource(R.string.chat_dismiss), modifier = Modifier.size(14.dp)) + } + } + } +} + +@Composable +internal fun ChatBanners( + ui: ChatUiState, + onRetryHistory: () -> Unit, +) { + val colors = LocalOmpColors.current + val engine = ui.engine + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + engine.autoRetry?.let { retry -> + val attempt = listOfNotNull(retry.attempt?.let { "attempt $it" }, retry.maxAttempts?.let { "of $it" }) + .joinToString(" ") + BannerRow( + text = listOfNotNull( + if (attempt.isNotEmpty()) stringResource(R.string.chat_banner_retrying, attempt) + else stringResource(R.string.chat_banner_retrying_plain), + retry.message, + ).joinToString(" — "), + tone = colors.statusWarning, + action = null, + onAction = {}, + ) + } + if (ui.connection != ConnectionState.Connected) { + BannerRow( + text = when (ui.connection) { + ConnectionState.Reconnecting -> stringResource(R.string.chat_banner_reconnecting) + ConnectionState.Offline -> stringResource(R.string.chat_banner_offline) + ConnectionState.Connected -> "" + }, + tone = colors.statusWarning, + action = null, + onAction = {}, + ) + } + engine.error?.takeIf { it.isNotBlank() }?.let { error -> + BannerRow( + text = error, + tone = colors.statusError, + action = null, + onAction = {}, + ) + } + engine.historyError?.takeIf { it.isNotBlank() && engine.historyLoaded }?.let { error -> + BannerRow( + text = error, + tone = colors.statusError, + action = stringResource(R.string.chat_retry), + onAction = onRetryHistory, + ) + } + } +} + +/** + * Web `ChatWindow`'s compacted-history notice. It is the transcript's own row + * (rendered inside the scroll container, above the first message), so it + * scrolls away with the messages instead of pinning above the composer. + */ +@Composable +internal fun CompactionNotice(includePreCompaction: Boolean, onToggle: () -> Unit) { + BannerRow( + text = stringResource( + if (includePreCompaction) { + R.string.chat_branch_banner_pre_shown + } else { + R.string.chat_branch_banner_compacted + }, + ), + tone = LocalOmpColors.current.textMuted, + action = stringResource( + if (includePreCompaction) { + R.string.chat_branch_hide_earlier + } else { + R.string.chat_branch_show_earlier + }, + ), + onAction = onToggle, + ) +} + +/** + * Web `ComposerModeStatus`: the running goal (expandable, elapsed time) and… + * plan banner for a `/plan` turn. Both are status-only, never interactive work. + */ +@Composable +internal fun ComposerModeStatus(goal: ActiveGoal?, plan: String?) { + if (goal == null && plan == null) return + val colors = LocalOmpColors.current + var expanded by remember(goal?.startedAt, goal?.objective) { mutableStateOf(false) } + var now by remember(goal?.startedAt) { mutableStateOf(System.currentTimeMillis()) } + // The elapsed label is minute-granular; a 30 s tick keeps it honest. + LaunchedEffect(goal?.startedAt) { + if (goal == null) return@LaunchedEffect + while (true) { + now = System.currentTimeMillis() + kotlinx.coroutines.delay(30_000L) + } + } + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + if (goal != null) { + Surface( + color = colors.accent.copy(alpha = 0.07f), + contentColor = colors.text, + shape = RoundedCornerShape(6.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, colors.accent.copy(alpha = 0.32f)), + modifier = Modifier + .fillMaxWidth() + .clickable( + role = Role.Button, + onClickLabel = stringResource( + if (expanded) R.string.panels_goal_collapse else R.string.panels_goal_expand, + ), + onClick = { expanded = !expanded }, + ), + ) { + Row( + modifier = Modifier.padding(horizontal = 9.dp, vertical = 5.dp), + verticalAlignment = if (expanded) Alignment.Top else Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(R.string.panels_goal_active) + " · " + + formatGoalElapsed(now - goal.startedAt), + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + ) + Text( + text = goal.objective, + color = colors.text, + fontSize = 12.sp, + maxLines = if (expanded) Int.MAX_VALUE else 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } + } + } + if (plan != null) { + Surface( + color = colors.panel, + contentColor = colors.textMuted, + shape = RoundedCornerShape(6.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, colors.border), + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.padding(horizontal = 9.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(7.dp), + ) { + Icon( + imageVector = Icons.Filled.List, + contentDescription = null, + tint = colors.accent, + modifier = Modifier.size(14.dp), + ) + Text( + text = stringResource(R.string.panels_plan_in_progress), + color = colors.textMuted, + fontSize = 11.5.sp, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = plan, + color = colors.textDim, + fontSize = 11.5.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } + } + } + } +} + +/** + * Web `foreignRun` banner: another omp process owns this chat, so the send is + * held back. The text stays in the composer and the transcript keeps + * following the file; "send anyway" is the only way into a copy. + * + * [ElsewhereKind] names the owner, [ElsewhereSource] is not shown on purpose: + * a user cares who runs the chat, not how the server proved it (the + * confirmation dialog covers the weak-evidence case). + */ +@Composable +internal fun ForeignRunBanner( + elsewhere: ElsewhereSession?, + sendAnywayEnabled: Boolean, + onSendAnyway: () -> Unit, +) { + if (elsewhere == null) return + val colors = LocalOmpColors.current + val body = stringResource( + when (elsewhereKind(elsewhere.kind)) { + ElsewhereKind.OmpWeb -> R.string.composer_elsewhere_body_omp_web + ElsewhereKind.Terminal -> R.string.composer_elsewhere_body_terminal + ElsewhereKind.Unknown -> R.string.composer_elsewhere_body_unknown + }, + ) + Surface( + color = colors.statusWarning.copy(alpha = 0.08f), + contentColor = colors.text, + shape = RoundedCornerShape(6.dp), + border = BorderStroke(1.dp, colors.statusWarning.copy(alpha = 0.35f)), + modifier = Modifier + .fillMaxWidth() + .semantics { liveRegion = LiveRegionMode.Polite }, + ) { + Row( + modifier = Modifier.padding(start = 9.dp, end = 3.dp, top = 5.dp, bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(7.dp), + ) { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + tint = colors.statusWarning, + modifier = Modifier.size(14.dp), + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.composer_elsewhere_title), + color = colors.statusWarning, + fontSize = 11.5.sp, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = body, + color = colors.textMuted, + fontSize = 11.sp, + ) + } + TextButton(onClick = onSendAnyway, enabled = sendAnywayEnabled) { + Text( + text = stringResource(R.string.composer_elsewhere_send_anyway), + fontSize = 11.sp, + color = if (sendAnywayEnabled) colors.accent else colors.textDim, + ) + } + } + } +} + +/** + * Web `session_contention_fork` notice: a send raced the other owner, so omp + * continued the conversation in a copy with a new id. The copy holds the text; + * this banner keeps both chats reachable (the original stayed with the other + * process). Persistent until opened or dismissed, because a fork is never + * routine. + */ +@Composable +internal fun ContentionForkBanner( + copy: ContentionCopy?, + onOpen: (String) -> Unit, + onDismiss: () -> Unit, +) { + if (copy == null) return + val colors = LocalOmpColors.current + Surface( + color = colors.statusWarning.copy(alpha = 0.08f), + contentColor = colors.text, + shape = RoundedCornerShape(6.dp), + border = BorderStroke(1.dp, colors.statusWarning.copy(alpha = 0.35f)), + modifier = Modifier + .fillMaxWidth() + .semantics { liveRegion = LiveRegionMode.Polite }, + ) { + Column( + modifier = Modifier.padding(start = 9.dp, end = 3.dp, top = 5.dp, bottom = 3.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(7.dp), + ) { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + tint = colors.statusWarning, + modifier = Modifier.size(14.dp), + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.composer_contention_title), + color = colors.statusWarning, + fontSize = 11.5.sp, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = stringResource(R.string.composer_contention_body), + color = colors.textMuted, + fontSize = 11.sp, + ) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton(onClick = { onOpen(copy.originalId) }) { + Text( + text = stringResource(R.string.composer_contention_open_original), + fontSize = 11.sp, + color = colors.accent, + ) + } + copy.copyId?.let { copyId -> + TextButton(onClick = { onOpen(copyId) }) { + Text( + text = stringResource(R.string.composer_contention_open_copy), + fontSize = 11.sp, + color = colors.accent, + ) + } + } + TextButton(onClick = onDismiss) { + Text( + text = stringResource(R.string.composer_contention_dismiss), + fontSize = 11.sp, + color = colors.textMuted, + ) + } + } + } + } +} + +/** + * The confirmation before a send into a chat another omp process owns: omp + * moves the chat to a copy with a new id on the first write, and that must be + * a deliberate choice. "Keep reading" is the safe default. + */ +@Composable +internal fun ForeignSendDialog( + onSendAnyway: () -> Unit, + onCancel: () -> Unit, +) { + OmpAlertDialog( + onDismissRequest = onCancel, + title = { Text(stringResource(R.string.composer_elsewhere_confirm_title)) }, + text = { Text(stringResource(R.string.composer_elsewhere_confirm_body)) }, + confirmButton = { + TextButton(onClick = onSendAnyway) { + Text(stringResource(R.string.composer_elsewhere_confirm_send)) + } + }, + dismissButton = { + TextButton(onClick = onCancel) { + Text(stringResource(R.string.composer_elsewhere_confirm_cancel)) + } + }, + ) +} + +/** Web `compactResult` / `compactError`: transient banners (6 s / 3 s). */ +@Composable +private fun CompactBanner(result: CompactResult?, error: String?) { + val colors = LocalOmpColors.current + if (result != null) { + val reason = result.reason + val verb = if (reason.isNotBlank() && reason != "manual") { + stringResource( + R.string.panels_compacted_with_reason, + reason.replaceFirstChar { it.uppercase() }, + ) + } else { + stringResource(R.string.panels_compacted) + } + val saved = (result.tokensBefore - result.estimatedTokensAfter).coerceAtLeast(0L) + Row( + modifier = Modifier + .fillMaxWidth() + .background(colors.statusSuccess.copy(alpha = 0.08f), RoundedCornerShape(6.dp)) + .padding(horizontal = 9.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = null, + tint = colors.statusSuccess, + modifier = Modifier.size(12.dp), + ) + Text( + text = stringResource( + R.string.panels_compact_result, + verb, + result.tokensBefore, + result.estimatedTokensAfter, + saved, + ), + color = colors.statusSuccess, + fontSize = 11.5.sp, + ) + } + } + if (error != null) { + BannerRow( + text = stringResource(R.string.panels_compact_error, error), + tone = colors.statusError, + action = null, + onAction = {}, + ) + } +} + +@Composable +private fun BannerRow(text: String, tone: androidx.compose.ui.graphics.Color, action: String?, onAction: () -> Unit) { + if (text.isBlank()) return + val colors = LocalOmpColors.current + Surface( + color = tone.copy(alpha = 0.08f), + contentColor = tone, + shape = RoundedCornerShape(6.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .semantics { liveRegion = LiveRegionMode.Polite } + .padding(start = 9.dp, end = 3.dp, top = 2.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text, fontSize = 11.5.sp, modifier = Modifier.weight(1f)) + if (action != null) { + TextButton(onClick = onAction) { + Text(action, fontSize = 11.sp, color = colors.accent) + } + } + } + } +} + +@Composable +private fun phaseStatusText(label: PhaseLabel?): String? = when (label?.kind) { + null -> null + PhaseLabelKind.Compacting -> stringResource(R.string.chat_status_compacting) + PhaseLabelKind.WaitingModel -> stringResource(R.string.chat_phase_waiting_model) + PhaseLabelKind.RunningTools -> if (label.more > 0) { + stringResource( + R.string.chat_phase_running_tools_more, + label.tools.joinToString(", "), + label.more, + ) + } else { + stringResource( + R.string.chat_phase_running_tools, + label.tools.joinToString(", "), + ) + } + PhaseLabelKind.RunningCommand -> stringResource(R.string.chat_phase_running_command) + PhaseLabelKind.Working -> stringResource(R.string.chat_status_working) +} + +/** + * The status line under the composer. It only carries what the composer + * controls row does not already show: model, thinking level and context usage + * are on that row (picker, level, context ring), so repeating them here was a + * second, stale-looking copy of the same facts (web `ChatInput`'s `statusText` + * and the footer make the same split). + */ +@Composable +internal fun ChatFooter(ui: ChatUiState) { + val colors = LocalOmpColors.current + val engine = ui.engine + val running = engine.busy || engine.streamingActive + // Cached history on screen and no live word yet: without this line the + // footer looks idle ("nothing runs") while the client is still connecting. + val connecting = chatShowsConnecting( + liveStateKnown = engine.liveStateKnown, + hasTranscript = engine.historyStale || engine.messages.isNotEmpty(), + ) + val startedAt = remember( + running, + engine.runStartedAt, + engine.liveTools, + engine.messages, + ) { if (running) TranscriptFormat.turnStartedAt(engine) else null } + val parts = buildList { + if (connecting) add(stringResource(R.string.chat_status_connecting)) + val status = phaseStatusText(phaseLabel(engine)) + if (status != null) add(status) + if (engine.pagesLoading) add(stringResource(R.string.chat_status_pages)) + } + // The rate is visual only: it is kept out of the polite live region so a + // screen reader does not announce a new number twice a second. + val speedText = if (running) { + engine.liveTokensPerSecond?.let { + stringResource(R.string.chat_tokens_per_second, formatGenerationRate(it)) + } + } else { + null + } + // The turn duration reads the shared tick in its own composable, so the + // footer's other lines do not recompose every second. + val showElapsed = running && startedAt != null + val queued = engine.queue.steering.size + engine.queue.followUp.size + if (parts.isEmpty() && !showElapsed && queued == 0 && ui.speakingId == null && speedText == null) return + Row( + modifier = Modifier + .fillMaxWidth() + .semantics { liveRegion = LiveRegionMode.Polite } + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (connecting) { + CircularProgressIndicator( + modifier = Modifier.size(10.dp), + strokeWidth = 1.5.dp, + color = colors.textDim, + ) + Spacer(Modifier.width(5.dp)) + } else if (running && (parts.isNotEmpty() || showElapsed)) { + LivePulseDot(size = 6.dp) + Spacer(Modifier.width(5.dp)) + } + if (parts.isNotEmpty()) { + Text( + text = parts.joinToString(" · "), + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (speedText != null) { + Text( + text = (if (parts.isEmpty()) "" else " · ") + speedText, + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + modifier = Modifier.clearAndSetSemantics {}, + ) + } + ElapsedText( + startedAt = startedAt, + running = running, + prefix = if (parts.isEmpty() && speedText == null) "" else " · ", + color = colors.textDim, + fontSize = 10.sp, + ) + Spacer(Modifier.weight(1f)) + if (queued > 0) { + Text( + text = stringResource(R.string.chat_queued, queued), + color = colors.textDim, + fontSize = 10.sp, + ) + } + if (ui.speakingId != null) { + Text( + text = stringResource(R.string.chat_reading_aloud), + color = colors.accent, + fontSize = 10.sp, + modifier = Modifier.padding(start = 8.dp), + ) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatSearch.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatSearch.kt new file mode 100644 index 000000000..c7de1cf18 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatSearch.kt @@ -0,0 +1,158 @@ +package io.github.kahme247.ompweb.ui.chat + +import io.github.kahme247.ompweb.chat.AssistantBlock +import io.github.kahme247.ompweb.chat.ChatMessage +import io.github.kahme247.ompweb.chat.TranscriptRow + +/** + * One transcript row whose rendered text contains the search query. + * + * A hit is a *row*, not a substring and not a message: the transcript cannot + * scroll to a position inside a rendered markdown block, and a folded turn + * renders several messages inside one group row. Counting messages instead + * would report a higher total than the number of marked rows and let the + * arrows land on the same row twice. + */ +data class ChatSearchHit( + /** Key of the [TranscriptRow] to scroll to and mark. */ + val rowKey: String, +) + +/** + * The in-chat search surface (top-bar search icon): the open field, the query, + * the hits in transcript order and the hit the arrows have selected. + */ +data class ChatSearchState( + val open: Boolean = false, + val query: String = "", + val hits: List = emptyList(), + val activeIndex: Int = 0, + /** + * An entry the transcript must scroll to and flash — the hand-off point of + * the server-side search in `ui/sessions` (see + * `ChatViewModel.scrollToEntry`). Cleared by the transcript once it jumped. + */ + val revealEntryId: String? = null, +) { + val total: Int get() = hits.size + + val active: ChatSearchHit? get() = hits.getOrNull(activeIndex) +} + +/** + * Text search over the *loaded* transcript (user feedback 25.10.: find text in + * the chat without leaving it). Pure: the view model and the transcript only + * hand in rows/messages, so matching and stepping are unit-testable without a + * composition. + */ +internal object ChatSearch { + + fun normalize(query: String): String = query.trim().lowercase() + + /** + * Everything the row shows as text. Tool output is included on purpose: a + * command's output is often what the reader is looking for. Thinking text + * is part of the row only while the reader has not hidden it — searching + * what is not rendered would count hits the transcript can never reveal. + */ + fun searchableText(message: ChatMessage, hideThinking: Boolean = false): String = when (message) { + is ChatMessage.User -> message.text + is ChatMessage.Assistant -> buildString { + for (block in message.blocks) { + when (block) { + is AssistantBlock.Text -> append(block.text) + is AssistantBlock.Thinking -> if (!hideThinking) append(block.thinking) + is AssistantBlock.ToolCall -> append(block.toolName).append(' ').append(block.input) + is AssistantBlock.Image, is AssistantBlock.Other -> Unit + } + append('\n') + } + } + is ChatMessage.ToolResult -> message.toolName + "\n" + message.text + is ChatMessage.BashExecution -> message.command + "\n" + (message.output ?: "") + is ChatMessage.Custom -> message.text + is ChatMessage.Other -> message.role + } + + /** + * Maps every message key to the row that renders it. A folded turn renders + * all of its messages inside one group row, so the group's whole message + * range maps to the group key. + */ + fun rowKeyByMessage(rows: List, messages: List): Map { + val map = HashMap(messages.size * 2) + for (row in rows) { + when (row) { + is TranscriptRow.Message -> map[row.message.key] = row.key + is TranscriptRow.Group -> { + for (index in row.anchorIndex until row.endIndex) { + messages.getOrNull(index)?.let { map[it.key] = row.key } + } + } + } + } + return map + } + + /** + * All rows that contain [query], in transcript order and exactly once per + * row. Hits without a rendered row (a mount notice folded away) are + * dropped: there is nothing to scroll to. + */ + fun search( + messages: List, + rowKeyByMessage: Map, + query: String, + hideThinking: Boolean = false, + ): List { + val needle = normalize(query) + if (needle.isEmpty()) return emptyList() + val hits = ArrayList() + val seenRows = HashSet() + for (message in messages) { + val rowKey = rowKeyByMessage[message.key] ?: continue + if (!seenRows.add(rowKey)) continue + val text = searchableText(message, hideThinking) + // A row that holds several messages only needs one match, but the + // first message of a folded turn may be the one without it. + if (text.contains(needle, ignoreCase = true)) { + hits += ChatSearchHit(rowKey = rowKey) + } else { + seenRows.remove(rowKey) + } + } + return hits + } + + /** Next/previous hit, wrapping at both ends. */ + fun step(current: Int, total: Int, delta: Int): Int { + if (total <= 0) return 0 + return ((current + delta) % total + total) % total + } + + fun rowIndexForRowKey(rows: List, rowKey: String): Int? = + rows.indexOfFirst { it.key == rowKey }.takeIf { it >= 0 } + + /** + * The row that carries [entryId] — the hand-off point of the server-side + * search (`ui/sessions`), which only knows message ids. + */ + fun rowIndexForEntry( + rows: List, + messages: List, + entryId: String, + ): Int? { + if (entryId.isEmpty()) return null + rows.forEachIndexed { index, row -> + when (row) { + is TranscriptRow.Message -> if (row.message.entryId == entryId) return index + is TranscriptRow.Group -> { + for (messageIndex in row.anchorIndex until row.endIndex) { + if (messages.getOrNull(messageIndex)?.entryId == entryId) return index + } + } + } + } + return null + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatSearchBar.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatSearchBar.kt new file mode 100644 index 000000000..c1ea24187 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatSearchBar.kt @@ -0,0 +1,175 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors + +/** + * The in-chat search field under the chat's top bar (user feedback 25.10.): + * query, hit count `3/17`, ↑/↓ stepping through the loaded transcript and the + * way out to a search that also covers history the transcript does not hold. + */ +@Composable +internal fun ChatSearchBar( + state: ChatSearchState, + /** History exists that is not part of the loaded transcript (compaction view off). */ + olderHistoryHidden: Boolean, + /** Tapping "search full history" only flips a view; disable while it loads. */ + fullHistoryBusy: Boolean, + onQueryChange: (String) -> Unit, + onStep: (Int) -> Unit, + onClose: () -> Unit, + onSearchFullHistory: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.requestFocus() } + val countText = when { + state.total > 0 -> stringResource(R.string.chat_search_count, state.activeIndex + 1, state.total) + state.query.isBlank() -> "" + else -> stringResource(R.string.chat_search_no_hits) + } + Surface( + color = colors.panel, + contentColor = colors.text, + shape = RoundedCornerShape(10.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, colors.border), + modifier = modifier.fillMaxWidth().testTag("chat-search-bar"), + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(18.dp), + ) + BasicTextField( + value = state.query, + onValueChange = onQueryChange, + singleLine = true, + textStyle = TextStyle(color = colors.text, fontSize = 14.sp, lineHeight = 20.sp), + cursorBrush = SolidColor(colors.accent), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { onStep(1) }), + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp) + .heightIn(min = 40.dp) + .focusRequester(focusRequester) + .testTag("chat-search-field"), + decorationBox = { inner -> + androidx.compose.foundation.layout.Box { + if (state.query.isEmpty()) { + Text( + text = stringResource(R.string.chat_search_placeholder), + color = colors.textDim, + fontSize = 14.sp, + ) + } + inner() + } + }, + ) + if (countText.isNotEmpty()) { + Text( + text = countText, + color = if (state.total > 0) colors.textMuted else colors.statusError, + fontSize = 12.sp, + modifier = Modifier + .padding(horizontal = 4.dp) + .testTag("chat-search-count"), + ) + } + IconButton( + onClick = { onStep(-1) }, + enabled = state.total > 0, + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowUp, + contentDescription = stringResource(R.string.chat_search_previous), + modifier = Modifier.size(20.dp), + ) + } + IconButton( + onClick = { onStep(1) }, + enabled = state.total > 0, + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource(R.string.chat_search_next), + modifier = Modifier.size(20.dp), + ) + } + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.chat_search_close), + modifier = Modifier.size(20.dp), + ) + } + } + if (olderHistoryHidden && state.query.isNotBlank()) { + Row( + modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 6.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.chat_search_history_hidden), + color = colors.textDim, + fontSize = 11.5.sp, + modifier = Modifier.weight(1f), + ) + TextButton(onClick = onSearchFullHistory, enabled = !fullHistoryBusy) { + Text( + text = stringResource(R.string.chat_search_full_history), + fontSize = 12.sp, + ) + } + } + } + } + } +} + +/** A row's search mark, so the transcript can paint matches without knowing the query. */ +internal enum class ChatRowHighlight { None, Match, Active } diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatViewModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatViewModel.kt new file mode 100644 index 000000000..fd18b3253 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ChatViewModel.kt @@ -0,0 +1,3370 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.kahme247.ompweb.chat.BranchNode +import io.github.kahme247.ompweb.chat.BranchRow +import io.github.kahme247.ompweb.chat.BranchTree +import io.github.kahme247.ompweb.chat.ChatEffect +import io.github.kahme247.ompweb.chat.ChatEngine +import io.github.kahme247.ompweb.chat.ChatMessage +import io.github.kahme247.ompweb.chat.ChatNotice +import io.github.kahme247.ompweb.chat.ChatState +import io.github.kahme247.ompweb.chat.CompactResult +import io.github.kahme247.ompweb.chat.ContentionCopy +import io.github.kahme247.ompweb.chat.ContextPage +import io.github.kahme247.ompweb.chat.ContextSync +import io.github.kahme247.ompweb.chat.ForkTarget +import io.github.kahme247.ompweb.chat.ForkTargets +import io.github.kahme247.ompweb.chat.HistoryCursor +import io.github.kahme247.ompweb.chat.TranscriptPlan +import io.github.kahme247.ompweb.chat.TranscriptRow +import io.github.kahme247.ompweb.chat.NoticeKind +import io.github.kahme247.ompweb.data.ChatSettings +import io.github.kahme247.ompweb.data.ComposerDraftStore +import io.github.kahme247.ompweb.data.DraftRecovery +import io.github.kahme247.ompweb.data.TranscriptCache +import io.github.kahme247.ompweb.domain.ActiveGoal +import io.github.kahme247.ompweb.domain.BashInvocation +import io.github.kahme247.ompweb.domain.ComposerImage +import io.github.kahme247.ompweb.domain.ComposerRoute +import io.github.kahme247.ompweb.domain.DictationState +import io.github.kahme247.ompweb.domain.ExtensionNoticeLevel +import io.github.kahme247.ompweb.domain.ForeignSubmitDecision +import io.github.kahme247.ompweb.domain.foreignSubmitDecision +import io.github.kahme247.ompweb.domain.SttAfter +import io.github.kahme247.ompweb.domain.SubmitDuringRunBehavior +import io.github.kahme247.ompweb.domain.WordGhost +import io.github.kahme247.ompweb.domain.isClientBuiltinCommand +import io.github.kahme247.ompweb.domain.isValidSubagentId +import io.github.kahme247.ompweb.domain.routeComposerSubmission +import io.github.kahme247.ompweb.domain.validateOutgoingPrompt +import io.github.kahme247.ompweb.domain.SubagentActivityEvent +import io.github.kahme247.ompweb.domain.SubagentInfo +import io.github.kahme247.ompweb.domain.SubagentSource +import io.github.kahme247.ompweb.domain.SubagentTranscriptMessage +import io.github.kahme247.ompweb.domain.createActiveGoal +import io.github.kahme247.ompweb.domain.parseSubagentHistoryEntry +import io.github.kahme247.ompweb.domain.parseSubagentSnapshot +import io.github.kahme247.ompweb.domain.parseSubagentTranscriptPage +import io.github.kahme247.ompweb.domain.readCompactResult +import io.github.kahme247.ompweb.ui.hosttools.HostToolsController +import io.github.kahme247.ompweb.ui.panels.btw.BtwAskResult +import io.github.kahme247.ompweb.ui.panels.btw.BtwRefusal +import io.github.kahme247.ompweb.ui.panels.btw.BtwViewModel +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionUiController +import io.github.kahme247.ompweb.events.ConnectionState +import io.github.kahme247.ompweb.events.EventChannel +import io.github.kahme247.ompweb.events.SessionEventSource +import io.github.kahme247.ompweb.net.ApiError +import io.github.kahme247.ompweb.net.ChatApi +import io.github.kahme247.ompweb.net.ElsewhereSession +import io.github.kahme247.ompweb.net.FilesApi +import io.github.kahme247.ompweb.net.QueueRestore +import io.github.kahme247.ompweb.net.SkillDiagnosticsApi +import io.github.kahme247.ompweb.net.QueuedMessageRemoval +import io.github.kahme247.ompweb.net.isQueuedWhileShellRunning +import io.github.kahme247.ompweb.net.userMessage +import io.github.kahme247.ompweb.ui.composer.ComposerController +import io.github.kahme247.ompweb.ui.composer.ComposerStrings +import io.github.kahme247.ompweb.ui.composer.ComposerSubmission +import io.github.kahme247.ompweb.ui.composer.CompletionSound +import io.github.kahme247.ompweb.ui.composer.controls.ComposerControlsViewModel +import io.github.kahme247.ompweb.ui.composer.controls.WordPredictor +import io.github.kahme247.ompweb.ui.composer.dictation.DictationViewModel +import io.github.kahme247.ompweb.domain.DictationOutcome +import io.github.kahme247.ompweb.domain.DictationResult +import io.github.kahme247.ompweb.domain.dictationOutcome +import io.github.kahme247.ompweb.ui.render.GithubRepoRef +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put + +/** Display preferences the chat needs from `SettingsStore` (web `omp-web:*` keys). */ +data class ChatPreferences( + val toolCallsCollapsed: Boolean = true, + val hideToolActivity: Boolean = true, + val expandThinking: Boolean = false, + val hideThinking: Boolean = false, + val ttsAutoplay: Boolean = false, +) + +/** A deferred thinking block's fetched text. */ +sealed interface ThinkingEntry { + data object Loading : ThinkingEntry + + data class Text(val text: String) : ThinkingEntry + + data class Error(val message: String) : ThinkingEntry +} + +/** A one-shot instruction for the screens (navigation or feedback). */ +sealed interface ChatEvent { + /** The fork command produced a new session; open it. */ + data class Forked(val sessionId: String) : ChatEvent + + /** omp cancelled the fork because a run was still in flight. */ + data object ForkCancelled : ChatEvent + + /** The fork command answered `success` but created no session. */ + data object ForkFailed : ChatEvent + + /** Show a short message (clipboard/network/agent failure). */ + data class Toast(val message: String) : ChatEvent + + /** Put [text] on the clipboard (builtin `/copy`). */ + data class CopyText(val text: String) : ChatEvent +} + +data class BranchNavigatorState( + val rows: List, +) + +/** A subagent's paged raw transcript (web transcript pager state). */ +data class SubagentTranscriptState( + val open: Boolean = false, + val messages: List = emptyList(), + val nextByte: Long = 0L, + val loading: Boolean = false, + val error: String? = null, + val exhausted: Boolean = false, +) + +/** + * The subagent transcript dialog (web `SubagentTranscriptDialog`): the roster + * entry's header telemetry, the settled completion, and the paged raw + * transcript. `live` mirrors the roster source (history entries never show the + * activity tail and add the token/cost/duration line). + */ +data class SubagentDialogState( + val id: String, + val loading: Boolean = true, + val completion: String? = null, + val truncated: Boolean = false, + val error: String? = null, + val agent: String? = null, + val description: String? = null, + val task: String? = null, + val sessionFile: String? = null, + val resolvedModel: String? = null, + /** Raw history telemetry; the dialog localizes the units. */ + val tokens: Long? = null, + val cost: Double? = null, + val durationMs: Long? = null, + val outcomeError: String? = null, + val live: Boolean = true, + val activity: List = emptyList(), + val transcript: SubagentTranscriptState = SubagentTranscriptState(), +) + +/** A loaded (or failed) full bash-output file. */ +sealed interface BashOutputUi { + data object Loading : BashOutputUi + + data class Ready(val text: String) : BashOutputUi + + data class Error(val message: String) : BashOutputUi +} + +/** Everything the chat surface renders. */ +data class ChatUiState( + val sessionId: String? = null, + val engine: ChatState = ChatState(), + val rows: List = emptyList(), + val toolResults: Map = emptyMap(), + val forkTargets: List = emptyList(), + val preferences: ChatPreferences = ChatPreferences(), + val githubRepo: GithubRepoRef? = null, + val hasCompaction: Boolean = false, + val branchTree: List = emptyList(), + val branchHasBranch: Boolean = false, + val branch: BranchNavigatorState? = null, + val subagent: SubagentDialogState? = null, + val thinking: Map = emptyMap(), + val notices: List = emptyList(), + /** Full bash output loaded on demand, keyed by `fullOutputPath`. */ + val bashOutputs: Map = emptyMap(), + /** Id of the reply currently read aloud, if any. */ + val speakingId: String? = null, + val forkingEntryId: String? = null, + val stopping: Boolean = false, + /** Forked edit-and-resend text waiting for the composer to adopt it. */ + val prefill: String? = null, + /** The session's active `/goal`, shown above the composer. */ + val activeGoal: ActiveGoal? = null, + /** `omp-web:submit-during-run`, read for the composer's Enter behavior. */ + val submitDuringRun: SubmitDuringRunBehavior = SubmitDuringRunBehavior.DEFAULT, + /** The `/plan` turn still running; its banner sits above the composer. */ + val planObjective: String? = null, + /** System prompt panel: closed, loading, loaded text, empty or failed. */ + val systemPrompt: SystemPromptState = SystemPromptState.Idle, + /** Startup skill-notice "Turn off" round trip is in flight. */ + val skillDiagnosticsSaving: Boolean = false, + /** Why the last "Turn off" did not take effect (shown inline). */ + val skillDiagnosticsFailure: SkillDiagnosticsFailure? = null, + /** Transient compaction result banner (web `compactResult`, 6 s). */ + val compactResult: CompactResult? = null, + /** Transient compaction failure banner (web `compactError`, 3 s). */ + val compactError: String? = null, + /** `omp-web:composer-todo-collapsed`: the attached todo panel's body. */ + val todoCollapsed: Boolean = true, + /** `omp-web:composer-subagents-collapsed`: the attached roster's body. */ + val subagentsCollapsed: Boolean = true, + val connection: ConnectionState = ConnectionState.Reconnecting, + /** Row to restore on the next transcript layout; consumed by the screen. */ + val restoreScrollIndex: Int? = null, + /** + * A submit is waiting for the live run state before it can be routed: the + * composer shows it as sending and refuses further input, the draft stays + * in the field until the route decision is made. + */ + val submitPending: Boolean = false, + /** + * A send the chat is holding because another omp process owns the session + * (web "Trotzdem senden (Kopie)"). Non-null = the confirmation dialog is + * open for exactly this submission; the draft stays in the field until the + * user decides, and only the dialog's confirm sends with `?force=1`. + */ + val foreignSend: ComposerSubmission? = null, + /** + * omp moved this chat to a copy because another process held the file + * (`session_contention_fork`). The banner offers opening the original and + * the copy; the copy carries the sent text. + */ + val contentionCopy: ContentionCopy? = null, +) { + val busy: Boolean + get() = engine.busy || engine.streamingActive +} + +/** + * Phase 5 chat view model: owns the transport around the pure [ChatEngine] + * (web `useAgentSession*`). + * + * It subscribes the session channel, executes the engine's effects (context + * sync, agent-state reconcile, autoplay), reassembles multi-page history, + * settles a just-sent prompt, forks, fetches deferred thinking/subagent + * output/files, and keeps the per-session scroll memory. All engine input + * happens on the main dispatcher, so the coalescer's schedule callback can be + * a 16 ms coroutine. + */ +class ChatViewModel( + private val api: ChatApi, + private val eventMux: SessionEventSource, + private val settings: ChatSettings, + private val speech: Speech, + private val savedStateHandle: SavedStateHandle, + /** Localized `R.string.chat_prompt_maybe_started`; the view model has no + * resources, so the container resolves it. */ + private val maybeStartedMessage: String, + /** Localized `R.string.chat_stop_unconfirmed`: omp did not answer the abort + * in time, so the run may still be going. */ + private val stopUnconfirmedMessage: String, + /** Per-session composer drafts (text survives process death). */ + private val drafts: ComposerDraftStore, + /** Localized composer labels the view model raises (no `Context` here). */ + private val composerStrings: ComposerStrings, + /** Slice B: model / thinking / tools / advisor / context controls. */ + val controls: ComposerControlsViewModel, + /** Slice C: voice dictation. */ + val dictation: DictationViewModel, + /** Slice B: ghost-text word prediction. */ + private val wordPredictor: WordPredictor, + /** Completion tone (`omp-sound-enabled`). */ + private val completionSound: CompletionSound, + /** Slice B: side questions (`/btw`), fed by the session's frames. */ + val btw: BtwViewModel, + /** Slice C: extension UI requests and push updates for the bound session. */ + val extensionUi: ExtensionUiController, + /** Slice C: the app-wide host-tool bridge (`open_url`/`notify`/`open_file`). */ + private val hostTools: HostToolsController, + /** The composer's `/api/file-index` client (one client with the explorer). */ + files: FilesApi, + /** + * `set_skill_startup_diagnostics` (GAP 3). Optional so existing tests keep + * constructing the view model; the container passes [ApiClient], and a + * missing client surfaces as "unavailable" in the notice instead of a + * silent no-op. + */ + private val skillDiagnostics: SkillDiagnosticsApi? = null, + /** + * The offline transcript cache. `null` disables cache-first painting; the + * container passes the `SessionCacheStore`, and the JVM tests a fake or + * nothing. + */ + private val transcripts: TranscriptCache? = null, + /** + * Where the transcript cache is read and written. `Dispatchers.IO` in the + * app; the JVM tests pass their test dispatcher so the seed lands + * deterministically before the request. + */ + private val cacheDispatcher: CoroutineDispatcher = Dispatchers.IO, + /** + * Bumped by `AppContainer.clearLocalUserData` (sign-out, server switch, + * 401). On a change every transcript this view model holds in memory is + * dropped: the disk cache is cleared at the same moment, and nothing of + * the previous installation may survive in the heap. + */ + private val sessionEpoch: StateFlow? = null, +) : ViewModel() { + + private val engine = ChatEngine( + scheduler = { flush -> + val job = viewModelScope.launch { + delay(FRAME_COALESCE_MILLIS) + flush() + } + return@ChatEngine { job.cancel() } + }, + // `btw_*` frames never reach the transcript (web routes them before + // `catchUp.observe`); the engine consumes them here. + onBtwFrame = { frame -> btw.onFrame(frame) }, + ) + + /** The composer's text, attachments, palettes and `!` shell mode. */ + val composer = ComposerController( + drafts = drafts, + scope = viewModelScope, + api = api, + files = files, + strings = composerStrings, + notice = { message -> _events.tryEmit(ChatEvent.Toast(message)) }, + ) + + /** Slice B: the prediction for the current draft state, painted as ghost text. */ + val ghost: StateFlow = wordPredictor.ghost + + private val _ui = MutableStateFlow(ChatUiState()) + val ui: StateFlow = _ui.asStateFlow() + + /** + * The in-chat search (top-bar search icon) and the jump hand-off for the + * server-side search in `ui/sessions` + * ([scrollToEntry]). + */ + private val _search = MutableStateFlow(ChatSearchState()) + val search: StateFlow = _search.asStateFlow() + + private val _events = MutableSharedFlow(extraBufferCapacity = 8) + val events: SharedFlow = _events.asSharedFlow() + + private var sessionId: String? = null + private var cwd: String? = null + + /** Names warned about once ("unknown slash command sent as text"). */ + private val warnedSlashCommands = HashSet() + /** One queue mutation (edit/delete/steer) at a time, like the web's guard. */ + private var queueMutationInFlight = false + + /** Edit-and-resend text waiting for the session a fork created. */ + private val prefillBySession = HashMap() + + /** Search hits waiting for the session they belong to (server-side search hand-off). */ + private val pendingReveals = HashMap() + + /** + * The server origin [pendingReveals] were queued for. Those entry ids mean + * nothing on another installation, so a change drops the queue. + */ + private var lastOrigin: String? = settings.snapshot.value.origin?.origin + + /** `/btw` questions waiting for the session the new-session pane created. */ + private val pendingSideQuestions = HashMap() + + /** Null means "the session's latest leaf"; a branch pick pins the view. */ + private var viewLeafId: String? = null + private var detailLeafId: String? = null + + private var preferences = ChatPreferences() + private var rowsCacheSource: List? = null + private var rowsCacheHideThinking = false + private var rowsCache: List = emptyList() + /** Last session-file model/thinking forwarded to Slice B's controls. */ + private var lastSessionModelKey: String? = null + private var lastSessionThinking: String? = null + private var committedToolResults: Map = emptyMap() + private var forkCache: List = emptyList() + + private var subscriptionJob: Job? = null + private var historyJob: Job? = null + private var historyRequested = false + private var pendingReset = false + private var historyGeneration = 0 + private var historyCursor: HistoryCursor? = null + private var metaJob: Job? = null + + private var settleJob: Job? = null + + /** + * Sessions with a `GET /api/agent/{id}` on the wire. The guard is + * per-session because the view model outlives a session switch: a slow + * answer for the previous session must never swallow the first live-state + * read of the next one. A duplicate request for the session that is + * already waiting is dropped; its answer is the same state. + */ + private val reconcileInFlight = HashSet() + + /** + * Consecutive 503 answers that carried no run state, per session, and when + * the first of them arrived. Reset by any successful read (and on run end): + * it only bounds the "unknown" window of a server that does not report the + * run state — the answer itself is authoritative when it does. + */ + private var slowStateSession: String? = null + private var slowStateRunId = -1L + private var slowStateAnswers = 0 + + /** + * The submit that is waiting for the live state of [sessionId]: one at a + * time, so a second Enter (or the button) cannot start a second route. + */ + private var submitPendingJob: Job? = null + + /** The session whose submit is waiting; null when none is. */ + private var submitPendingSession: String? = null + + /** + * Bumped by every [open] with another session and by + * [clearSessionData]. A wait of an older epoch belongs to a chat that is + * gone: it is cancelled and must neither route its draft nor clear the + * "Sending…" of the new chat. + */ + private var submitEpoch = 0L + + /** Auto-dismiss timers for the transient compaction banners. */ + private var compactResultJob: Job? = null + private var compactErrorJob: Job? = null + /** Debounced per-subagent refetches for the open transcript dialog. */ + private var subagentWatchJob: Job? = null + private var subagentCompletionJob: Job? = null + private var subagentTranscriptJob: Job? = null + private var subagentDetailJob: Job? = null + private var refetchedSubagentVersion = 0L + private var refetchedTranscriptVersion = 0L + private var latestSubagentVersion = 0L + /** Guards the async subagent fetches of the currently open dialog. */ + private var subagentRequestSeq = 0 + private var transcriptRequestSeq = 0 + + private var scrollSaveJob: Job? = null + private var currentScrollIndex = 0 + private var lastPersistedScroll = 0 + private var lastNearBottom = true + + private val thinkingCache = LinkedHashMap() + private val dismissedNotices = HashSet() + + private var runCounter = 0L + private var pumpingEffects = false + + init { + composer.textObserver = { text, caret -> wordPredictor.onTextChanged(sessionId, text, caret) } + viewModelScope.launch { + settings.snapshot.collect { snapshot -> + // Entry ids are session-file UUIDs; after "Change server" the + // queued jump targets address another installation and must not + // fire against this chat's transcript. + val origin = snapshot.origin?.origin + if (origin != lastOrigin) { + lastOrigin = origin + pendingReveals.clear() + } + val next = ChatPreferences( + toolCallsCollapsed = snapshot.toolCallsCollapsed, + hideToolActivity = snapshot.hideToolActivity, + expandThinking = snapshot.expandThinking, + hideThinking = snapshot.hideThinking, + ttsAutoplay = snapshot.ttsAutoplay, + ) + if (next != preferences) { + if (next.hideThinking != preferences.hideThinking) rowsCacheSource = null + preferences = next + speech.setVoice(snapshot.ttsVoice) + publish() + } else if ( + _ui.value.todoCollapsed != snapshot.composerTodoCollapsed || + _ui.value.subagentsCollapsed != snapshot.composerSubagentsCollapsed + ) { + publish() + } + } + } + viewModelScope.launch { + engine.state.collect { state -> publish(state) } + } + sessionEpoch?.let { epoch -> + viewModelScope.launch { + epoch.drop(1).collect { clearSessionData() } + } + } + viewModelScope.launch { + eventMux.state.collect { connection -> + val previous = _ui.value.connection + _ui.update { it.copy(connection = connection) } + if (connection == ConnectionState.Connected && previous != ConnectionState.Connected) { + requestHistory(reset = false) + reconcileNow() + } + } + } + viewModelScope.launch { + speech.state.collect { voice -> + _ui.update { it.copy(speakingId = voice.speakingId) } + } + } + viewModelScope.launch { + // The live state of a freshly opened session can fail to arrive + // (one 5xx, a proxy timeout, omp's session_slow). While it is + // unknown there is no stream that would correct it, so the loop + // asks again — with a growing delay, so an offline device does not + // poll every few seconds. A known state polls only a busy run. + var unknownBackoff = RECONCILE_UNKNOWN_FIRST_MILLIS + var polledSession: String? = null + while (isActive) { + val current = engine.state.value + if (sessionId != polledSession) { + polledSession = sessionId + unknownBackoff = RECONCILE_UNKNOWN_FIRST_MILLIS + } + val known = current.liveStateKnown + // Another omp process owns the chat: no session channel streams + // its transcript here, so the file is followed by polling (web + // `catchUp`), and every poll refreshes the owner report. + val foreign = current.foreignRun?.active == true + delay( + when { + !known -> unknownBackoff + foreign -> FOREIGN_RUN_POLL_MILLIS + else -> RECONCILE_MILLIS + }, + ) + if (!known) { + unknownBackoff = minOf(unknownBackoff * 2, RECONCILE_UNKNOWN_MAX_MILLIS) + reconcileNow(countSlowAnswer = true) + } else if (foreign) { + // Follow the file read-only AND re-read the owner report: + // without the state poll an owner that ended would keep the + // banner (and the send gate) forever, because no frame of + // this refused session channel would ever say otherwise. + requestHistory(reset = false) + reconcileNow(countSlowAnswer = true) + } else if (engine.state.value.busy) { + reconcileNow(countSlowAnswer = true) + } + } + } + viewModelScope.launch { + dictation.results.collect { result -> applyDictationResult(result) } + } + // On-device recognition writes its partials into the composer field at + // the caret; the view model reads text and caret right before each edit + // and hands the finished state back. + dictation.attachLiveField( + text = { composer.state.value.text }, + caret = { composer.state.value.caret }, + apply = composer::replaceText, + ) + viewModelScope.launch { + controls.state.collect { state -> + state.foreignRefusal?.let { refusal -> + // A settings command (model/thinking/fast/compact/…) hit the + // ownership gate: the banner goes up and the message says the + // command was not applied instead of the send wording. + engine.setForeignRun(refusal.elsewhere ?: engine.state.value.foreignRun) + controls.clearForeignRefusal() + _events.tryEmit(ChatEvent.Toast(composerStrings.foreignCommandBlocked)) + } + state.error?.let { message -> + _events.tryEmit(ChatEvent.Toast(message)) + controls.clearError() + } + } + } + // Slice C surfaces its host/extension notices with omp's level; the + // notice bar colours errors and warnings instead of dropping the level. + viewModelScope.launch { + extensionUi.notices.collect { notice -> + engine.notice( + when (notice.level) { + ExtensionNoticeLevel.Info -> NoticeKind.Info + ExtensionNoticeLevel.Warning -> NoticeKind.Warning + ExtensionNoticeLevel.Error -> NoticeKind.Error + }, + notice.message, + ) + } + } + viewModelScope.launch { + hostTools.notices.collect { message -> _events.tryEmit(ChatEvent.Toast(message)) } + } + // `extension_ui_editor_text`: a request may write the composer field. + viewModelScope.launch { + extensionUi.editorText.collect { text -> composer.setText(text) } + } + } + + // --------------------------------------------------------------- composer + + /** Tab/`→`: accept the ghost when it still matches the current draft. */ + fun acceptGhost(): Boolean { + if (!composer.state.value.canAcceptGhost) return false + val state = composer.state.value + val insertion = wordPredictor.accept(state.text, state.caret) ?: return false + composer.replaceText(insertion.text, insertion.caret) + return true + } + + /** Adopt the fork's edit-and-resend text into the field. */ + fun adoptPrefill() { + val text = _ui.value.prefill ?: return + _ui.update { it.copy(prefill = null) } + composer.setText(text) + } + + fun openHistory() { + val busy = engine.state.value.busy || engine.state.value.streamingActive + if (composer.state.value.text.isNotEmpty() || busy) return + val userTexts = engine.state.value.messages.mapNotNull { message -> + (message as? ChatMessage.User)?.text?.takeIf { !it.isBlank() } + } + composer.openHistory(userTexts) + } + + // ---------------------------------------------------------- system prompt + + /** + * Top-bar terminal icon (web `AppShell`): load the session's composed + * system prompt on demand. `get_state` is the only source; an idle session + * without an omp child answers from the saved state. + */ + fun openSystemPrompt() { + val sid = sessionId ?: return + if (_ui.value.systemPrompt is SystemPromptState.Loading) return + _ui.update { it.copy(systemPrompt = SystemPromptState.Loading) } + viewModelScope.launch { + try { + val root = api.agentState(sid) + if (sessionId != sid) return@launch resetStaleSystemPrompt() + _ui.update { it.copy(systemPrompt = systemPromptFor(root)) } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (sessionId != sid) return@launch resetStaleSystemPrompt() + _ui.update { it.copy(systemPrompt = SystemPromptState.Failed(error.userMessage())) } + } + } + } + + /** + * A load started for a session the screen has left must not surface; the + * sheet returns to idle, but a load the new session started stays. + */ + private fun resetStaleSystemPrompt() { + _ui.update { + if (it.systemPrompt is SystemPromptState.Loading) { + it.copy(systemPrompt = SystemPromptState.Idle) + } else { + it + } + } + } + + fun closeSystemPrompt() { + _ui.update { it.copy(systemPrompt = SystemPromptState.Idle) } + } + + private fun refreshOpenSystemPrompt(root: JsonObject) { + if (_ui.value.systemPrompt is SystemPromptState.Idle) return + val text = systemPromptText(root) ?: return + _ui.update { + it.copy( + systemPrompt = if (text.isEmpty()) SystemPromptState.Empty else SystemPromptState.Loaded(text), + ) + } + } + + private fun systemPromptFor(root: JsonObject): SystemPromptState { + val text = systemPromptText(root) ?: return SystemPromptState.Failed(null) + return if (text.isEmpty()) SystemPromptState.Empty else SystemPromptState.Loaded(text) + } + + private fun systemPromptText(root: JsonObject): String? { + val state = (root["state"] as? JsonObject) ?: root + return (state["systemPrompt"] as? kotlinx.serialization.json.JsonPrimitive) + ?.takeIf { it.isString } + ?.content + } + + // --------------------------------------------------------------- shortcuts + + /** + * Ctrl/Cmd+M (web `ChatWindow`): start dictation into the draft, or stop + * the running take. Upload/transcribe states are left alone so a shortcut + * cannot lose a finished recording. + */ + fun toggleDictation() { + when (dictation.state.value) { + is DictationState.Idle -> dictation.start(SttAfter.Insert) + // Both listening modes end the same way; the text stays in the draft. + is DictationState.Recording, is DictationState.Live -> dictation.stop() + else -> Unit + } + } + + /** + * Startup-notice "Turn off": persist the preference with a revision fence + * so a live `skill_diagnostics_update` that lands during the call wins + * (web `skillDiagnosticsSeqRef`). The notice shows why a change did not + * stick instead of pretending it did. + */ + fun setSkillStartupDiagnostics(enabled: Boolean) { + if (_ui.value.skillDiagnosticsSaving) return + val sid = sessionId + val client = skillDiagnostics + if (sid == null || client == null) { + _ui.update { + it.copy( + skillDiagnosticsFailure = if (sid == null) { + SkillDiagnosticsFailure.SaveFailed + } else { + SkillDiagnosticsFailure.Unavailable + }, + ) + } + return + } + _ui.update { it.copy(skillDiagnosticsSaving = true, skillDiagnosticsFailure = null) } + viewModelScope.launch { + val revision = engine.state.value.skillDiagnosticsRevision + try { + val result = client.setSkillStartupDiagnostics(sid, enabled) + if (result == null) { + _ui.update { it.copy(skillDiagnosticsFailure = SkillDiagnosticsFailure.Unavailable) } + } else { + engine.applySkillDiagnostics(result, revision) + if (result.showStartupDiagnostics) { + _ui.update { it.copy(skillDiagnosticsFailure = SkillDiagnosticsFailure.Overridden) } + } + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + _ui.update { it.copy(skillDiagnosticsFailure = SkillDiagnosticsFailure.SaveFailed) } + } finally { + _ui.update { it.copy(skillDiagnosticsSaving = false) } + } + } + } + + /** Ctrl/Cmd+Alt+M: ask omp to advance the session's model (`cycle_model`). */ + fun cycleModel() = controls.cycleModel() + + /** Ctrl/Cmd+Alt+T: ask omp to advance the thinking level (`cycle_thinking_level`). */ + fun cycleThinkingLevel() = controls.cycleThinkingLevel() + + /** + * The primary action: shell command, slash builtin/expansion or prompt; + * during a run it queues/steers per `omp-web:submit-during-run` (Enter) or + * always queues (button). + */ + fun submit(forceQueue: Boolean = false) { + submitComposer(forceQueue) + } + + private fun submitComposer(forceQueue: Boolean) { + // A submit that is still resolving the live state owns the composer; + // a second Enter must not route the same draft again. + if (_ui.value.submitPending) return + val submission = composer.resolve() ?: return + submitResolved(submission, forceQueue) + } + + /** Route a resolved submission: shell, web expansion, builtin or prompt. */ + private fun submitResolved(submission: ComposerSubmission, forceQueue: Boolean) { + // `/btw` runs beside any main turn (never queued, never a prompt) and + // is checked before the streaming branch, like the web composer. + if (isSideQuestion(submission.raw)) { + submitSideQuestion(submission) + return + } + val state = engine.state.value + val streaming = state.busy || state.streamingActive + val route = routeOf(submission) + // After a cold start the app has no live view yet (cached transcript, + // footer "Connecting…") while the session may well be running. A + // plain `prompt` now would be refused by omp and the steer/follow-up + // intent lost, so resolve the run state first and route afterwards. + // Without a live state the idle path is not a decision the app is + // allowed to make; `/btw` (above) and client builtins never depend on it. + val liveDependent = route is ComposerRoute.Bash || + route is ComposerRoute.WebPrompt || + route is ComposerRoute.Prompt + if (sessionId != null && !streaming && !state.liveStateKnown && liveDependent) { + resolveLiveStateThenSubmit(submission, forceQueue) + return + } + when (route) { + is ComposerRoute.Bash -> { + // Web parity: during a run a `!cmd` becomes a queued follow-up; + // the draft only clears when the shell command actually starts. + if (streaming) { + queueOrSteer(submission, route, submitMode(forceQueue)) + } else if (!holdForForeignOwner(submission)) { + if (executeBash(route.invocation)) composer.clear() + } + } + is ComposerRoute.Usage -> _events.tryEmit( + ChatEvent.Toast(composerStrings.requiresArgs(route.command, route.argumentHint)), + ) + is ComposerRoute.WebPrompt -> { + if (streaming) { + queueOrSteer(submission, route, submitMode(forceQueue)) + return + } + if (route.name == "advisor" && !controls.state.value.advisorEnabled) { + _events.tryEmit(ChatEvent.Toast(composerStrings.advisorDisabled)) + return + } + if (rejectsOversized(route.prompt, emptyList())) return + val outgoing = ComposerSubmission( + raw = submission.raw, + prompt = route.prompt, + images = emptyList(), + hasAttachments = false, + bash = null, + ) + if (holdForForeignOwner(outgoing)) return + val dispatched = sendPrompt(outgoing, clearOnSuccess = true) + if (route.name == "plan") { + // Web `activePlan`: the banner lives for this turn only. + _ui.update { it.copy(planObjective = route.args.takeIf { dispatched && it.isNotEmpty() }) } + } + if (route.name == "goal" && route.args.isNotEmpty()) { + val sid = sessionId + if (sid != null) { + val goal = createActiveGoal(route.args, System.currentTimeMillis()) + viewModelScope.launch { settings.setActiveGoal(sid, goal) } + _ui.update { it.copy(activeGoal = goal) } + } + } + } + is ComposerRoute.Builtin -> runBuiltinAction(route.name, route.args) + is ComposerRoute.Prompt -> { + if (streaming) { + queueOrSteer(submission, route, submitMode(forceQueue)) + } else if (!holdForForeignOwner(submission)) { + sendPrompt(submission, clearOnSuccess = true) + } + } + } + } + + /** + * Hold a submission back while another omp process owns this session. + * + * omp lets one process write a session; a send from here would move the + * chat to a copy on its first write. The server refuses such a send, but + * the refusal must not swallow the draft, so the screen decides first (and + * the server stays the authority for the racing case, see `sendPrompt`). + * Returns true when the submit was stopped. + * + * [foreignSubmitDecision] separates a proven owner (banner + notice; the + * explicit "send anyway (copy)" in the banner is the only way through) from + * the weak transcript signal (a confirmation dialog right away). + */ + private fun holdForForeignOwner(submission: ComposerSubmission): Boolean { + val decision = foreignSubmitDecision(engine.state.value.foreignRun, confirmed = false) + if (decision == ForeignSubmitDecision.Allow) return false + val route = routeOf(submission) + val prompt = route is ComposerRoute.Prompt || route is ComposerRoute.WebPrompt + // A `!cmd` has no text worth keeping in a copy: it is simply refused. + if (!prompt || decision == ForeignSubmitDecision.Block) { + engine.notify(NoticeKind.Warning, composerStrings.foreignSendBlocked) + return true + } + _ui.update { it.copy(foreignSend = submission) } + return true + } + + /** + * The banner's "send anyway (copy)": open the confirmation for the current + * draft. A client builtin needs no omp process and is routed as usual. + */ + fun requestForeignSend() { + val submission = composer.resolve() ?: return + val route = routeOf(submission) + if (route !is ComposerRoute.Prompt && route !is ComposerRoute.WebPrompt) { + submitResolved(submission, forceQueue = false) + return + } + _ui.update { it.copy(foreignSend = submission) } + } + + /** The confirmation's cancel: nothing is sent, the draft stays as it is. */ + fun cancelForeignSend() { + _ui.update { it.copy(foreignSend = null) } + } + + /** Close the "continued in a copy" banner (after opening a chat). */ + fun dismissContentionFork() { + _ui.update { it.copy(contentionCopy = null) } + } + + /** + * The confirmation's "send anyway (copy)": send with `?force=1`. The send + * goes into a copy with a new chat id; the original stays as the other + * process left it. + */ + fun confirmForeignSend() { + val pending = _ui.value.foreignSend ?: return + _ui.update { it.copy(foreignSend = null) } + if (sendPrompt(pending, clearOnSuccess = true, force = true)) return + // The run state changed under the dialog (a stream arrived); the draft + // is untouched, so the user can simply send again. + engine.notify(NoticeKind.Warning, composerStrings.foreignSendBlocked) + } + + /** + * Take the session list's owner report for the open chat, unless this chat + * already has one. + * + * The list hears the `running` frames of the server, so it can know about an + * owner while this chat's own `GET /api/agent/{id}` is still on the wire or + * was refused (`session_slow`). The seed only fills a gap — an owner this + * chat already reports stays — and it stays a hint: the 2 s state poll while + * a foreign run is active is what ends it again. `null` says nothing here: + * an absent list entry comes from the list's own activity window, not from + * this session's state. + */ + private fun seedForeignRun(elsewhere: ElsewhereSession?) { + if (elsewhere == null) return + if (engine.state.value.foreignRun != null) return + engine.setForeignRun(elsewhere) + } + + private fun submitMode(forceQueue: Boolean): String = if (forceQueue) { + "followUp" + } else { + when (settings.snapshot.value.submitDuringRun) { + SubmitDuringRunBehavior.STEER -> "steer" + SubmitDuringRunBehavior.QUEUE -> "followUp" + } + } + + /** + * Wait for the live run state of [submission]'s session, then route it. + * + * The submit is held in the composer (`submitPending`): the draft stays + * where it is, the primary action reads "Sending…" and input is locked, so + * the text can neither be lost nor routed twice. Once the state is known + * [submitResolved] runs again and picks steer/follow-up for a running + * session or a prompt for an idle one. A state that cannot be read within + * [LIVE_STATE_SUBMIT_TIMEOUT_MILLIS] leaves the draft untouched and says + * so, instead of guessing and letting omp refuse the prompt. + * + * The wait belongs to the session that started it: switching the chat or + * dropping the signed-in user ([clearSessionData]) cancels it through + * [abandonPendingSubmit], and the epoch keeps a late cancellation from + * touching the next chat's composer. + */ + private fun resolveLiveStateThenSubmit(submission: ComposerSubmission, forceQueue: Boolean) { + val sid = sessionId ?: return + if (submitPendingJob?.isActive == true && submitPendingSession == sid) return + val epoch = submitEpoch + submitPendingSession = sid + _ui.update { it.copy(submitPending = true) } + submitPendingJob = viewModelScope.launch { + try { + val read = awaitLiveStateForSubmit() + if (epoch != submitEpoch || sessionId != sid) return@launch + when (read) { + // Slow only leaves the wait loop as running (the engine was + // marked alive); routing here still sees the busy state. + LiveRead.Running, LiveRead.Idle, LiveRead.Slow -> submitResolved(submission, forceQueue) + LiveRead.Skipped, is LiveRead.Failed -> + _events.tryEmit(ChatEvent.Toast(composerStrings.liveStateUnknown)) + } + } finally { + // A cancelled wait of the previous chat has already been + // released by [abandonPendingSubmit]; only the current one may + // clear the flags of the open composer. + if (epoch == submitEpoch) { + submitPendingJob = null + submitPendingSession = null + _ui.update { it.copy(submitPending = false) } + } + } + } + } + + /** + * Drops a submit that is still waiting for the live state, because the + * chat it belongs to is gone ([open] with another session) or everything + * of the signed-in user is being dropped ([clearSessionData]). The draft + * stays in the composer store (Findings 1/2 of the 1.0.9 review: the wait + * used to outlive its chat and kept the new composer on "Sending…"). + */ + private fun abandonPendingSubmit() { + submitEpoch++ + val job = submitPendingJob + submitPendingJob = null + submitPendingSession = null + job?.cancel() + if (_ui.value.submitPending) { + _ui.update { it.copy(submitPending = false) } + _events.tryEmit(ChatEvent.Toast(composerStrings.submitAbandoned)) + } + } + + /** + * The bounded read [resolveLiveStateThenSubmit] waits on. A frame may + * settle the state while the GET is pending, so the engine is checked + * first; a read already on the wire is awaited rather than duplicated. + */ + private suspend fun awaitLiveStateForSubmit(): LiveRead = + withTimeoutOrNull(LIVE_STATE_SUBMIT_TIMEOUT_MILLIS) { + var backoff = LIVE_STATE_SUBMIT_FIRST_MILLIS + while (true) { + val state = engine.state.value + if (state.liveStateKnown) { + return@withTimeoutOrNull if (state.busy || state.streamingActive) { + LiveRead.Running + } else { + LiveRead.Idle + } + } + when (val read = readLiveState()) { + LiveRead.Running, LiveRead.Idle -> return@withTimeoutOrNull read + LiveRead.Skipped -> delay(LIVE_STATE_SUBMIT_POLL_MILLIS) + // A busy child: the engine is now marked running, so the + // next pass answers from its state; the backoff keeps a + // fenced (dropped) mark from spinning. + LiveRead.Slow, is LiveRead.Failed -> { + delay(backoff) + backoff = minOf(backoff * 2, LIVE_STATE_SUBMIT_MAX_RETRY_MILLIS) + } + } + } + @Suppress("UNREACHABLE_CODE") + LiveRead.Skipped + } ?: LiveRead.Skipped + + /** `/btw` with or without a question (web `sendSideQuestion` regex). */ + private fun isSideQuestion(raw: String): Boolean = SIDE_QUESTION_REGEX.containsMatchIn(raw.trim()) + + /** + * `/btw ` asks a side question beside the main turn; `/btw` + * alone opens the history dialog. Attachments cannot ride along, so the + * draft (and its files) stay in the field (web `btw.attachmentsUnsupported`). + */ + private fun submitSideQuestion(submission: ComposerSubmission) { + if (submission.hasAttachments || submission.images.isNotEmpty()) { + _events.tryEmit(ChatEvent.Toast(composerStrings.btwAttachmentsUnsupported)) + return + } + if (sessionId == null) { + _events.tryEmit(ChatEvent.Toast(composerStrings.noSession)) + return + } + askSideQuestion(submission.raw) + } + + /** + * `/btw` for the open chat. A side question the new-session pane created + * this session for arrives through [askSideQuestionInNewChat] and runs on + * the first [open] of [sessionId]. + */ + private fun askSideQuestion(rawSubmission: String) { + val sid = sessionId ?: return + val raw = rawSubmission.trim() + val question = raw.removePrefix("/btw").trim() + // The session channel is observer-only: attach before asking so the + // first deltas are not missed (web `ensureEventsConnected` before ask). + ensureSubscribed(sid) + if (question.isEmpty()) { + composer.clear() + btw.openHistory() + return + } + viewModelScope.launch { + when (val result = btw.ask(question)) { + BtwAskResult.Started -> { + // Only clear what was sent; typing during the ask survives. + if (composer.state.value.text.trim() == raw) composer.clear() + } + is BtwAskResult.Refused -> when (result.reason) { + BtwRefusal.NO_SESSION -> _events.tryEmit(ChatEvent.Toast(composerStrings.noSession)) + // BUSY keeps the draft; UNSUPPORTED/FAILED already reach `btw.errors`. + BtwRefusal.BUSY, BtwRefusal.EMPTY_QUESTION, BtwRefusal.UNSUPPORTED, BtwRefusal.FAILED -> Unit + } + } + } + } + + /** + * Web `ensure_session` parity for `/btw` typed in a brand-new chat: the + * pane created the session; the question is asked here once it is open. + */ + fun askSideQuestionInNewChat(sessionId: String, raw: String) { + if (this.sessionId == sessionId) { + askSideQuestion(raw) + } else { + pendingSideQuestions[sessionId] = raw + } + } + + /** The 6 s compaction result banner (web `compactResult`). */ + private fun showCompactResult(result: CompactResult?) { + compactResultJob?.cancel() + _ui.update { it.copy(compactResult = result, compactError = null) } + if (result == null) return + compactResultJob = viewModelScope.launch { + delay(COMPACT_RESULT_VISIBLE_MILLIS) + _ui.update { it.copy(compactResult = null) } + } + } + + /** The 3 s compaction failure banner (web `compactError`). */ + private fun showCompactError(message: String?) { + compactErrorJob?.cancel() + _ui.update { it.copy(compactError = message, compactResult = null) } + if (message == null) return + compactErrorJob = viewModelScope.launch { + delay(COMPACT_ERROR_VISIBLE_MILLIS) + _ui.update { it.copy(compactError = null) } + } + } + + /** + * `sendQueued` of the web composer, used during a run and by dictation + * transcripts with a stored `after` choice: web slash commands still + * expand, everything else goes through steer/follow_up. + */ + private fun queueOrSteer(submission: ComposerSubmission, route: ComposerRoute, mode: String) { + val sid = sessionId ?: return + val key = composer.currentDraftKey + // A side question is never queued behind the run it sits beside. + if (isSideQuestion(submission.raw)) { + submitSideQuestion(submission) + return + } + when (route) { + is ComposerRoute.Usage -> { + _events.tryEmit( + ChatEvent.Toast(composerStrings.requiresArgs(route.command, route.argumentHint)), + ) + return + } + is ComposerRoute.WebPrompt -> { + if (rejectsOversized(route.prompt, submission.images)) return + dispatchQueued(sid, mode, route.prompt, submission.images, key) + } + is ComposerRoute.Bash, is ComposerRoute.Builtin, is ComposerRoute.Prompt -> + dispatchQueued(sid, mode, submission.prompt, submission.images, key) + } + } + + private fun dispatchQueued( + sid: String, + mode: String, + text: String, + images: List, + key: String?, + ) { + if (text.isBlank() && images.isEmpty()) return + composer.clear() + viewModelScope.launch { + try { + val reply = if (mode == "steer") api.steer(sid, text, images) else api.followUp(sid, text, images) + if (isQueuedWhileShellRunning(reply)) { + _events.tryEmit(ChatEvent.Toast(composerStrings.queuedWhileShellRunning)) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: ApiError) { + // The queue call never reached omp: text and images go back to + // the draft they were sent from, even after a session switch. + if (error is ApiError.SessionSlow && error.maybeStarted) { + _events.tryEmit(ChatEvent.Toast(maybeStartedMessage)) + } else { + recoverDraft(key, text, images) + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } + } catch (error: Exception) { + recoverDraft(key, text, images) + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } + } + } + + /** Put queued text back where it came from (`recoverDraft` in the web hook). */ + private fun recoverDraft(key: String?, text: String, images: List) { + if (key.isNullOrEmpty() || (text.isEmpty() && images.isEmpty())) return + drafts.recoverDraft(key, DraftRecovery(text = text, images = images)) + if (key == composer.currentDraftKey) composer.applyStoredDraft() + } + + /** + * A non-send command was refused because another omp process owns the chat + * (server `409 session_running_elsewhere`). Show the ownership banner the + * prompt path uses and say the command was not applied — the send wording + * ("your message was not sent") would be wrong. Returns true when handled. + */ + private fun reportForeignCommand(error: Exception): Boolean { + if (error !is ApiError.SessionRunningElsewhere) return false + engine.setForeignRun(error.elsewhere ?: engine.state.value.foreignRun) + _events.tryEmit(ChatEvent.Toast(composerStrings.foreignCommandBlocked)) + return true + } + + /** Returns true when the shell command actually started (the draft may clear). */ + private fun executeBash(invocation: BashInvocation): Boolean { + val sid = sessionId ?: run { + _events.tryEmit(ChatEvent.Toast(composerStrings.shellSessionFailed)) + return false + } + val state = engine.state.value + if (state.bashRunning || state.busy) return false + engine.bashStarted() + viewModelScope.launch { + try { + api.bash(sid, invocation.command, invocation.excludeFromContext) + requestHistory(reset = false) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + val input = (if (invocation.excludeFromContext) "!!" else "!") + invocation.command + if (!reportForeignCommand(error)) engine.notify(NoticeKind.Error, error.userMessage()) + composer.insertIfEmpty(input) + } finally { + engine.bashFinished() + pumpEffects() + } + } + return true + } + + /** + * Run a client-owned `/command`. The caller already knows it is ours; a + * command that needs a session without one keeps the typed text. + */ + private fun runBuiltinAction(name: String, args: String) { + val sid = sessionId + if (sid == null) { + if (name in setOf("copy", "compact", "reload", "name", "session")) { + _events.tryEmit(ChatEvent.Toast(composerStrings.noSession)) + } + return + } + when (name) { + "compact" -> { + composer.clear() + viewModelScope.launch { + try { + val reply = api.compact(sid, args.ifEmpty { null }) + _events.tryEmit(ChatEvent.Toast(composerStrings.compactedContext)) + val parsed = readCompactResult(reply, "manual") + if (parsed != null) { + showCompactResult( + CompactResult( + reason = parsed.reason, + tokensBefore = parsed.tokensBefore.toLong(), + estimatedTokensAfter = parsed.estimatedTokensAfter.toLong(), + ), + ) + } + requestHistory(reset = true) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } + } + } + "reload" -> { + composer.clear() + viewModelScope.launch { + try { + api.reload(sid) + _events.tryEmit(ChatEvent.Toast(composerStrings.reloadedResources)) + requestHistory(reset = false) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } + } + } + "name" -> { + if (args.isEmpty()) { + _events.tryEmit(ChatEvent.Toast(composerStrings.nameUsage)) + return + } + composer.clear() + viewModelScope.launch { + try { + api.setSessionName(sid, args) + _events.tryEmit(ChatEvent.Toast(composerStrings.renamed(args))) + loadMeta(sid) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } + } + } + "session" -> { + composer.clear() + controls.openContextSheet() + controls.refreshSessionStats() + } + "copy" -> { + composer.clear() + viewModelScope.launch { + try { + val text = api.lastAssistantText(sid) + if (text.isNullOrBlank()) { + _events.tryEmit(ChatEvent.Toast(composerStrings.noMessageToCopy)) + } else { + _events.emit(ChatEvent.CopyText(text)) + _events.tryEmit(ChatEvent.Toast(composerStrings.copiedLastMessage)) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } + } + } + } + } + + private fun rejectsOversized(prompt: String, images: List): Boolean { + val error = validateOutgoingPrompt(prompt, images) ?: return false + _events.tryEmit(ChatEvent.Toast(error)) + return true + } + + private fun applyDictationResult(result: DictationResult) { + val outcome = dictationOutcome(result, composer.state.value.text, hasSession = sessionId != null) + if (outcome is DictationOutcome.Insert) { + composer.insertAtCaret(outcome.text) + return + } + // Build the submission from the composer so attached images and files + // ride along, exactly like typing the transcript and pressing send. + val submission = composer.resolve(overrideText = outcome.text) ?: return + when (outcome) { + is DictationOutcome.Send -> submitResolved(submission, forceQueue = false) + is DictationOutcome.Steer -> queueOrSteer(submission, routeOf(submission), "steer") + is DictationOutcome.FollowUp -> queueOrSteer(submission, routeOf(submission), "followUp") + is DictationOutcome.Insert -> Unit + } + } + + private fun routeOf(submission: ComposerSubmission): ComposerRoute = routeComposerSubmission( + raw = submission.raw, + prompt = submission.prompt, + hasAttachments = submission.hasAttachments, + hasImages = submission.images.isNotEmpty(), + bash = submission.bash, + ) + + // ------------------------------------------------------------- public API + + /** + * Show [sessionId]; switching cancels the previous session's work. + * + * [elsewhere] is the session list's owner report for this session (its + * `elsewhereSessions`). It seeds the banner right away, so a chat the list + * already marks as "running elsewhere" never looks free while this chat's + * own live-state read is still on the wire (or when it is refused). + */ + fun open(sessionId: String, cwd: String?, elsewhere: ElsewhereSession? = null) { + if (this.sessionId == sessionId) { + if (cwd != null && cwd != this.cwd) { + this.cwd = cwd + composer.bind(sessionId, cwd, sessionId) + controls.bind(sessionId, cwd) + publish() + loadMeta(sessionId) + } + // The owner can appear while the chat is open (a `running` frame + // reaches the list first): the banner must not wait for a send. + seedForeignRun(elsewhere) + // A surface recreated after [onChatHidden] (rotation, back to the + // chat) re-arms the consent scopes it dropped on dispose. + onChatVisible() + return + } + val previousId = this.sessionId + // A submit still waiting for the live state belongs to the chat being + // left: its composer is gone, and the new chat must not stay locked on + // "Sending…" until that read (bounded, but still seconds) returns. + abandonPendingSubmit() + if (previousId != null) { + scrollSaveJob?.cancel() + val next = if (lastNearBottom) 0 else currentScrollIndex + if (next != lastPersistedScroll) { + viewModelScope.launch { settings.setChatScrollPosition(previousId, next) } + } + } + this.sessionId = sessionId + this.cwd = cwd + val savedSession = savedStateHandle.get(KEY_SESSION) + val restoring = savedSession == sessionId + val savedScroll = if (restoring) savedStateHandle.get(KEY_SCROLL) ?: 0 else 0 + val restore = savedScroll.takeIf { it > 0 } + ?: settings.snapshot.value.chatScrollPositions[sessionId] + ?: 0 + viewLeafId = if (restoring) savedStateHandle.get(KEY_LEAF) else null + detailLeafId = null + historyCursor = null + historyJob?.cancel() + historyJob = null + historyRequested = false + pendingReset = false + historyGeneration++ + settleJob?.cancel() + dismissedNotices.clear() + thinkingCache.clear() + rowsCacheSource = null + committedToolResults = emptyMap() + forkCache = emptyList() + currentScrollIndex = 0 + lastPersistedScroll = 0 + lastNearBottom = true + savedStateHandle[KEY_SESSION] = sessionId + savedStateHandle[KEY_LEAF] = viewLeafId + savedStateHandle[KEY_SCROLL] = 0 + engine.setSession(sessionId, viewLeafId) + // The list may already know the owner (it hears the `running` frames + // first); the seed paints the banner before this chat's own read + // answers, and that read still has the last word. + seedForeignRun(elsewhere) + // A search belongs to the transcript it was typed in; the reveal hit of + // the session list survives the switch it triggered. + val reveal = pendingReveals.remove(sessionId) + _search.update { ChatSearchState(revealEntryId = reveal) } + _ui.update { + it.copy( + sessionId = sessionId, + branch = null, + subagent = null, + branchTree = emptyList(), + branchHasBranch = false, + githubRepo = null, + restoreScrollIndex = restore.takeIf { index -> index > 0 }, + prefill = prefillBySession.remove(sessionId), + activeGoal = settings.snapshot.value.activeGoals[sessionId], + planObjective = null, + systemPrompt = SystemPromptState.Idle, + // A move belongs to the chat that was copied, not the next one. + contentionCopy = null, + ) + } + composer.bind(sessionId, cwd, sessionId) + controls.bind(sessionId, cwd.orEmpty()) + dictation.bindScope(sessionId) + onChatVisible() + hostTools.register(sessionId) + startSubscription(sessionId) + // Cache-first: the transcript may be the disk copy, and an idle session + // has no event stream to say so (its channel is refused). One cheap + // `GET /api/agent/{id}` settles the live state, so the footer can stop + // saying "Connecting…" and report the real run state. + reconcileNow() + loadMeta(sessionId) + // The session file's task toolResults hold finished runs from earlier + // visits (web `loadSession` → `refreshSubagentHistory`). + refreshSubagentHistory(sessionId) + pumpEffects() + pendingSideQuestions.remove(sessionId)?.let { raw -> askSideQuestion(raw) } + // The last-view marker is written by the shell when it selects the + // session (`SessionsViewModel.showSession`), before this detail 404s. + } + + /** + * The chat surface is on screen: host tools and extension panels may act + * for this session. Extension panes are re-bound, which is idempotent for + * the same session and re-shows the panels it kept while hidden. + */ + fun onChatVisible() { + val sid = sessionId ?: return + hostTools.setViewedSession(sid) + extensionUi.bind(sid) + btw.bind(sid) + } + + /** + * The chat surface left the stage (list, rotation, sign-out). Nothing may + * keep acting as if the user still watched this session: the host consent + * scope closes, the extension panels stop being reachable and an active + * dictation ends (same rule as the composer's ON_STOP in + * `ReportVisibility`). The recognizer and recorder have no UI left to + * report to; leaving them running would keep the microphone busy behind + * the list. The extension panels themselves stay in `ExtensionUiController` + * (its subscription keeps running) — [bind] drops them only on a session + * change, and [clearSessionData] wipes them. + */ + fun onChatHidden() { + hostTools.setViewedSession(null) + extensionUi.bind(null) + btw.bind(null) + if (dictation.state.value is DictationState.Recording) dictation.stop() + dictation.setVisible(false) + } + + /** + * Drops every piece of the signed-in user's data this view model holds in + * memory: transcript rows, the engine's messages, fetched thinking text, + * subagent dialogs and the session subscription. `AppContainer` + * (`clearLocalUserData`) clears the disk cache and bumps the session epoch + * on sign-out, a server switch and a 401; this keeps the same data from + * surviving in the heap (a debug/bugreport heap dump, or the next server's + * UI). + */ + fun clearSessionData() { + // The waiting submit holds the drafted text of a chat that is being + // dropped: it must not route it, and it must not keep a "Sending…" + // flag for the next sign-in either. + abandonPendingSubmit() + historyJob?.cancel() + historyJob = null + historyRequested = false + pendingReset = false + historyGeneration++ + historyCursor = null + metaJob?.cancel() + metaJob = null + subscriptionJob?.cancel() + subscriptionJob = null + settleJob?.cancel() + settleJob = null + scrollSaveJob?.cancel() + scrollSaveJob = null + subagentWatchJob?.cancel() + subagentCompletionJob?.cancel() + subagentTranscriptJob?.cancel() + subagentDetailJob?.cancel() + sessionId = null + cwd = null + viewLeafId = null + detailLeafId = null + savedStateHandle.remove(KEY_SESSION) + savedStateHandle.remove(KEY_LEAF) + rowsCacheSource = null + rowsCache = emptyList() + rowsCacheHideThinking = false + thinkingCache.clear() + dismissedNotices.clear() + committedToolResults = emptyMap() + forkCache = emptyList() + prefillBySession.clear() + pendingReveals.clear() + pendingSideQuestions.clear() + warnedSlashCommands.clear() + lastSessionModelKey = null + lastSessionThinking = null + engine.setSession(null, null) + composer.bind(null, null, null) + hostTools.setViewedSession(null) + extensionUi.reset() + btw.bind(null) + _search.value = ChatSearchState() + _ui.value = ChatUiState( + preferences = preferences, + submitDuringRun = _ui.value.submitDuringRun, + todoCollapsed = _ui.value.todoCollapsed, + subagentsCollapsed = _ui.value.subagentsCollapsed, + connection = eventMux.state.value, + ) + } + + fun retryHistory() { + requestHistory(reset = true) + } + + /** Direct prompt send without composer state (tests and programmatic callers). */ + fun send(text: String) { + val message = text.trim() + if (message.isEmpty()) return + sendPrompt( + ComposerSubmission( + raw = message, + prompt = message, + images = emptyList(), + hasAttachments = false, + bash = null, + ), + clearOnSuccess = false, + ) + } + + /** + * Start a prompt run. [submission] has already passed the composer's size + * validation; the optimistic turn, settlement, `session_slow` handling and + * the text rollback on failure are the web `handleSend` rules. [force] + * appends `?force=1`: the send goes into a copy of a chat another omp + * process owns, only ever after the user confirmed it. + */ + private fun sendPrompt( + submission: ComposerSubmission, + clearOnSuccess: Boolean, + force: Boolean = false, + ): Boolean { + val sid = sessionId ?: return false + val message = submission.prompt.trim() + val images = submission.images + if (message.isEmpty() && images.isEmpty()) return false + val state = engine.state.value + if (state.busy || state.bashRunning) return false + + // omp runs TUI-only commands nowhere but its terminal; over RPC the text + // reaches the model as literal slash text, so say it once per name. + if (message.startsWith("/") && images.isEmpty()) { + val name = message.removePrefix("/").substringBefore(' ').substringBefore('\n') + val known = isClientBuiltinCommand(name) || + engine.state.value.remoteCommands.any { it.name == name || it.name == "skill:$name" } + if (!known && name.isNotEmpty() && warnedSlashCommands.add(name)) { + _events.tryEmit(ChatEvent.Toast(composerStrings.unknownCommand(name))) + } + } + + val key = composer.currentDraftKey + val rawForRecovery = submission.raw + val imagesForRecovery = images + if (clearOnSuccess) composer.clear() + + val runId = ++runCounter + engine.sendStarted(message, runId, command = message.startsWith("/") && images.isEmpty()) + ensureSubscribed(sid) + // The wrapper starts with this prompt: (re)register the host-tool + // bridge and re-read the live roster (web send path). + hostTools.register(sid) + refreshSubagentRoster(sid) + viewModelScope.launch { + try { + val result = api.prompt(sid, message, images, force = force) + if (engine.state.value.runId != runId) return@launch + val root = result as? JsonObject + val queued = root?.get("queued")?.jsonPrimitive?.booleanOrNull == true + if (queued) { + val reason = root["reason"]?.jsonPrimitive?.contentOrNull + engine.promptQueued( + if (reason == "shellRunning") composerStrings.queuedWhileShellRunning else reason, + ) + } else { + scheduleSettlement(runId) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (engine.state.value.runId == runId) { + // The frame may already sit in omp's stdin: keep the + // optimistic turn and the run (sending stays locked) and + // only surface the hint. The next frame or successful + // reconcile settles it. + if (error is ApiError.SessionSlow && error.maybeStarted) { + engine.promptMaybeStarted(maybeStartedMessage) + } else if (error is ApiError.SessionRunningElsewhere) { + // Another omp process owns the chat (it started writing + // between our state read and the send). Nothing was + // sent: drop the optimistic turn, keep the text and + // offer the explicit copy. + engine.setForeignRun(error.elsewhere ?: engine.state.value.foreignRun) + engine.sendFailed(null) + recoverDraft(key, rawForRecovery, imagesForRecovery) + if (force) { + engine.notify(NoticeKind.Warning, composerStrings.foreignSendBlocked) + } else { + _ui.update { it.copy(foreignSend = submission) } + } + } else { + engine.sendFailed(error.userMessage()) + // Hand the text back instead of losing it with the + // rolled-back bubble (web `chatInputRef.insertIfEmpty`). + recoverDraft(key, rawForRecovery, imagesForRecovery) + } + } + } finally { + pumpEffects() + } + } + return true + } + + /** `abort_retry`: let the auto-retry banner stop waiting for the backoff. */ + fun abortRetry() { + val sid = sessionId ?: return + viewModelScope.launch { + try { + api.abortRetry(sid) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } + } + } + + // ------------------------------------------------------------------ queue + + /** Edit a queued chip: withdraw it from omp, then restore text and images. */ + fun editQueued(queue: String, text: String) { + mutateQueued(queue, text) { result -> + val restoredText = if (text == "[Image]" && result.images.isNotEmpty()) "" else text + recoverDraft(sessionId, restoredText, result.images) + } + } + + /** Delete a queued chip (the UI asked for confirmation first). */ + fun deleteQueued(queue: String, text: String) { + mutateQueued(queue, text) { } + } + + private fun mutateQueued( + queue: String, + text: String, + onRemoved: (QueuedMessageRemoval) -> Unit, + ) { + val sid = sessionId ?: return + if (queueMutationInFlight || text.isEmpty()) return + queueMutationInFlight = true + viewModelScope.launch { + try { + val result = api.removeQueuedMessage(sid, text, queue) + if (result.removed) { + onRemoved(result) + } else { + _events.tryEmit(ChatEvent.Toast(composerStrings.queuedRemovalUnavailable)) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } finally { + queueMutationInFlight = false + } + } + } + + /** Promote a queued follow-up into omp's steering queue. */ + fun steerQueued(text: String) { + val sid = sessionId ?: return + if (queueMutationInFlight || text.isEmpty()) return + if (text !in engine.state.value.queue.followUp) return + queueMutationInFlight = true + viewModelScope.launch { + try { + if (!api.promoteQueuedMessage(sid, text)) { + _events.tryEmit(ChatEvent.Toast(composerStrings.queuedPromotionUnavailable)) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } finally { + queueMutationInFlight = false + } + } + } + + /** + * Republish the live tokens/s estimate without a frame. The composer + * ticker calls this while a reply streams, so a stalled stream ages out + * instead of keeping its last number. + */ + fun refreshLiveRate() { + engine.refreshLiveRate() + } + + /** Stop a `!!` shell command or the run, restoring omp's queue to the draft. */ + fun stop() { + val sid = sessionId ?: return + if (engine.state.value.bashRunning) { + viewModelScope.launch { + try { + api.abortBash(sid) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } + } + return + } + if (_ui.value.stopping) return + _ui.update { it.copy(stopping = true) } + viewModelScope.launch { + try { + withdrawAndAbort(sid) + } finally { + pumpEffects() + _ui.update { it.copy(stopping = false) } + delay(ABORT_RECONCILE_MILLIS) + reconcileNow() + } + } + } + + /** + * Port of `withdrawAndAbort`: `abort_and_restore_queue` twice, fenced to + * the run captured at the click; when omp does not know the command, its + * pending messages are withdrawn one by one before `abort`. + */ + private suspend fun withdrawAndAbort(sid: String) { + val runId = engine.state.value.runId + val runsEnded = engine.state.value.runsEnded + fun sameRun(): Boolean = + engine.state.value.runId == runId && engine.state.value.runsEnded == runsEnded + + var restored: QueueRestore? = null + var unsupported = false + var failures = 0 + var abortUnconfirmed = false + for (attempt in 0..1) { + if (attempt > 0 && (!sameRun() || unsupported || abortUnconfirmed || restored != null)) break + try { + restored = api.abortAndRestoreQueue(sid) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + unsupported = error.message?.contains("Unknown command") == true || + (error is ApiError.ServerResponse && error.code == "unknown_command") + if (!unsupported) failures++ + if (error is ApiError.SessionSlow && error.abortUnconfirmed) abortUnconfirmed = true + } + } + if (abortUnconfirmed) { + _events.tryEmit(ChatEvent.Toast(stopUnconfirmedMessage)) + return + } + if (!unsupported) { + val entries = restored?.entries.orEmpty() + val texts = entries + .filterNot { it.text == "[Image]" && it.images.isNotEmpty() } + .map { it.text } + .filter { it.isNotEmpty() } + val images = entries.flatMap { it.images } + if (texts.isNotEmpty() || images.isNotEmpty()) { + recoverDraft(sid, texts.joinToString("\n\n"), images) + } + engine.abortSent() + if (failures > 0 && texts.isEmpty()) { + _events.tryEmit(ChatEvent.Toast(composerStrings.queueRestoreUncertain)) + } + return + } + + // omp before `abort_and_restore_queue`: withdraw first, abort second. + val snapshot = engine.state.value.queue + data class Entry(val text: String, val queue: String) + val entries = snapshot.followUp.map { Entry(it, "followUp") } + snapshot.steering.map { Entry(it, "steering") } + val removed = BooleanArray(entries.size) + val restoredFlag = BooleanArray(entries.size) + var recoveredBlock = "" + var failed = false + var cancelled = false + + fun recoverWithdrawn() { + val fresh = entries.indices.filter { removed[it] && !restoredFlag[it] }.map { entries[it].text } + if (fresh.isEmpty()) return + for (index in entries.indices) if (removed[index]) restoredFlag[index] = true + val block = entries.indices.filter { restoredFlag[it] }.joinToString("\n\n") { entries[it].text } + drafts.recoverDraft( + sid, + DraftRecovery( + text = block, + replace = if (recoveredBlock.isEmpty()) null else DraftRecovery.Replace(recoveredBlock, fresh.joinToString("\n\n")), + ), + ) + if (sid == composer.currentDraftKey) composer.applyStoredDraft() + recoveredBlock = block + } + + kotlinx.coroutines.coroutineScope { + val removals = entries.mapIndexed { index, entry -> + async { + val queues = if (entry.queue == "followUp") listOf("followUp", "steering") else listOf("steering") + try { + for (queue in queues) { + if (queue == "steering" && entry.queue == "followUp" && !sameRun()) return@async + val result = api.removeQueuedMessage(sid, entry.text, queue) + if (result.removed) { + removed[index] = true + return@async + } + } + } catch (cancel: CancellationException) { + throw cancel + } catch (_: Exception) { + failed = true + } + } + } + withTimeoutOrNull(WITHDRAW_BEFORE_ABORT_MILLIS) { + removals.forEach { it.await() } + } + recoverWithdrawn() + if (sameRun()) { + try { + api.abort(sid) + engine.abortSent() + } catch (cancel: CancellationException) { + throw cancel + } catch (error: Exception) { + if (error is ApiError.SessionSlow && error.abortUnconfirmed) { + _events.tryEmit(ChatEvent.Toast(stopUnconfirmedMessage)) + } + } + } + removals.forEach { it.await() } + recoverWithdrawn() + } + if (failed) { + _events.tryEmit(ChatEvent.Toast(composerStrings.queueDiscardFailed)) + } + } + + fun togglePreCompactionHistory() { + engine.setIncludePreCompaction(!engine.state.value.includePreCompaction) + pumpEffects() + } + + fun dismissNotice(notice: ChatNotice) { + dismissedNotices += noticeKey(notice) + // Also drop it from the engine: the dedupe there would otherwise + // swallow the same message for the rest of the session. + engine.dismissNotice(notice) + publish() + } + + fun consumeRestoreScroll() { + if (_ui.value.restoreScrollIndex != null) _ui.update { it.copy(restoreScrollIndex = null) } + } + + /** + * The transcript's first visible row. Only a reading position is + * persisted; an end-anchored (bottom) view stores 0 so the next open jumps + * to the newest turn. + */ + fun onScrollPosition(rowIndex: Int, nearBottom: Boolean) { + currentScrollIndex = rowIndex + lastNearBottom = nearBottom + val sid = sessionId ?: return + val next = if (nearBottom) 0 else rowIndex + if (next == lastPersistedScroll) return + savedStateHandle[KEY_SCROLL] = next + savedStateHandle[KEY_LEAF] = viewLeafId + scrollSaveJob?.cancel() + scrollSaveJob = viewModelScope.launch { + delay(SCROLL_SAVE_DEBOUNCE_MILLIS) + settings.setChatScrollPosition(sid, next) + lastPersistedScroll = next + } + } + + fun onForeground() { + requestHistory(reset = false) + reconcileNow() + } + + // ---------------------------------------------------------------- history + + private fun requestHistory(reset: Boolean) { + if (sessionId == null) return + if (reset) { + pendingReset = true + // A reset (branch/compaction/reconnect) must not publish the page + // that is still in flight for the previous view. + if (historyJob?.isActive == true) historyGeneration++ + } + historyRequested = true + if (historyJob?.isActive == true) return + historyJob = viewModelScope.launch { historyLoop() } + } + + private suspend fun historyLoop() { + while (historyRequested) { + historyRequested = false + val sid = sessionId ?: return + val reset = pendingReset + pendingReset = false + if (reset) historyCursor = null + val generation = ++historyGeneration + engine.pagesLoading(true) + try { + if (reset || historyCursor == null) { + loadFullHistory(sid, generation) + } else { + loadIncrementalHistory(sid, generation) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (generation == historyGeneration && sessionId == sid) { + engine.historyRequestFailed(error.userMessage()) + } + } finally { + engine.pagesLoading(false) + } + if (generation == historyGeneration && sessionId != sid) return + reconcileNow() + } + } + + /** + * Whole display history in ONE request (web `loadContext`), then publish a + * single replacement page. Paging through `?sync=1` pages up front would + * re-read the session file per page and delay first paint on large files. + * + * Cache-first: the last response for exactly this view (session, leaf, + * compaction setting) is painted while the request is on the wire, and the + * response replaces it and refreshes the cache. Nothing is seeded when the + * engine already holds content, so a live frame or a page that arrived + * first always wins. + */ + private suspend fun loadFullHistory(sid: String, generation: Int) { + val request = HistoryView( + // Frozen for the whole request: a server switch while the body is + // on the wire must neither seed nor store under the new origin. + origin = settings.snapshot.value.origin?.origin, + leafId = viewLeafId, + includePreCompaction = engine.state.value.includePreCompaction, + ) + seedCachedHistory(sid, request, generation) + val response = loadFullContext(sid, request) + val page = ContextSync.parse(response.root) + if (generation != historyGeneration || sessionId != sid) return + if (request.origin != settings.snapshot.value.origin?.origin) return + if (page.sessionId != null && page.sessionId != sid) return + if (page.entryIds.size != page.messages.size) throw ChatHistoryException("Invalid history page") + val loaded = page.copy( + mode = "replace", + baseEntryId = null, + cursor = null, + hasMore = false, + sessionId = sid, + ) + // Later appends page from the delivered tail (live events, agent_end). + historyCursor = HistoryCursor(page.entryIds.firstOrNull(), page.entryIds.lastOrNull()) + engine.applyContextPage(loaded) + storeCachedHistory(sid, request, response.body, page) + } + + /** + * The view a full-history request is answered for: the cache key's other + * half plus the origin the request went to. The origin is captured when + * the request starts, never when its answer is written. + */ + private data class HistoryView( + val origin: String?, + val leafId: String?, + val includePreCompaction: Boolean, + ) + + /** One `GET .../context` answer: the body as sent, plus the parsed root. */ + private data class HistoryResponse(val body: String?, val root: JsonObject) + + /** + * Paints the cached transcript of [view] before the request leaves. Only + * an empty engine is seeded, and only the transcript half of the page: the + * cached live snapshot (stream cursor, running tools) is stale by + * definition and must never be replayed. + */ + private suspend fun seedCachedHistory(sid: String, view: HistoryView, generation: Int) { + val cache = transcripts ?: return + val origin = view.origin ?: return + val body = withContext(cacheDispatcher) { + cache.read(origin, sid, view.leafId, view.includePreCompaction) + } ?: return + if (generation != historyGeneration || sessionId != sid) return + if (settings.snapshot.value.origin?.origin != origin) return + val page = runCatching { + ContextSync.parse(Json.parseToJsonElement(body).jsonObject) + }.getOrNull() ?: return + if (page.sessionId != null && page.sessionId != sid) return + if (page.entryIds.size != page.messages.size) return + engine.seedCachedHistory( + page.copy( + mode = "replace", + baseEntryId = null, + cursor = null, + hasMore = false, + sessionId = sid, + live = null, + ), + ) + } + + /** Refreshes the cache with a complete answer for [view]. */ + private suspend fun storeCachedHistory( + sid: String, + view: HistoryView, + body: String?, + page: ContextPage, + ) { + val cache = transcripts ?: return + if (body.isNullOrEmpty() || page.messages.isEmpty() || page.hasMore) return + val origin = view.origin ?: return + // The answer belongs to the server the request went to. A switch while + // it was on the wire (the container already wiped the disk cache) must + // not file another installation's transcript — nor undo the wipe. + if (settings.snapshot.value.origin?.origin != origin) return + withContext(cacheDispatcher) { + cache.write(origin, sid, view.leafId, view.includePreCompaction, body) + } + } + + /** + * The whole history root for [sid]. omp does not write the session file + * until the first assistant message exists, so a session that is already + * running can answer 404 without a load problem — the server's sync route + * answers an empty page for exactly this case. Treat that 404 as the empty + * transcript it is (a notification tap into a dialog of a fresh session + * must not paint a load error); a 404 from a session the server no longer + * runs keeps the error. + */ + private suspend fun loadFullContext(sid: String, view: HistoryView): HistoryResponse { + try { + val body = api.contextFullRaw( + sessionId = sid, + leafId = view.leafId, + includePreCompaction = view.includePreCompaction, + ) + return HistoryResponse(body, parseContextBody(body)) + } catch (missing: ApiError.Http) { + if (missing.code != 404 || !sessionIsLive(sid)) throw missing + return HistoryResponse(null, JsonObject(emptyMap())) + } + } + + /** + * Same contract as the HTTP client's `decode`: a body that is not a JSON + * object is an `ApiError.Http`, not a crash inside the history loop. + */ + private fun parseContextBody(body: String): JsonObject = try { + Json.parseToJsonElement(body).jsonObject + } catch (error: SerializationException) { + throw ApiError.Http(200, body.take(2048)) + } + + /** `GET /api/agent/{id}`: the server still has a runner (child) for [sid]. */ + private suspend fun sessionIsLive(sid: String): Boolean = try { + val body = api.agentState(sid) + // `alive` is the child; `running` is a turn in flight. An idle wrapper + // still serves the session, so only `alive` answers this. An older + // server has no `alive` and used `running` for exactly this. + (body["alive"] ?: body["running"])?.jsonPrimitive?.booleanOrNull == true + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + false + } + + /** One append page; a reset/truncation answer falls back to a full reload. */ + private suspend fun loadIncrementalHistory(sid: String, generation: Int) { + var cursor = historyCursor ?: return loadFullHistory(sid, generation) + var pages = 0 + while (true) { + val page = fetchPage(sid, encodeCursor(cursor)) + if (generation != historyGeneration || sessionId != sid) return + if (page.sessionId != null && page.sessionId != sid) return + if (page.entryIds.size != page.messages.size) throw ChatHistoryException("Invalid history page") + val appendable = page.mode == "append" && + page.baseEntryId == engine.state.value.entryIds.lastOrNull() + if (!appendable) { + // The selected branch, compaction view or the file changed. + return loadFullHistory(sid, generation) + } + engine.applyContextPage(page) + val next = page.cursor ?: break + cursor = next + historyCursor = next + pages++ + if (!page.hasMore || pages > MAX_PAGES) break + } + } + + private suspend fun fetchPage(sid: String, cursor: String?): ContextPage { + val root = api.contextSync( + sessionId = sid, + cursor = cursor, + leafId = viewLeafId, + includePreCompaction = engine.state.value.includePreCompaction, + ) + return ContextSync.parse(root) + } + + private fun encodeCursor(cursor: HistoryCursor?): String? { + if (cursor?.lastEntryId == null) return null + return buildJsonObject { + put("firstEntryId", cursor.firstEntryId) + put("lastEntryId", cursor.lastEntryId) + }.toString() + } + + // ----------------------------------------------------------- meta & branch + + private fun loadMeta(sid: String) { + metaJob?.cancel() + metaJob = viewModelScope.launch { + try { + val detail = api.sessionDetail(sid) + if (sessionId != sid) return@launch + detailLeafId = detail.leafId + val tree = BranchTree.parse(detail.tree) + _ui.update { + it.copy( + branchTree = tree, + branchHasBranch = BranchTree.hasBranch(tree), + branch = it.branch, + ) + } + if (tree.isNotEmpty() && _ui.value.branch != null) openBranchNavigator() + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // A running-only session has no file yet; the branch tree is + // simply empty until the first write. + } + val directory = cwd + if (directory.isNullOrBlank()) return@launch + try { + val repo = api.githubRepo(directory) ?: return@launch + val slash = repo.indexOf('/') + if (slash <= 0 || slash == repo.lastIndex) return@launch + if (sessionId != sid) return@launch + _ui.update { + it.copy(githubRepo = GithubRepoRef(repo.substring(0, slash), repo.substring(slash + 1))) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // Not a GitHub checkout, or the cwd is outside the allowed roots. + } + } + } + + fun openBranchNavigator() { + val tree = _ui.value.branchTree + val active = viewLeafId ?: detailLeafId + _ui.update { + it.copy(branch = BranchNavigatorState(BranchTree.rows(tree, active))) + } + } + + fun closeBranchNavigator() { + _ui.update { it.copy(branch = null) } + } + + /** Returns false when the view cannot branch (a running turn owns the transcript). */ + fun selectLeaf(leafId: String?): Boolean { + if (engine.state.value.busy) return false + viewLeafId = leafId + savedStateHandle[KEY_LEAF] = leafId + engine.setLeaf(leafId) + _ui.update { state -> + state.copy(restoreScrollIndex = null) + } + pumpEffects() + return true + } + + /** + * "Edit from here" on a user row: branch the view back to the assistant + * answer before that turn and put its text into the composer, without + * forking the session (web `ChatWindow` edit affordance). Like the web + * (`MessageView` only prefills after `onNavigate()` succeeded, and + * `insertIfEmpty` never overwrites a draft), a failed navigation aborts + * and an existing draft stays untouched. + */ + fun editFromHere(prevAssistantEntryId: String, text: String) { + if (!selectLeaf(prevAssistantEntryId)) return + if (composer.state.value.text.isNotBlank()) return + composer.setText(text) + } + + // ------------------------------------------------------------------ fork + + fun fork(entryId: String, editPrompt: Boolean) { + val sid = sessionId ?: return + if (engine.state.value.busy) return + if (_ui.value.forkingEntryId != null) return + _ui.update { it.copy(forkingEntryId = entryId) } + viewModelScope.launch { + try { + val outcome = api.forkSession(sid, entryId) + val newSessionId = outcome.newSessionId + when { + // omp skipped the branch (a run was in flight); saying so is + // the only feedback the user would get otherwise. + outcome.cancelled -> _events.tryEmit(ChatEvent.ForkCancelled) + newSessionId.isNullOrBlank() -> _events.tryEmit(ChatEvent.ForkFailed) + else -> { + if (editPrompt) { + val text = outcome.text.orEmpty() + if (text.isNotBlank()) prefillBySession[newSessionId] = text + } + _events.tryEmit(ChatEvent.Forked(newSessionId)) + } + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (!reportForeignCommand(error)) _events.tryEmit(ChatEvent.Toast(error.userMessage())) + } finally { + _ui.update { it.copy(forkingEntryId = null) } + } + } + } + + // -------------------------------------------------------- thinking & files + + fun loadThinking(entryId: String?, blockIndex: Int) { + val sid = sessionId ?: return + if (entryId == null) return + val key = thinkingKey(entryId, blockIndex) + if (thinkingCache[key] != null) return + thinkingCache[key] = ThinkingEntry.Loading + publish() + viewModelScope.launch { + try { + val text = api.thinking(sid, entryId, blockIndex) + putThinking(key, ThinkingEntry.Text(text)) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + putThinking(key, ThinkingEntry.Error(error.userMessage())) + } + } + } + + private fun putThinking(key: String, entry: ThinkingEntry) { + thinkingCache[key] = entry + while (thinkingCache.size > THINKING_CACHE_LIMIT) { + val oldest = thinkingCache.keys.first() + thinkingCache.remove(oldest) + } + publish() + } + + /** Fetch the full output a truncated `bashExecution` row points at. */ + fun loadBashOutput(path: String) { + val sid = sessionId ?: return + if (_ui.value.bashOutputs[path] is BashOutputUi.Ready) return + _ui.update { it.copy(bashOutputs = it.bashOutputs + (path to BashOutputUi.Loading)) } + viewModelScope.launch { + val next = try { + when (val result = api.bashOutput(sid, path)) { + is io.github.kahme247.ompweb.net.BashOutputResult.Text -> BashOutputUi.Ready(result.output) + is io.github.kahme247.ompweb.net.BashOutputResult.TooLarge -> + BashOutputUi.Error("Output is too large to preview (${formatBytes(result.size)}).") + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + BashOutputUi.Error(error.userMessage()) + } + _ui.update { it.copy(bashOutputs = it.bashOutputs + (path to next)) } + } + } + + /** The raw bytes of a truncated bash output, for SAF download. */ + suspend fun bashOutputBytes(path: String): ByteArray? { + val sid = sessionId ?: return null + return try { + api.bashOutputBytes(sid, path) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + _events.tryEmit(ChatEvent.Toast(error.userMessage())) + null + } + } + + /** A transcript link (`agent://` or a dotted child id) opens its dialog. */ + fun openSubagent(handle: String) { + if (sessionId == null) return + // A handle the transcript route would reject never reaches the dialog. + val ids = io.github.kahme247.ompweb.ui.render.agentLinkIds(handle) + .ifEmpty { listOf(handle.removePrefix("agent://")) } + .filter { isValidSubagentId(it) } + if (ids.isEmpty()) return + val roster = engine.state.value.subagents + val known = ids.firstNotNullOfOrNull { id -> roster.firstOrNull { it.id == id } } + // Unknown handles get a synthetic history entry (web `agentLinkTarget`), + // so the completion fetch decides what the dialog can show. + val target = known ?: SubagentInfo( + id = ids.last(), + agent = ids.last(), + status = io.github.kahme247.ompweb.domain.SubagentStatus.COMPLETED, + index = 0, + source = SubagentSource.HISTORY, + ) + openSubagentDialog(target) + } + + /** A roster chip opens the dialog with its live data. */ + fun openSubagent(info: SubagentInfo) { + if (sessionId == null) return + openSubagentDialog(info) + } + + fun closeSubagent() { + subagentWatchJob?.cancel() + subagentWatchJob = null + subagentCompletionJob?.cancel() + subagentTranscriptJob?.cancel() + subagentDetailJob?.cancel() + subagentRequestSeq++ + transcriptRequestSeq++ + _ui.update { it.copy(subagent = null) } + } + + /** Show or hide the raw transcript; opening it fetches the first page. */ + fun toggleSubagentTranscript() { + val current = _ui.value.subagent ?: return + val open = !current.transcript.open + _ui.update { state -> + val dialog = state.subagent ?: return@update state + state.copy(subagent = dialog.copy(transcript = dialog.transcript.copy(open = open))) + } + if (open && current.transcript.messages.isEmpty() && !current.transcript.loading) { + loadSubagentTranscriptPage(0L) + } + } + + fun loadMoreSubagentTranscript() { + val current = _ui.value.subagent ?: return + if (current.transcript.loading || current.transcript.exhausted) return + loadSubagentTranscriptPage(current.transcript.nextByte) + } + + private fun openSubagentDialog(info: SubagentInfo) { + // Roster chips and synthetic handles share this chokepoint: an id the + // transcript route rejects must not open a dialog that can only 400. + if (!isValidSubagentId(info.id)) return + // Bumped sequences invalidate every in-flight fetch of the previous dialog. + subagentRequestSeq++ + transcriptRequestSeq++ + subagentWatchJob?.cancel() + subagentCompletionJob?.cancel() + subagentTranscriptJob?.cancel() + subagentDetailJob?.cancel() + // Per-subagent version counters must start at zero (web resets them on + // open): a value carried over would skip the next refetch. + refetchedSubagentVersion = 0L + refetchedTranscriptVersion = 0L + latestSubagentVersion = 0L + _ui.update { it.copy(subagent = initialDialogState(info)) } + watchSubagentVersions(info.id) + loadSubagentCompletion(info.id) + } + + private fun initialDialogState(info: SubagentInfo): SubagentDialogState { + val progress = info.progress + val history = info.source == SubagentSource.HISTORY + return SubagentDialogState( + id = info.id, + loading = true, + agent = info.agent, + description = info.description, + task = info.task ?: info.assignment, + sessionFile = info.sessionFile, + resolvedModel = progress?.resolvedModel, + tokens = progress?.tokens, + cost = progress?.cost, + durationMs = progress?.durationMs, + live = !history, + activity = subagentActivity(info.id), + outcomeError = if (history) info.result?.abortReason ?: info.result?.error else null, + ) + } + + /** Roster updates (progress/lifecycle) refresh the open dialog's header. */ + private fun syncDialog(current: SubagentDialogState, info: SubagentInfo): SubagentDialogState { + val progress = info.progress + val history = info.source == SubagentSource.HISTORY + return current.copy( + agent = info.agent, + description = info.description ?: current.description, + task = info.task ?: info.assignment ?: current.task, + sessionFile = info.sessionFile ?: current.sessionFile, + resolvedModel = progress?.resolvedModel ?: current.resolvedModel, + tokens = progress?.tokens ?: current.tokens, + cost = progress?.cost ?: current.cost, + durationMs = progress?.durationMs ?: current.durationMs, + live = !history, + activity = subagentActivity(info.id), + outcomeError = if (history) info.result?.abortReason ?: info.result?.error else current.outcomeError, + ) + } + + private fun subagentActivity(id: String): List = + engine.state.value.subagentActivity[id].orEmpty().takeLast(4) + + /** Fetch the settled `.md` output; live snapshots only enrich the header. */ + private fun loadSubagentCompletion(id: String) { + val sid = sessionId ?: return + val seq = ++subagentRequestSeq + subagentCompletionJob?.cancel() + _ui.update { state -> + val dialog = state.subagent + if (dialog != null && dialog.id == id) { + state.copy(subagent = dialog.copy(loading = true, error = null)) + } else { + state + } + } + subagentCompletionJob = viewModelScope.launch { + try { + var completion: String? = null + var truncated = false + try { + val found = api.subagentCompletion(sid, id) + completion = found.completion + truncated = found.truncated + } catch (error: ApiError.ServerResponse) { + // 404: no settled output (or no session file yet). + if (error.status != 404) throw error + } + // Live snapshots enrich the header (resolved model etc.) but + // carry no settled output; the disk `.md` is the source. + if (completion == null && _ui.value.subagent?.live == true) { + val snapshot = fetchSubagentSnapshot(sid, id) + if (seq != subagentRequestSeq) return@launch + snapshot?.let { applySubagentDetail(it) } + } + if (seq != subagentRequestSeq) return@launch + _ui.update { state -> + val dialog = state.subagent + if (dialog == null || dialog.id != id) return@update state + state.copy( + subagent = dialog.copy( + loading = false, + completion = completion, + truncated = truncated, + ), + ) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (seq != subagentRequestSeq) return@launch + // A failed refetch re-arms the version so the next bump retries. + refetchedSubagentVersion = 0L + _ui.update { state -> + val dialog = state.subagent + if (dialog == null || dialog.id != id) return@update state + state.copy(subagent = dialog.copy(loading = false, error = error.userMessage())) + } + } + } + } + + /** `get_subagents` lookup for the dialog's live header enrichment. */ + private suspend fun fetchSubagentSnapshot(sid: String, id: String): SubagentInfo? { + val root = api.subagents(sid) as? JsonObject ?: return null + val snapshots = (root["subagents"] as? kotlinx.serialization.json.JsonArray).orEmpty() + return snapshots + .mapNotNull { parseSubagentSnapshot(it, System.currentTimeMillis()) } + .firstOrNull { it.id == id } + } + + private fun applySubagentDetail(detail: SubagentInfo) { + _ui.update { state -> + val dialog = state.subagent + if (dialog == null || dialog.id != detail.id) return@update state + state.copy( + subagent = dialog.copy( + agent = detail.agent, + description = detail.description ?: dialog.description, + task = detail.task ?: detail.assignment ?: dialog.task, + sessionFile = detail.sessionFile ?: dialog.sessionFile, + resolvedModel = detail.progress?.resolvedModel ?: dialog.resolvedModel, + tokens = detail.progress?.tokens ?: dialog.tokens, + cost = detail.progress?.cost ?: dialog.cost, + durationMs = detail.progress?.durationMs ?: dialog.durationMs, + ), + ) + } + } + + /** + * One page of the raw child transcript: RPC registry first, disk reader as + * the fallback for a live entry whose wrapper restarted (web rule). + */ + private fun loadSubagentTranscriptPage(startByte: Long) { + val sid = sessionId ?: return + val dialog = _ui.value.subagent ?: return + val id = dialog.id + if (!isValidSubagentId(id)) return + val seq = ++transcriptRequestSeq + subagentTranscriptJob?.cancel() + _ui.update { state -> + val current = state.subagent + if (current != null && current.id == id) { + state.copy(subagent = current.copy(transcript = current.transcript.copy(loading = true, error = null))) + } else { + state + } + } + subagentTranscriptJob = viewModelScope.launch { + try { + val fromDisk = !dialog.live + val page = try { + if (fromDisk) { + api.subagentTranscriptDisk(sid, id, startByte) + } else { + api.subagentTranscript(sid, id, dialog.sessionFile, startByte) + } + } catch (error: Exception) { + if (error is CancellationException) throw error + // The RPC registry only knows children of the current + // process; a live entry from a restarted session falls + // back to the disk reader. + if (fromDisk) throw error + api.subagentTranscriptDisk(sid, id, startByte) + } + if (seq != transcriptRequestSeq) return@launch + val parsed = parseSubagentTranscriptPage(page) + ?: throw ChatHistoryException("Invalid transcript page") + _ui.update { state -> + val current = state.subagent + if (current == null || current.id != id) return@update state + val transcript = current.transcript + val messages = if (parsed.reset) parsed.messages else transcript.messages + parsed.messages + state.copy( + subagent = current.copy( + transcript = transcript.copy( + messages = messages, + nextByte = parsed.nextByte, + loading = false, + error = null, + exhausted = parsed.exhausted || parsed.nextByte <= parsed.fromByte, + ), + ), + ) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + if (seq != transcriptRequestSeq) return@launch + _ui.update { state -> + val current = state.subagent + if (current == null || current.id != id) return@update state + state.copy( + subagent = current.copy( + transcript = current.transcript.copy(loading = false, error = error.userMessage()), + ), + ) + } + } + } + } + + /** + * Live child frames bump a per-subagent version; the open dialog debounces + * (600 ms) a completion refetch and, while the transcript is open, the next + * page. The latest version is never dropped: a bump mid-flight re-arms. + */ + private fun watchSubagentVersions(id: String) { + subagentWatchJob?.cancel() + subagentWatchJob = viewModelScope.launch { + engine.state + .map { it.subagentVersions[id] ?: 0L } + .distinctUntilChanged() + .collectLatest { version -> + if (version == 0L) return@collectLatest + if ( + version == refetchedSubagentVersion && + version == latestSubagentVersion && + version == refetchedTranscriptVersion + ) { + return@collectLatest + } + latestSubagentVersion = version + // A newer frame re-arms the timer instead of stacking fetches. + delay(SUBAGENT_REFETCH_DEBOUNCE_MILLIS) + if (version != refetchedSubagentVersion) { + refetchedSubagentVersion = version + loadSubagentCompletion(id) + } + val current = _ui.value.subagent + if ( + current?.id == id && + current.transcript.open && + !current.transcript.loading && + refetchedTranscriptVersion != latestSubagentVersion + ) { + refetchedTranscriptVersion = latestSubagentVersion + loadSubagentTranscriptPage(current.transcript.nextByte) + } + } + } + } + + + // --------------------------------------------------------- subagent roster + + /** + * Hydrate the LIVE roster from `get_subagents` (web `refreshSubagentRoster`). + * + * Fenced after the await by session, run and roster generation: a snapshot + * that resolves after a session switch, a new prompt or a roster clear + * belongs to a different roster and must neither merge nor prune. + */ + private fun refreshSubagentRoster(sid: String) { + val requestedAt = System.currentTimeMillis() + val runId = engine.state.value.runId + val generation = engine.subagentGeneration() + viewModelScope.launch { + try { + val root = api.subagents(sid) as? JsonObject ?: return@launch + if ( + sessionId != sid || + engine.state.value.runId != runId || + engine.subagentGeneration() != generation + ) { + return@launch + } + val snapshots = (root["subagents"] as? kotlinx.serialization.json.JsonArray) + .orEmpty() + .mapNotNull { parseSubagentSnapshot(it, requestedAt) } + // A point-in-time view never overwrites entries live frames + // updated after the request was made. + engine.mergeSubagents(snapshots, skipNewerThan = requestedAt) + // The registry deletes a child before `get_subagents` returns + // once its lifecycle is terminal; a live entry missing from the + // snapshot lost a terminal frame. Entries newer than the request + // survive the prune. + engine.pruneLiveSubagents(snapshots.mapTo(HashSet()) { it.id }, requestedAt) + // Mid-run disk history can hold children that finished before + // the subscription attached. + refreshSubagentHistory(sid) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // Best effort: lifecycle/progress frames are the primary source. + } + } + } + + /** + * Recover the ON-DISK roster (the parent's `task` toolResults); survives + * reloads and shows finished runs from earlier visits (web + * `refreshSubagentHistory`). + */ + private fun refreshSubagentHistory(sid: String) { + val generation = engine.subagentGeneration() + viewModelScope.launch { + try { + // A 404 means "no on-disk history yet". + val root = api.subagentHistory(sid) as? JsonObject ?: return@launch + if (sessionId != sid || engine.subagentGeneration() != generation) return@launch + val entries = (root["subagents"] as? kotlinx.serialization.json.JsonArray) + .orEmpty() + .mapNotNull { parseSubagentHistoryEntry(it) } + engine.mergeSubagents(entries) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // Best effort; live frames win while a run is active. + } + } + } + + // ----------------------------------------------------------------- speech + + fun toggleSpeech(entryId: String?, key: String, text: String) { + speech.toggle(entryId ?: key, text) + } + + fun stopSpeech() { + speech.stop() + } + + /** Bind the TTS service once the transcript is on screen. */ + fun prepareSpeech() { + speech.prepare() + } + + private fun autoplaySpeech() { + if (!preferences.ttsAutoplay) return + val messages = engine.state.value.messages + val latest = messages.asReversed().firstOrNull { message -> + message is ChatMessage.Assistant && message.streaming.not() + } as? ChatMessage.Assistant ?: return + val text = latest.blocks.filterIsInstance() + .joinToString("\n\n") { it.text } + if (text.isBlank()) return + speech.speak(latest.entryId ?: latest.key, text) + } + + // --------------------------------------------------------------- transport + + private fun startSubscription(sid: String) { + subscriptionJob?.cancel() + subscriptionJob = viewModelScope.launch { + var retryMillis = 0L + while (isActive) { + if (retryMillis > 0) delay(retryMillis) + try { + eventMux.subscribe(EventChannel.Session(sid)).collect { frame -> + if (sessionId != sid) return@collect + // Live again: a later drop starts a fresh backoff. + retryMillis = SUBSCRIBE_RETRY_MIN_MILLIS + // Slice C consumes its own request/bridge frames so the + // transcript never sees them; side-question frames are + // consumed inside the engine ahead of `observe`. + if (hostTools.onFrame(sid, frame)) return@collect + if (extensionUi.onFrame(frame)) return@collect + controls.onFrame(frame) + engine.onFrame(frame) + // The engine's coalescer owns the flush (16 ms), matching + // the web's message-update coalescer; flushing per frame + // defeated it. + pumpEffects() + } + retryMillis = SUBSCRIBE_RETRY_MIN_MILLIS + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // An idle session has no server-side wrapper, so the channel + // is refused; the web retries silently with capped backoff + // instead of surfacing an error. The next prompt restarts + // this loop immediately (see ensureSubscribed). + retryMillis = (retryMillis * 2).coerceIn( + SUBSCRIBE_RETRY_MIN_MILLIS, + SUBSCRIBE_RETRY_MAX_MILLIS, + ) + } + if (sessionId != sid) return@launch + } + } + } + + private fun ensureSubscribed(sid: String) { + // The prompt creates the server-side wrapper; a subscription stuck in + // the idle-session backoff must not delay the live stream. + startSubscription(sid) + } + + private fun pumpEffects() { + if (pumpingEffects) return + pumpingEffects = true + try { + while (true) { + val effects = engine.drainEffects() + if (effects.isEmpty()) break + for (effect in effects) { + when (effect) { + is ChatEffect.RequestContext -> requestHistory(effect.reset) + ChatEffect.RequestAgentState -> reconcileNow() + ChatEffect.SpeakLastReply -> autoplaySpeech() + ChatEffect.PlayCompletionTone -> { + if (settings.snapshot.value.soundEnabled) completionSound.play() + } + is ChatEffect.ShowCompactResult -> showCompactResult(effect.result) + is ChatEffect.ShowCompactError -> showCompactError(effect.message) + // Run end: the registry is empty, the file holds this run's + // children; re-read both so the roster survives a missed frame. + ChatEffect.RefreshSubagents -> sessionId?.let { sid -> + refreshSubagentRoster(sid) + refreshSubagentHistory(sid) + } + // (Re)connected: register the bridges and re-read the side + // questions + roster the stream may have missed. + ChatEffect.StreamOpened -> sessionId?.let { sid -> + btw.onStreamOpen() + hostTools.register(sid) + refreshSubagentRoster(sid) + refreshSubagentHistory(sid) + } + // The conversation continued in a copy: keep the + // original reachable from the chat the user is in. + is ChatEffect.ContentionFork -> _ui.update { + it.copy(contentionCopy = ContentionCopy(effect.fromId, effect.toId)) + } + } + } + } + } finally { + pumpingEffects = false + } + } + + private fun scheduleSettlement(runId: Long) { + settleJob?.cancel() + settleJob = viewModelScope.launch { + delay(PROMPT_SETTLE_FIRST_MILLIS) + repeat(PROMPT_SETTLE_ATTEMPTS) { + if (engine.state.value.runId != runId) return@launch + // Not a poll: this one-shot look after a send must not advance + // the bounded "unknown" window, or "three consecutive polls" + // would already be reached by the settlement alone. The + // periodic reconcile loop is the only counter (web + // `waitForPromptSettlement` does not count either). + if (reconcileOnce(countSlowAnswer = false)) return@launch + delay(PROMPT_SETTLE_INTERVAL_MILLIS) + } + } + } + + /** + * Asks for the live state once. [countSlowAnswer] marks a **poll**: an + * answer without a run state then advances the unknown window. A read that + * merely follows an open/refresh is not a poll — opening a chat fires + * several of them at the same instant (the open itself, the connection + * becoming live, the first history page landing), and counting each would + * exhaust the window before the first timer tick. The window must measure + * the polling cadence, not how many triggers happened to fire. + */ + private fun reconcileNow(countSlowAnswer: Boolean = false) { + viewModelScope.launch { reconcileOnce(countSlowAnswer) } + } + + /** What a `GET /api/agent/{id}` says about the session. */ + private sealed interface LiveRead { + /** omp answered: a turn (or a compacting run) is live. */ + data object Running : LiveRead + + /** omp answered: nothing is running. */ + data object Idle : LiveRead + + /** No session, a read is already on the wire, or its answer is stale. */ + data object Skipped : LiveRead + + /** + * omp answered 503 `session_slow`: alive but blocked (a large session), + * so a run is under way with an unknown state — never an ended one. The + * engine keeps showing it as running and the next poll settles it. + */ + data object Slow : LiveRead + + /** The read did not answer; the live state stays unknown. */ + data class Failed(val error: Exception) : LiveRead + } + + /** + * Returns true once the agent owns the run (stream/state took over). A slow + * session counts: it is still running, so the settlement attempts must stop + * hammering a blocked child; the periodic poll keeps watching it. + */ + private suspend fun reconcileOnce(countSlowAnswer: Boolean = false): Boolean = + when (readLiveState(countSlowAnswer)) { + LiveRead.Running, LiveRead.Slow -> true + else -> false + } + + /** `GET /api/agent/{id}` applied to the engine, with the run fences. */ + private suspend fun readLiveState(countSlowAnswer: Boolean = false): LiveRead { + val sid = sessionId ?: return LiveRead.Skipped + if (!reconcileInFlight.add(sid)) { + // A GET for this session is already on its way and its answer is + // the same state; another one would only race it. + return LiveRead.Skipped + } + // A snapshot requested before a new prompt must never end that + // prompt's run (the GET can outlive the send), and one requested + // before the current run ended must not resurrect it: omp freezes + // `isPromptRunning`/`isStreaming` in the answer, and a same-run + // snapshot would otherwise show a phantom run for a whole cycle. The + // fences also cover the busy answer below. + val runId = engine.state.value.runId + val runsEnded = engine.state.value.runsEnded + try { + val queueRevision = engine.state.value.queueRevision + val root = api.agentState(sid) + if (sessionId != sid || engine.state.value.runId != runId) return LiveRead.Skipped + if (engine.state.value.runsEnded != runsEnded) return LiveRead.Skipped + clearSlowStateAnswers() + // Keep an open system-prompt panel in sync with the same GET, so a + // prompt change never shows stale text. + refreshOpenSystemPrompt(root) + controls.onAgentState(root) + engine.applyAgentState(root, expectedRunId = runId, expectedQueueRevision = queueRevision) + // This poll is the only reliable re-sync for a missed lifecycle + // frame, and the only side-question refresh while streaming. + btw.onReconcile() + refreshSubagentRoster(sid) + pumpEffects() + val state = engine.state.value + return if (state.busy || state.streamingActive) LiveRead.Running else LiveRead.Idle + } catch (cancelled: CancellationException) { + throw cancelled + } catch (slow: ApiError.SessionSlow) { + // omp is busy (large sessions block its event loop for tens of + // seconds), not gone. A frame that ended the run meanwhile wins + // (same fences as the snapshot above); a submit that is waiting for + // the state then routes as during a run instead of guessing "idle". + if (sessionId == sid && engine.state.value.runId == runId + && engine.state.value.runsEnded == runsEnded + ) { + when (slow.running) { + // Authoritative: no turn is in flight (the child is only + // blocked loading), so the run the screen still shows is + // over. Settling now is what stops "working" from + // outliving the run by hours. + false -> { + clearSlowStateAnswers() + engine.markLiveStateIdle() + return LiveRead.Idle + } + // Unreported (older server): bound the unknown window and + // let the running list — the server's own registry — decide. + // Only a poll advances the window: the burst of reads an + // open fires (open, connection, first page) is one + // observation, not three polls. + null -> { + engine.markLiveStateSlow(null) + if (countSlowAnswer && noteSlowStateAnswer(sid, runId)) { + // The window restarts before the registry decides + // (web clears `slowStateAnswersRef` first): a list + // that still names the run makes the next window + // three fresh polls, not one more every poll. + clearSlowStateAnswers() + if (!runningListNamesSession(sid)) { + engine.markLiveStateIdle() + return LiveRead.Idle + } + } + } + true -> engine.markLiveStateSlow(true) + } + } + return LiveRead.Slow + } catch (error: Exception) { + return LiveRead.Failed(error) + } finally { + reconcileInFlight -= sid + } + } + + /** Counts a 503 without a run state for this run; true once the bound is exceeded. */ + private fun noteSlowStateAnswer(sid: String, runId: Long): Boolean { + if (slowStateSession != sid || slowStateRunId != runId) { + slowStateSession = sid + slowStateRunId = runId + slowStateAnswers = 1 + return false + } + slowStateAnswers += 1 + return slowStateAnswers >= SLOW_STATE_MAX_ANSWERS + } + + private fun clearSlowStateAnswers() { + slowStateSession = null + slowStateRunId = -1L + slowStateAnswers = 0 + } + + /** + * Last resort when a 503 does not say whether a run exists: the running + * list is the server's own run registry. An unreadable list answers `true` + * (keep the run marked) — a failed lookup is never evidence of idleness. + */ + private suspend fun runningListNamesSession(sid: String): Boolean = try { + sid in api.runningSessionIds() + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + true + } + + // ---------------------------------------------------------------- render + + private fun publish(state: ChatState = engine.state.value) { + // An idle session has no omp child to answer `get_state`; the file's + // model/reasoning still label the controls until a live value arrives. + val modelKey = state.model?.let { "${it.provider}/${it.id}/${it.name}" } + if (modelKey != null && modelKey != lastSessionModelKey) { + lastSessionModelKey = modelKey + state.model?.let { controls.onSessionModel(it.provider, it.id, it.name) } + } + if (state.thinkingLevel != lastSessionThinking) { + lastSessionThinking = state.thinkingLevel + controls.onSessionThinking(state.thinkingLevel) + } + if (rowsCacheSource !== state.messages || rowsCacheHideThinking != preferences.hideThinking) { + rowsCacheSource = state.messages + rowsCacheHideThinking = preferences.hideThinking + rowsCache = TranscriptPlan.build(state.messages, preferences.hideThinking) + committedToolResults = TranscriptPlan.collectToolResults(state.messages) + forkCache = ForkTargets.resolve( + state.messages, + state.entryIds.map { it.ifBlank { null } }, + ) + } + val results = if (state.liveTools.isEmpty()) { + committedToolResults + } else { + val merged = committedToolResults.toMutableMap() + for (tool in state.liveTools) { + tool.result?.let { merged[it.toolCallId] = it } + } + merged + } + val notices = state.notices.filterNot { noticeKey(it) in dismissedNotices } + if (state.notices.isEmpty() && dismissedNotices.isNotEmpty()) dismissedNotices.clear() + val hasCompaction = state.messages.any { + it is ChatMessage.Custom && it.customType == "compaction" + } + val restore = _ui.value.restoreScrollIndex + // A compaction start replaces any banner the previous one left behind + // (web clears both on `auto_compaction_start`). + if (state.compacting && (_ui.value.compactResult != null || _ui.value.compactError != null)) { + compactResultJob?.cancel() + compactErrorJob?.cancel() + _ui.update { it.copy(compactResult = null, compactError = null) } + } + // A `/plan` banner belongs to the turn that is running right now. + val planObjective = _ui.value.planObjective?.takeIf { state.agentRunning } + val dialog = _ui.value.subagent?.let { current -> + state.subagents.firstOrNull { it.id == current.id }?.let { info -> syncDialog(current, info) } ?: current + } + _ui.update { + it.copy( + engine = state, + rows = rowsCache, + toolResults = results, + forkTargets = forkCache, + preferences = preferences, + hasCompaction = hasCompaction, + thinking = LinkedHashMap(thinkingCache), + restoreScrollIndex = restore, + // Compaction banners only matter while history is shown. + notices = notices, + activeGoal = sessionId?.let { settings.snapshot.value.activeGoals[it] }, + submitDuringRun = settings.snapshot.value.submitDuringRun, + planObjective = planObjective, + todoCollapsed = settings.snapshot.value.composerTodoCollapsed, + subagentsCollapsed = settings.snapshot.value.composerSubagentsCollapsed, + subagent = dialog, + ) + } + } + + /** Collapse/expand the attached todo panel (`omp-web:composer-todo-collapsed`). */ + fun setTodoCollapsed(collapsed: Boolean) { + _ui.update { it.copy(todoCollapsed = collapsed) } + viewModelScope.launch { settings.setComposerTodoCollapsed(collapsed) } + } + + /** Collapse/expand the attached subagent roster. */ + fun setSubagentsCollapsed(collapsed: Boolean) { + _ui.update { it.copy(subagentsCollapsed = collapsed) } + viewModelScope.launch { settings.setComposerSubagentsCollapsed(collapsed) } + } + + // ------------------------------------------------------------ chat search + + /** The top bar's search icon: opens the field, or closes it if it is open. */ + fun toggleSearch() { + if (_search.value.open) closeSearch() else openSearch() + } + + fun openSearch() { + if (_search.value.open) return + _search.update { it.copy(open = true) } + refreshSearchHits() + } + + /** Closing the field drops the query and its marks, like the web's `Escape`. */ + fun closeSearch() { + if (_search.value == ChatSearchState()) return + _search.update { ChatSearchState() } + } + + fun setSearchQuery(query: String) { + _search.update { it.copy(query = query, activeIndex = 0) } + refreshSearchHits() + } + + /** ↑/↓ through the hits; wraps at both ends. */ + fun stepSearch(delta: Int) { + _search.update { state -> + state.copy(activeIndex = ChatSearch.step(state.activeIndex, state.hits.size, delta)) + } + } + + /** + * Re-runs the query against the loaded transcript. The screen calls this + * whenever rows or messages change (a streamed reply, a reloaded history), + * so the marks and the count never describe an older transcript. + */ + fun refreshSearchHits() { + val state = _search.value + if (!state.open || state.query.isBlank()) { + if (state.hits.isNotEmpty() || state.activeIndex != 0) { + _search.update { it.copy(hits = emptyList(), activeIndex = 0) } + } + return + } + val ui = _ui.value + val hits = ChatSearch.search( + messages = ui.engine.messages, + rowKeyByMessage = ChatSearch.rowKeyByMessage(ui.rows, ui.engine.messages), + query = state.query, + // The transcript drops thinking blocks while the reader hides + // them; searching the raw text would report hits that no row shows. + hideThinking = ui.preferences.hideThinking, + ) + _search.update { current -> + current.copy( + hits = hits, + activeIndex = current.activeIndex.coerceIn(0, (hits.size - 1).coerceAtLeast(0)), + ) + } + } + + /** + * Scroll the transcript to the message [entryId] of [sessionId] and flash + * it. The hand-off point for the server-side search in `ui/sessions`: the + * caller needs no knowledge of rows, windows or lazy indices, and does not + * clear anything afterwards (the transcript consumes the request after the + * jump). + * + * A hit in another session is queued for that session: the session switch + * runs after this call and resets the in-chat search state, so the request + * would otherwise be dropped. A request made before the rows are loaded is + * kept and applied as soon as the row exists; an entry that is not part of + * the loaded display history (e.g. pre-compaction with the compaction view + * off) is reported by the transcript instead of silently ignored. + */ + fun scrollToEntry(sessionId: String, entryId: String) { + if (entryId.isEmpty()) return + if (this.sessionId == sessionId) { + _search.update { it.copy(revealEntryId = entryId) } + } else { + pendingReveals[sessionId] = entryId + } + } + + /** The transcript landed on [ChatSearchState.revealEntryId]. */ + fun consumeRevealEntry() { + if (_search.value.revealEntryId != null) _search.update { it.copy(revealEntryId = null) } + } + + private fun persistScrollNow() { + val sid = sessionId ?: return + val next = if (lastNearBottom) 0 else currentScrollIndex + savedStateHandle[KEY_SCROLL] = next + savedStateHandle[KEY_LEAF] = viewLeafId + if (next == lastPersistedScroll) return + // onCleared cannot launch a view-model coroutine; the settings store + // owns the dispatch (snapshot updates synchronously, DataStore writes + // on its own scope). + scrollSaveJob?.cancel() + settings.persistChatScrollPosition(sid, next) + lastPersistedScroll = next + } + + override fun onCleared() { + persistScrollNow() + // Slice C's dialog is bound to this chat; a cleared chat drops it and + // must also stop treating the old session as "viewed" (consent rule). + onChatHidden() + // The panel controller is app-scoped, this chat is not. A panel that + // arrived while hidden must not outlive its chat: once the surface is + // gone for good, no subscription of this client is left, the id may + // have been settled by another tab, and an answer would vanish + // fire-and-forget. Nothing is lost by dropping it — the next + // subscription to that session replays whatever is still open. + extensionUi.reset() + speech.shutdown() + super.onCleared() + } + + private companion object { + const val FRAME_COALESCE_MILLIS = 16L + const val RECONCILE_MILLIS = 15_000L + /** + * Another omp process owns the chat: its transcript is followed by + * polling (web `EXTERNAL_RUN_POLL_MS`, 2 s) for as long as the owner + * reports activity. + */ + const val FOREIGN_RUN_POLL_MILLIS = 2_000L + /** First retry of a live state that never arrived; doubles to the max. */ + const val RECONCILE_UNKNOWN_FIRST_MILLIS = 3_000L + const val RECONCILE_UNKNOWN_MAX_MILLIS = 60_000L + /** + * Consecutive 503 `session_slow` answers that carry no run state (older + * server) before the running list decides: an unknown state must not + * keep the screen in "working" forever (web `SLOW_STATE_MAX_ANSWERS`). + */ + const val SLOW_STATE_MAX_ANSWERS = 3 + /** A submit waits at most this long for the live state before keeping the draft. */ + const val LIVE_STATE_SUBMIT_TIMEOUT_MILLIS = 10_000L + /** Retry of a failed submit-time live-state read; doubles to the max. */ + const val LIVE_STATE_SUBMIT_FIRST_MILLIS = 500L + const val LIVE_STATE_SUBMIT_MAX_RETRY_MILLIS = 4_000L + /** Wait for a read that is already on the wire instead of stacking GETs. */ + const val LIVE_STATE_SUBMIT_POLL_MILLIS = 150L + const val PROMPT_SETTLE_FIRST_MILLIS = 800L + const val PROMPT_SETTLE_INTERVAL_MILLIS = 600L + const val PROMPT_SETTLE_ATTEMPTS = 32 + const val ABORT_RECONCILE_MILLIS = 800L + const val WITHDRAW_BEFORE_ABORT_MILLIS = 1_500L + const val SUBSCRIBE_RETRY_MIN_MILLIS = 1_000L + const val SUBSCRIBE_RETRY_MAX_MILLIS = 30_000L + const val SCROLL_SAVE_DEBOUNCE_MILLIS = 600L + const val THINKING_CACHE_LIMIT = 100 + const val MAX_PAGES = 200 + /** Web `compactResult` / `compactError` visibility. */ + const val COMPACT_RESULT_VISIBLE_MILLIS = 6_000L + const val COMPACT_ERROR_VISIBLE_MILLIS = 3_000L + /** Streaming child frames coalesce into one dialog refetch (web 600 ms). */ + const val SUBAGENT_REFETCH_DEBOUNCE_MILLIS = 600L + + /** `/btw` with a question or alone (web `sendSideQuestion`). */ + val SIDE_QUESTION_REGEX = Regex("^/btw(\\s|$)") + + const val KEY_SESSION = "chat_session_id" + const val KEY_LEAF = "chat_leaf_id" + const val KEY_SCROLL = "chat_scroll_index" + + fun thinkingKey(entryId: String, blockIndex: Int): String = "$entryId:$blockIndex" + + fun noticeKey(notice: ChatNotice): String = + "${notice.kind}:${notice.createdAt}:${notice.message}" + } +} + +private class ChatHistoryException(message: String) : Exception(message) + +private fun formatBytes(bytes: Long): String = when { + bytes >= 1_048_576 -> String.format(java.util.Locale.US, "%.1f MB", bytes / 1_048_576.0) + bytes >= 1024 -> String.format(java.util.Locale.US, "%.1f KB", bytes / 1024.0) + else -> "$bytes B" +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/Compaction.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/Compaction.kt new file mode 100644 index 000000000..aaddd9503 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/Compaction.kt @@ -0,0 +1,30 @@ +package io.github.kahme247.ompweb.ui.chat + +/** Compaction summary body plus the file context omp appends (web `lib/compaction-summary.ts`). */ +data class ParsedCompactionSummary( + val body: String, + val readFiles: List, + val modifiedFiles: List, +) + +private val FILE_SECTION_RE = Regex( + """<(read-files|modified-files)>\s*([\s\S]*?)\s*""", +) +private val TRAILING_FILE_SECTIONS_RE = Regex( + """(?:\r?\n){2,}((?:[ \t]*<(?:read-files|modified-files)>[ \t]*\r?\n[\s\S]*?\r?\n[ \t]*[ \t]*(?:\r?\n)?)+)\s*$""", +) + +fun parseCompactionSummary(summary: String): ParsedCompactionSummary { + val readFiles = ArrayList() + val modifiedFiles = ArrayList() + val metadataMatch = TRAILING_FILE_SECTIONS_RE.find(summary) + val metadataBlock = metadataMatch?.groups?.get(1)?.value.orEmpty() + val body = if (metadataMatch == null) summary else summary.substring(0, metadataMatch.range.first) + + for (match in FILE_SECTION_RE.findAll(metadataBlock)) { + val section = match.groups[1]!!.value + val files = match.groups[2]!!.value.lineSequence().map { it.trim() }.filter { it.isNotEmpty() } + if (section == "read-files") readFiles += files else modifiedFiles += files + } + return ParsedCompactionSummary(body.trim(), readFiles, modifiedFiles) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/LiveFold.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/LiveFold.kt new file mode 100644 index 000000000..c9c4aea96 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/LiveFold.kt @@ -0,0 +1,172 @@ +package io.github.kahme247.ompweb.ui.chat + +import android.content.Context +import android.database.ContentObserver +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.platform.LocalContext +import io.github.kahme247.ompweb.chat.AgentPhase +import io.github.kahme247.ompweb.chat.AssistantBlock +import io.github.kahme247.ompweb.chat.ChatMessage +import io.github.kahme247.ompweb.chat.ChatState + +/** + * What the running turn is doing right now. With "hide tool activity" the + * running turn folds into one collapsed "Process details" row; the collapsed + * row still has to show that work is in progress (web `ChatWindow`'s + * `pendingToolHeaders` and phase line, which stay above the composer). + */ +sealed interface LiveActivity { + /** [name] plus its short argument/intent; [more] counts the other running calls. */ + data class Tool(val name: String, val intent: String, val more: Int) : LiveActivity + + /** The model is working and has not written anything yet. */ + data object Thinking : LiveActivity + + /** The model is writing its answer. */ + data object Writing : LiveActivity + + data object RunningCommand : LiveActivity + + data object Compacting : LiveActivity + + /** Busy without a known phase (an older omp, or between frames). */ + data object Working : LiveActivity +} + +/** + * The running turn's live activity, or null when nothing runs (the fold then + * returns to its static summary). + * + * A tool is named from the transcript itself: an assistant tool call that has + * no result yet is what the turn waits on. The server does not always stream + * the tool phase frames (real runs show `waiting for model` during a 60 s + * `sleep`), so the transcript is the reliable signal; the phase is the + * fallback for servers that do stream them. The argument preview is the same + * one the collapsed card header uses. + * + * "Writing…" is only claimed once the streamed message actually has text. + */ +fun liveActivity(state: ChatState): LiveActivity? { + if (!state.busy && !state.streamingActive) return null + if (state.compacting) return LiveActivity.Compacting + pendingToolCall(state)?.let { return it } + return when (val phase = state.agentPhase) { + is AgentPhase.RunningTools -> phaseTool(phase, state) ?: LiveActivity.Working + AgentPhase.RunningCommand -> LiveActivity.RunningCommand + AgentPhase.WaitingModel, null -> if (writingNow(state)) LiveActivity.Writing else LiveActivity.Thinking + } +} + +/** + * The model is writing its answer: the streamed message has text, or the + * transcript's newest message is the assistant's answer (the server commits + * the answer as it grows and does not keep a separate streaming message). + * A freshly sent prompt is the newest message until the answer starts, so the + * previous turn's text never reads as "Writing…". + */ +private fun writingNow(state: ChatState): Boolean = + hasText(state.streaming) || hasText(state.messages.lastOrNull() as? ChatMessage.Assistant) + +private fun hasText(message: ChatMessage.Assistant?): Boolean = + message?.blocks?.any { block -> block is AssistantBlock.Text && block.text.isNotBlank() } == true + +/** + * The newest assistant tool call without a result. Only the live message and + * the last committed assistant message are considered, so a call from an + * older turn cannot be mistaken for the current one. + */ +private fun pendingToolCall(state: ChatState): LiveActivity.Tool? { + val resolved = state.messages + .filterIsInstance() + .mapTo(HashSet()) { it.toolCallId } + val candidates = listOfNotNull( + state.streaming, + state.messages.lastOrNull { it is ChatMessage.Assistant } as? ChatMessage.Assistant, + ) + for (message in candidates) { + val open = message.blocks + .filterIsInstance() + .filter { it.toolCallId !in resolved } + if (open.isEmpty()) continue + val call = open.last() + return LiveActivity.Tool( + name = call.toolName.ifBlank { "tool" }, + intent = toolIntent(AssistantBlock.ToolCall(call.toolCallId, call.toolName, call.input)), + more = open.size - 1, + ) + } + return null +} + +private fun phaseTool(phase: AgentPhase.RunningTools, state: ChatState): LiveActivity.Tool? { + val first = phase.tools.firstOrNull() ?: return null + val live = state.liveTools.firstOrNull { it.toolCallId == first.id } + ?: state.liveTools.firstOrNull { it.endedAt == null && it.toolName.equals(first.name, ignoreCase = true) } + return LiveActivity.Tool( + name = first.name.ifBlank { "tool" }, + intent = live?.args?.let { args -> toolIntent(AssistantBlock.ToolCall(first.id, first.name, args)) }.orEmpty(), + more = (phase.tools.size - 1).coerceAtLeast(0), + ) +} + +/** The one-line argument summary of a tool call, or an empty string. */ +private fun toolIntent(call: AssistantBlock.ToolCall): String = + getToolPreview(call).lineSequence().firstOrNull()?.trim().orEmpty() + +/** + * The clock the live fold measures elapsed time with. The transcript's own + * timestamps come from the messages, so only the running row reads a wall + * clock; tests pin it to keep screenshots deterministic. + */ +internal val LocalChatClock = staticCompositionLocalOf<() -> Long> { { System.currentTimeMillis() } } + +/** + * Whether the platform asks for motion (the "remove animations" accessibility + * setting, i.e. animator duration scale 0). The running fold then shows a + * static dot instead of the pulse, exactly like the web's reduced-motion path. + */ +internal val LocalChatMotion = staticCompositionLocalOf { true } + +/** Reads and observes the system animator duration scale. */ +@Composable +internal fun rememberSystemMotionEnabled(): Boolean { + val context = LocalContext.current + var enabled by remember(context) { mutableStateOf(motionEnabled(context)) } + DisposableEffect(context) { + val resolver = context.contentResolver + val observer = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean) { + enabled = motionEnabled(context) + } + } + resolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), + false, + observer, + ) + onDispose { resolver.unregisterContentObserver(observer) } + } + return enabled +} + +private fun motionEnabled(context: Context): Boolean = try { + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, + 1f, + ) > 0f +} catch (_: SecurityException) { + // A restricted profile may hide the setting; motion stays on. + true +} catch (_: IllegalArgumentException) { + true +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/PhaseLabel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/PhaseLabel.kt new file mode 100644 index 000000000..e72c5a2b0 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/PhaseLabel.kt @@ -0,0 +1,48 @@ +package io.github.kahme247.ompweb.ui.chat + +import io.github.kahme247.ompweb.chat.AgentPhase +import io.github.kahme247.ompweb.chat.ChatState + +/** What the composer status line names (web `phaseLabel`). */ +enum class PhaseLabelKind { Compacting, WaitingModel, RunningTools, RunningCommand, Working } + +data class PhaseLabel( + val kind: PhaseLabelKind, + /** Distinct tool names the label lists; empty for every other kind. */ + val tools: List = emptyList(), + /** Names the label folds into "(+N more)" (web `runningNamedMore`). */ + val more: Int = 0, +) + +/** Up to this many names the status line lists them all (web `names.length <= 3`). */ +const val PHASE_MAX_TOOLS = 3 + +/** Above [PHASE_MAX_TOOLS] the web shows the first two names plus a counter. */ +private const val PHASE_NAMED_TOOLS = 2 + +/** + * The status line for the current turn. `null` means "nothing runs", so the + * line shows nothing (web `phaseLabel` returning null). + * + * The cap follows `components/ChatWindow.tsx` exactly: up to three tool names + * are listed; beyond that the first two are named and the rest collapse into + * [PhaseLabel.more]. + */ +fun phaseLabel(state: ChatState): PhaseLabel? { + if (state.compacting) return PhaseLabel(PhaseLabelKind.Compacting) + if (!state.busy) return null + val phase = state.agentPhase + if (phase is AgentPhase.RunningTools) { + val names = phase.tools.map { it.name.ifBlank { "tool" } }.distinct() + if (names.isEmpty()) return PhaseLabel(PhaseLabelKind.WaitingModel) + val shown = if (names.size <= PHASE_MAX_TOOLS) names else names.take(PHASE_NAMED_TOOLS) + return PhaseLabel( + kind = PhaseLabelKind.RunningTools, + tools = shown, + more = (names.size - shown.size).coerceAtLeast(0), + ) + } + if (phase is AgentPhase.RunningCommand) return PhaseLabel(PhaseLabelKind.RunningCommand) + if (phase is AgentPhase.WaitingModel) return PhaseLabel(PhaseLabelKind.WaitingModel) + return PhaseLabel(PhaseLabelKind.Working) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SkillDiagnosticsNotice.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SkillDiagnosticsNotice.kt new file mode 100644 index 000000000..cdb21c57d --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SkillDiagnosticsNotice.kt @@ -0,0 +1,355 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.SkillDiagnostic +import io.github.kahme247.ompweb.domain.SkillDiagnosticReason +import io.github.kahme247.ompweb.domain.SkillDiagnosticsSnapshot +import io.github.kahme247.ompweb.domain.SkillEntry +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpDialog + +/** Why the last startup-notice change failed (the notice shows the reason inline). */ +enum class SkillDiagnosticsFailure { Overridden, SaveFailed, Unavailable } + +/** + * Startup notice for conflicting or redundant skills (web + * `SkillDiagnosticsNotice`): one line above the panels, a details dialog, and + * Turn off / Dismiss. Dismissal hides this exact report for the session; a + * changed report shows again. + */ +@Composable +internal fun SkillDiagnosticsNotice( + snapshot: SkillDiagnosticsSnapshot?, + saving: Boolean, + failure: SkillDiagnosticsFailure?, + onDisable: () -> Unit, +) { + if (snapshot == null) return + val colors = LocalOmpColors.current + val reportKey = remember(snapshot) { skillDiagnosticsReportKey(snapshot) } + var dismissedKey by rememberSaveable { mutableStateOf(null) } + var open by remember { mutableStateOf(false) } + val conflicts = snapshot.conflictCount + val duplicates = snapshot.duplicateCount + val visible = snapshot.showStartupDiagnostics && (conflicts > 0 || duplicates > 0) && + dismissedKey != reportKey + if (!visible) return + val summary = buildList { + if (conflicts > 0) { + add( + if (conflicts == 1) { + stringResource(R.string.skill_diag_conflicts_one, conflicts) + } else { + stringResource(R.string.skill_diag_conflicts_other, conflicts) + }, + ) + } + if (duplicates > 0) { + add( + if (duplicates == 1) { + stringResource(R.string.skill_diag_duplicates_one, duplicates) + } else { + stringResource(R.string.skill_diag_duplicates_other, duplicates) + }, + ) + } + }.joinToString(" · ") + val tone = if (conflicts > 0) colors.statusWarning else colors.textMuted + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + // Web `role="status" aria-live="polite"`. + .semantics { liveRegion = LiveRegionMode.Polite }, + color = colors.backgroundSubtle, + contentColor = tone, + shape = RoundedCornerShape(10.dp), + border = BorderStroke(1.dp, colors.border), + ) { + Column(Modifier.padding(start = 12.dp, end = 6.dp, top = 6.dp, bottom = 6.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = if (conflicts > 0) Icons.Filled.Warning else Icons.Filled.Info, + contentDescription = null, + tint = tone, + modifier = Modifier.size(14.dp), + ) + Text( + text = summary, + color = tone, + fontSize = 11.5.sp, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + TextButton(onClick = { open = true }, modifier = Modifier.heightIn(min = 48.dp)) { + Text(stringResource(R.string.skill_diag_details), fontSize = 11.sp) + } + TextButton( + onClick = onDisable, + enabled = !saving, + modifier = Modifier.heightIn(min = 48.dp), + ) { + Text( + text = if (saving) { + stringResource(R.string.skill_diag_saving) + } else { + stringResource(R.string.skill_diag_disable) + }, + fontSize = 11.sp, + ) + } + Spacer(Modifier.weight(1f)) + IconButton(onClick = { dismissedKey = reportKey }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.skill_diag_dismiss), + tint = colors.textMuted, + modifier = Modifier.size(14.dp), + ) + } + } + if (failure != null) { + Text( + text = stringResource( + when (failure) { + SkillDiagnosticsFailure.Overridden -> R.string.skill_diag_overridden + SkillDiagnosticsFailure.SaveFailed -> R.string.skill_diag_save_failed + SkillDiagnosticsFailure.Unavailable -> R.string.skill_diag_unavailable + }, + ), + color = colors.statusError, + fontSize = 11.sp, + ) + } + } + } + if (open) { + OmpDialog(onDismissRequest = { open = false }) { + SkillDiagnosticsBody(snapshot = snapshot, onDismiss = { open = false }) + } + } +} + +/** + * The diagnostics dialog's content without the dialog window, so the hardened + * matrix can render it at 200 % font (dialogs use their own window). + */ +@Composable +internal fun SkillDiagnosticsBody(snapshot: SkillDiagnosticsSnapshot, onDismiss: () -> Unit) { + val colors = LocalOmpColors.current + Surface( + color = colors.panel, + contentColor = colors.text, + shape = RoundedCornerShape(12.dp), + border = BorderStroke(1.dp, colors.border), + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier + .heightIn(max = 520.dp) + .verticalScroll(rememberScrollState()) + .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.skill_diag_title), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .weight(1f) + .semantics { heading() }, + ) + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.skill_diag_close), + tint = colors.textMuted, + modifier = Modifier.size(16.dp), + ) + } + } + Text( + text = displayText(snapshot.cwd), + color = colors.textDim, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + ) + if (snapshot.diagnostics.isEmpty()) { + Text( + text = stringResource(R.string.skill_diag_clean), + color = colors.textMuted, + fontSize = 12.sp, + ) + } else { + snapshot.diagnostics.forEach { diagnostic -> + DiagnosticSection(diagnostic) + } + Text( + text = stringResource(R.string.skill_diag_lineage), + color = colors.textMuted, + fontSize = 11.sp, + ) + } + } + } +} + +@Composable +private fun DiagnosticSection(diagnostic: SkillDiagnostic) { + val colors = LocalOmpColors.current + val selected = diagnostic.skills.firstOrNull { it.name == diagnostic.name } + Surface( + modifier = Modifier.fillMaxWidth(), + color = colors.backgroundSubtle, + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, colors.border), + ) { + Column( + modifier = Modifier.padding(10.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = displayText(diagnostic.name), + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + ) + if (selected != null) { + DiagnosticEntry(stringResource(R.string.skill_diag_default), selected) + Text( + text = stringResource(reasonText(diagnostic.reason)), + color = colors.textMuted, + fontSize = 11.sp, + ) + } else { + Text( + text = stringResource(R.string.skill_diag_no_default), + color = colors.textMuted, + fontSize = 11.sp, + ) + } + diagnostic.skills.filter { it !== selected }.forEach { entry -> + DiagnosticEntry(stringResource(R.string.skill_diag_variant), entry) + } + diagnostic.duplicates.forEach { duplicate -> + DiagnosticEntry(stringResource(R.string.skill_diag_redundant), duplicate.skill) + Text( + text = stringResource( + R.string.skill_diag_retained, + displayText(duplicate.retained.name), + displayText(duplicate.retained.filePath), + ), + color = colors.textDim, + fontSize = 11.sp, + ) + } + } + } +} + +@Composable +private fun DiagnosticEntry(label: String, entry: SkillEntry) { + val colors = LocalOmpColors.current + Column(Modifier.fillMaxWidth()) { + Row { + Text(text = "$label: ", color = colors.textMuted, fontSize = 11.sp, fontWeight = FontWeight.Medium) + Text( + text = displayText(entry.name), + color = colors.text, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + ) + } + Text( + text = displayText(entry.filePath), + color = colors.textMuted, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + ) + Text( + text = buildString { + append(stringResource(R.string.skill_diag_source, displayText(entry.source))) + entry.pluginName?.let { append(" · "); append(displayText(it)) } + }, + color = colors.textDim, + fontSize = 11.sp, + ) + } +} + +private fun reasonText(reason: SkillDiagnosticReason): Int = when (reason) { + SkillDiagnosticReason.SOURCE_ORDER -> R.string.skill_diag_reason_source_order + SkillDiagnosticReason.CUSTOM_DIRECTORY -> R.string.skill_diag_reason_custom_directory + SkillDiagnosticReason.AUTHORED_OVER_INSTALLED -> R.string.skill_diag_reason_authored_over_installed +} + +/** + * Stable identity of one report: dismissal only hides the report the user saw. + * Mirrors the web `JSON.stringify(snapshot.diagnostics)` key without JSON. + */ +private fun skillDiagnosticsReportKey(snapshot: SkillDiagnosticsSnapshot): String = buildString { + for (diagnostic in snapshot.diagnostics) { + append(diagnostic.name).append('|').append(diagnostic.reason.wireName).append('|') + for (entry in diagnostic.skills) { + append(entry.name).append('@').append(entry.filePath).append(';') + } + append('#') + for (duplicate in diagnostic.duplicates) { + append(duplicate.skill.filePath).append("->").append(duplicate.retained.filePath).append(';') + } + append('\n') + } +} + +/** Web `displayText`: strip control characters and bidi overrides from omp paths. */ +private val CONTROL_CHARS = Regex("[\\p{Cc}\\u202a-\\u202e\\u2066-\\u2069]") + +private fun displayText(value: String): String = value.replace(CONTROL_CHARS, "") diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SpeechController.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SpeechController.kt new file mode 100644 index 000000000..ca0eddf2b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SpeechController.kt @@ -0,0 +1,223 @@ +package io.github.kahme247.ompweb.ui.chat + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.speech.tts.TextToSpeech +import android.speech.tts.UtteranceProgressListener +import java.util.Locale +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Whether speech synthesis is usable and which reply is being read. */ +data class SpeechState( + val speakingId: String? = null, +) + +/** One installed platform TTS voice, as the settings picker shows it. */ +data class SpeechVoiceOption( + val name: String, + val locale: String, +) + +/** + * Read-aloud as the chat view model sees it, so the view model runs in JVM + * tests without a TTS engine. + */ +interface Speech { + val state: StateFlow + + fun prepare() + + fun setVoice(name: String?) + + fun speak(id: String, text: String) + + fun toggle(id: String, text: String) + + fun stop() + + fun shutdown() +} + +/** + * Read-aloud for assistant replies (web `useSpeechSynthesis` + + * `lib/speech-sanitizer.ts`). + * + * The engine starts lazily: the first read-aloud request binds the platform + * TTS service, so a process that never speaks never pays for it. Speaking a + * second reply stops the first; closing the screen stops playback. + */ +class SpeechController(context: Context) : Speech { + + private val appContext = context.applicationContext + private val main = Handler(Looper.getMainLooper()) + private val _state = MutableStateFlow(SpeechState()) + override val state: StateFlow = _state.asStateFlow() + + private val _voices = MutableStateFlow>(emptyList()) + /** Installed platform voices; empty until [refreshVoices] bound the engine. */ + val voices: StateFlow> = _voices.asStateFlow() + + private var engine: TextToSpeech? = null + private var initializing = false + private var pending: Pair? = null + private var voiceName: String? = null + private var utteranceCounter = 0 + + /** Bind the TTS service ahead of the first click so failures hide the buttons. */ + override fun prepare() { + ensureEngine() + } + + override fun setVoice(name: String?) { + voiceName = name + if (name != null) ensureEngine() + applyVoice() + } + + override fun speak(id: String, text: String) { + val spoken = sanitizeForSpeech(text) + if (spoken.isBlank()) return + val tts = ensureEngine() + if (tts == null) { + pending = id to spoken + return + } + applyVoice() + emit { it.copy(speakingId = id) } + utteranceCounter += 1 + // QUEUE_FLUSH: a new reply replaces the previous one. + val result = tts.speak(spoken, TextToSpeech.QUEUE_FLUSH, null, "omp-$id-$utteranceCounter") + if (result == TextToSpeech.ERROR) { + emit { it.copy(speakingId = null) } + } + } + + override fun toggle(id: String, text: String) { + if (_state.value.speakingId == id) stop() else speak(id, text) + } + + override fun stop() { + engine?.stop() + pending = null + emit { it.copy(speakingId = null) } + } + + override fun shutdown() { + pending = null + engine?.stop() + engine?.shutdown() + engine = null + initializing = false + emit { SpeechState() } + } + + private fun ensureEngine(): TextToSpeech? { + engine?.let { return it } + if (initializing) return null + initializing = true + val created = TextToSpeech(appContext) { status -> + main.post { + initializing = false + if (status != TextToSpeech.SUCCESS) { + engine = null + emit { SpeechState() } + pending = null + return@post + } + applyVoice() + publishVoices() + pending?.let { (id, text) -> + pending = null + speak(id, text) + } + } + } + engine = created + created.setOnUtteranceProgressListener( + object : UtteranceProgressListener() { + override fun onStart(utteranceId: String?) = Unit + + override fun onDone(utteranceId: String?) { + main.post { emit { it.copy(speakingId = null) } } + } + + @Deprecated("Platform callback; onError(String, Int) is the modern one") + override fun onError(utteranceId: String?) { + main.post { emit { it.copy(speakingId = null) } } + } + + override fun onError(utteranceId: String?, errorCode: Int) { + main.post { emit { it.copy(speakingId = null) } } + } + }, + ) + return created + } + + private fun applyVoice() { + val tts = engine ?: return + val installed = tts.voices.orEmpty() + val chosen = resolveSpeechVoice( + installed.map { SpeechVoiceOption(it.name, it.locale.toLanguageTag()) }, + voiceName, + ) + if (chosen != null) { + tts.voice = installed.firstOrNull { it.name == chosen.name } + return + } + tts.language = Locale.getDefault() + } + + /** + * Binds the engine (if needed) and publishes the installed voices for the + * settings picker. Safe to call from every open of the General tab. + */ + fun refreshVoices() { + if (ensureEngine() != null) publishVoices() + } + + private fun publishVoices() { + val tts = engine ?: return + val installed = tts.voices.orEmpty() + .map { SpeechVoiceOption(it.name, it.locale.toLanguageTag()) } + .sortedWith(compareBy({ it.locale }, { it.name })) + if (_voices.value != installed) _voices.value = installed + } + + private fun emit(transform: (SpeechState) -> SpeechState) { + _state.value = transform(_state.value) + } +} + +/** + * The installed voice a stored preference names, or `null` for the engine's + * default language. Voice names are device-specific, so a remembered name that + * is no longer installed falls back instead of silencing every reply. + */ +fun resolveSpeechVoice(installed: List, requested: String?): SpeechVoiceOption? = + requested?.let { name -> installed.firstOrNull { it.name == name } } + +/** + * Strips markdown, code fences, links and bare URLs before synthesis — the + * port of `sanitizeTextForSpeech` (same order, same rules). + */ +fun sanitizeForSpeech(text: String): String { + var cleaned = text + cleaned = cleaned.replace(Regex("```[\\s\\S]*?```"), " ") + cleaned = cleaned.replace(Regex("`([^`]+)`"), "$1") + cleaned = cleaned.replace(Regex("!\\[([^\\]]*)\\]\\([^)]*\\)"), "") + cleaned = cleaned.replace(Regex("\\[([^\\]]+)\\]\\([^)]*\\)"), "$1") + cleaned = cleaned.replace(Regex("https?://\\S+"), "") + cleaned = cleaned.replace(Regex("]*>"), "") + cleaned = cleaned.replace(Regex("^#{1,6}\\s+", RegexOption.MULTILINE), "") + cleaned = cleaned.replace(Regex("^>\\s+", RegexOption.MULTILINE), "") + cleaned = cleaned.replace(Regex("[*_]{1,3}([^*_]+)[*_]{1,3}"), "$1") + cleaned = cleaned.replace(Regex("~~([^~]+)~~"), "$1") + cleaned = cleaned.replace(Regex("^[\\s]*[-*+]\\s+", RegexOption.MULTILINE), "") + cleaned = cleaned.replace(Regex("^[\\s]*\\d+\\.\\s+", RegexOption.MULTILINE), "") + cleaned = cleaned.replace(Regex("^[-*_]{3,}\\s*$", RegexOption.MULTILINE), "") + return cleaned.replace(Regex("\\s+"), " ").trim() +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SystemPromptIcon.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SystemPromptIcon.kt new file mode 100644 index 000000000..e5366ad1a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SystemPromptIcon.kt @@ -0,0 +1,55 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +/** + * The Material `terminal` glyph for the system-prompt action (web `AppShell` + * uses lucide's `Terminal`). `material-icons-core` ships no terminal icon and + * `material-icons-extended` would add several megabytes for a single glyph. + */ +internal val SystemPromptIcon: ImageVector by lazy { + ImageVector.Builder( + name = "SystemPrompt", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(20f, 4f) + horizontalLineTo(4f) + curveTo(2.89f, 4f, 2f, 4.9f, 2f, 6f) + verticalLineToRelative(12f) + curveTo(2f, 19.1f, 2.89f, 20f, 4f, 20f) + horizontalLineToRelative(16f) + curveTo(21.1f, 20f, 22f, 19.1f, 22f, 18f) + verticalLineTo(6f) + curveTo(22f, 4.9f, 21.1f, 4f, 20f, 4f) + close() + moveTo(20f, 18f) + horizontalLineTo(4f) + verticalLineTo(8f) + horizontalLineToRelative(16f) + verticalLineTo(18f) + close() + moveTo(18f, 17f) + horizontalLineToRelative(-6f) + verticalLineToRelative(-2f) + horizontalLineToRelative(6f) + verticalLineTo(17f) + close() + moveTo(7.5f, 17f) + lineTo(6.09f, 15.59f) + lineTo(8.67f, 13f) + lineTo(6.09f, 10.41f) + lineTo(7.5f, 9f) + lineTo(11.5f, 13f) + lineTo(7.5f, 17f) + close() + } + }.build() +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SystemPromptSheet.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SystemPromptSheet.kt new file mode 100644 index 000000000..0fabbe2b4 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/SystemPromptSheet.kt @@ -0,0 +1,142 @@ +package io.github.kahme247.ompweb.ui.chat + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpDialog + +/** + * Lazy-loaded system prompt of the bound session (web `AppShell` terminal + * popover). Read-only: the prompt is composed by omp from the host config, the + * project and the tool set, so editing here would lie about what is running. + */ +sealed interface SystemPromptState { + /** Panel closed; nothing was requested yet. */ + data object Idle : SystemPromptState + + data object Loading : SystemPromptState + + data class Loaded(val text: String) : SystemPromptState + + /** omp answered with an empty prompt (e.g. tools disabled). */ + data object Empty : SystemPromptState + + /** Request failed, or this omp build does not serve `systemPrompt`. */ + data class Failed(val reason: String?) : SystemPromptState +} + +@Composable +internal fun SystemPromptSheet(state: SystemPromptState, onDismiss: () -> Unit) { + if (state is SystemPromptState.Idle) return + OmpDialog(onDismissRequest = onDismiss) { + SystemPromptBody(state = state, onDismiss = onDismiss) + } +} + +/** + * The sheet's content without the dialog window. Dialogs compose in their own + * window, so screenshots and layout checks render this body inside the + * hardened matrix (200 % font) instead. + */ +@Composable +internal fun SystemPromptBody(state: SystemPromptState, onDismiss: () -> Unit) { + if (state is SystemPromptState.Idle) return + val colors = LocalOmpColors.current + Surface( + color = colors.panel, + contentColor = colors.text, + shape = RoundedCornerShape(12.dp), + border = BorderStroke(1.dp, colors.border), + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier + .heightIn(max = 560.dp) + .verticalScroll(rememberScrollState()) + .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.system_prompt_title), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f).semantics { heading() }, + ) + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.system_prompt_close), + tint = colors.textMuted, + modifier = Modifier.size(16.dp), + ) + } + } + when (state) { + is SystemPromptState.Loading -> Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + strokeWidth = 1.5.dp, + color = colors.textMuted, + ) + Text( + text = stringResource(R.string.system_prompt_loading), + color = colors.textMuted, + fontSize = 12.sp, + ) + } + + is SystemPromptState.Loaded -> Text( + text = state.text, + color = colors.text, + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, + ) + + is SystemPromptState.Empty -> Text( + text = stringResource(R.string.system_prompt_empty), + color = colors.textMuted, + fontSize = 12.sp, + ) + + is SystemPromptState.Failed -> Text( + text = state.reason ?: stringResource(R.string.system_prompt_unavailable), + color = colors.statusError, + fontSize = 12.sp, + ) + + is SystemPromptState.Idle -> Unit + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ToolFormat.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ToolFormat.kt new file mode 100644 index 000000000..ffc92eba5 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/ToolFormat.kt @@ -0,0 +1,295 @@ +package io.github.kahme247.ompweb.ui.chat + +import io.github.kahme247.ompweb.chat.AssistantBlock +import io.github.kahme247.ompweb.domain.formatCompactNumber +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull + +/** Tool families that get their own icon/label treatment (web `MessageView-tool-format.ts`). */ +enum class ToolCategory { Read, Search, Edit, Terminal, Todo, Task, Code, Web, Hub, Other } + +fun getToolCategory(toolName: String): ToolCategory { + val name = toolName.lowercase().replace(Regex("""\s*\(.*\)\s*$"""), "") + return when { + name == "hub" -> ToolCategory.Hub + name == "read" || name.endsWith(".read") || name.endsWith("_read") || name == "file_read" -> ToolCategory.Read + name == "grep" || name == "glob" || name.contains("search") || name.endsWith(".grep") || name.endsWith(".glob") -> ToolCategory.Search + name == "edit" || name == "write" || name == "ast_edit" || name.endsWith(".edit") || name.endsWith(".write") -> ToolCategory.Edit + name == "bash" || name == "terminal" || name == "exec" || name == "shell" || name == "cmd" -> ToolCategory.Terminal + name == "todo" || name.endsWith(".todo") -> ToolCategory.Todo + name == "task" || name == "agent" || name.endsWith(".task") || name.endsWith(".agent") -> ToolCategory.Task + name == "eval" || name.endsWith(".eval") -> ToolCategory.Code + name == "web_search" || name == "browser" || name.contains("browser") -> ToolCategory.Web + else -> ToolCategory.Other + } +} + +data class TodoSummary(val op: String, val action: String, val label: String, val task: String?) + +fun getTodoSummary(input: JsonObject?): TodoSummary? { + val op = input?.str("op") ?: return null + val task = input.str("task") + val phase = input.str("phase") + val items = input.arr("items")?.mapNotNull { (it as? JsonPrimitive)?.contentOrNull } + return when (op) { + "done" -> TodoSummary(op, "Completed", if (task != null) "Completed \"$task\"" else "Completed task", task) + "start" -> TodoSummary(op, "Started", if (task != null) "Started \"$task\"" else "Started task", task) + "append" -> { + val count = items?.size ?: 0 + val label = when { + task != null -> "Added \"$task\"" + count > 0 -> "Added $count task${if (count > 1) "s" else ""}" + else -> "Added task" + } + TodoSummary(op, "Added", label, task ?: items?.firstOrNull()) + } + "init" -> { + val listCount = input.arr("list")?.sumOf { plan -> + (plan as? JsonObject)?.arr("items")?.size ?: 0 + } ?: (items?.size ?: 0) + TodoSummary(op, "Initialized", "Initialized plan ($listCount tasks)", phase) + } + "block" -> TodoSummary(op, "Blocked", if (task != null) "Blocked \"$task\"" else "Blocked task", task) + "unblock" -> TodoSummary(op, "Unblocked", if (task != null) "Unblocked \"$task\"" else "Unblocked task", task) + "drop" -> TodoSummary(op, "Dropped", if (task != null) "Dropped \"$task\"" else "Dropped task", task) + else -> TodoSummary(op, op, "Task: $op${if (task != null) " \"$task\"" else ""}", task) + } +} + +data class HubSendSummary(val to: List, val message: String, val snippet: String) + +fun getHubSendSummary(input: JsonObject?): HubSendSummary? { + if (input?.str("op") != "send") return null + val raw = when (val to = input["to"]) { + is JsonArray -> to.mapNotNull { (it as? JsonPrimitive)?.contentOrNull } + is JsonPrimitive -> listOfNotNull(to.contentOrNull) + else -> emptyList() + } + val to = raw.filter { it.isNotEmpty() }.take(10) + if (to.isEmpty()) return null + val message = input.str("message").orEmpty() + val firstLine = message.lineSequence().map { it.trim() }.firstOrNull { it.isNotEmpty() }.orEmpty() + val snippet = if (firstLine.length > 120) firstLine.take(120) + "…" else firstLine + return HubSendSummary(to, message, snippet) +} + +data class HubJob( + val id: String, + val type: String, + val status: String, + val label: String, + val durationMs: Double? = null, + val resolvedModel: String? = null, +) + +fun getHubJobs(details: JsonObject?): List? { + if (details?.str("op") != "jobs") return null + val jobs = details.arr("jobs") ?: return null + val rows = ArrayList() + for (raw in jobs) { + val job = raw as? JsonObject ?: continue + val id = job.str("id")?.takeIf { it.isNotEmpty() } ?: continue + rows += HubJob( + id = id, + type = job.str("type") ?: "task", + status = job.str("status") ?: "running", + label = job.str("label")?.takeIf { it.isNotEmpty() } ?: id, + durationMs = job.num("durationMs"), + resolvedModel = job.str("resolvedModel")?.takeIf { it.isNotEmpty() }, + ) + if (rows.size >= 50) break + } + return rows.ifEmpty { null } +} + +fun getHubJobsHeader(jobs: List): String { + val waiting = jobs.any { it.status == "running" || it.status == "started" || it.status == "waiting" } + return if (waiting) { + "waiting on ${jobs.size} job${if (jobs.size == 1) "" else "s"}" + } else { + "${jobs.size} job${if (jobs.size == 1) "" else "s"}" + } +} + +fun getToolPreview(block: AssistantBlock.ToolCall): String { + val input = block.input + if (block.toolName.lowercase() == "ask") { + val preview = getAskPreview(input) + if (preview != null) return preview.take(120) + } + when (getToolCategory(block.toolName)) { + ToolCategory.Todo -> getTodoSummary(input)?.let { return it.label } + ToolCategory.Hub -> { + when (input.str("op")) { + "send" -> return getHubSendSummary(input)?.snippet ?: "send" + "jobs" -> return "jobs" + else -> input.str("op")?.let { return it } + } + } + else -> Unit + } + val keys = input.keys.toList() + if (keys.isEmpty()) return "" + for (key in listOf("command", "path", "file_path", "pattern", "query")) { + if (key in input) return formatPreviewValue(input[key]).take(120) + } + return formatPreviewValue(input[keys.first()]).take(120) +} + +private fun getAskPreview(input: JsonObject): String? { + val questions = input.arr("questions") ?: return null + val prompts = questions.filterIsInstance().mapNotNull { question -> + val header = question.str("header").orEmpty().trim() + val prompt = question.str("question").orEmpty().trim() + when { + header.isNotEmpty() && prompt.isNotEmpty() -> "$header: $prompt" + header.isNotEmpty() -> header + prompt.isNotEmpty() -> prompt + else -> null + } + } + if (prompts.isEmpty()) { + return if (questions.isNotEmpty()) "${questions.size} question${if (questions.size == 1) "" else "s"}" else "" + } + val label = if (prompts.size == 1) "Question" else "${prompts.size} questions" + return "$label: ${prompts.joinToString(" · ") { "“$it”" }}" +} + +private fun formatPreviewValue(value: JsonElement?): String = when (value) { + null -> "" + is JsonPrimitive -> value.contentOrNull ?: value.toString() + else -> value.toString() +} + +data class SemanticToolLabel(val action: String, val target: String, val isFile: Boolean = false) + +fun getSemanticToolLabel(block: AssistantBlock.ToolCall): SemanticToolLabel { + val input = block.input + when (getToolCategory(block.toolName)) { + ToolCategory.Todo -> getTodoSummary(input)?.let { summary -> + return SemanticToolLabel(summary.action, summary.task ?: summary.label) + } + ToolCategory.Hub -> { + when (input.str("op")) { + "send" -> { + val send = getHubSendSummary(input) + return if (send != null) { + SemanticToolLabel("IRC", "→ ${send.to.joinToString(", ")}") + } else { + SemanticToolLabel("IRC", "send") + } + } + "jobs" -> return SemanticToolLabel("hub", "jobs") + else -> input.str("op")?.let { return SemanticToolLabel("hub", it) } + } + } + ToolCategory.Read -> input.str("path")?.let { + return SemanticToolLabel("Read", it, isFile = true) + } + ToolCategory.Search -> { + input.str("pattern")?.let { + val scope = input.str("path")?.let { path -> " in $path" }.orEmpty() + return SemanticToolLabel("Search", "\"$it\"$scope") + } + input.str("path")?.let { return SemanticToolLabel("Find", it) } + input.str("query")?.let { return SemanticToolLabel("Search", "\"$it\"") } + } + ToolCategory.Edit -> input.str("path")?.let { + val action = if (block.toolName.lowercase().contains("write")) "Create" else "Edit" + return SemanticToolLabel(action, it, isFile = true) + } + ToolCategory.Terminal -> input.str("command")?.let { return SemanticToolLabel("Run", it) } + ToolCategory.Code -> { + val target = input.str("title") ?: input.str("language")?.let { "$it code" } ?: "code" + return SemanticToolLabel("Eval", target) + } + ToolCategory.Web -> input.str("query")?.let { return SemanticToolLabel("Search Web", "\"$it\"") } + else -> Unit + } + return SemanticToolLabel(block.toolName, getToolPreview(block)) +} + +fun formatToolCommand(block: AssistantBlock.ToolCall): String { + val input = block.input + input.str("command")?.let { return it } + if (getToolCategory(block.toolName) == ToolCategory.Hub) { + val op = input.str("op") + if (op == "send") { + val send = getHubSendSummary(input) + return if (send != null) "hub send → ${send.to.joinToString(", ")}" else "hub send" + } + if (op != null) return "hub $op" + } + input.str("path")?.let { return "${block.toolName} $it" } + input.str("file_path")?.let { return "${block.toolName} $it" } + input.str("query")?.let { return "${block.toolName} $it" } + return "${block.toolName} $input" +} + +fun formatToolOutput(text: String, toolName: String): String = + if (isReadToolName(toolName)) { + text.lineSequence().joinToString("\n") { it.replace(Regex("""^\s*\d+:\s?"""), "") } + } else { + text + } + +fun isReadToolName(toolName: String): Boolean { + val name = toolName.lowercase() + return name == "read" || name.endsWith(".read") || name.endsWith("_read") +} + +fun getToolResultMeta(details: JsonObject?): String? { + if (details == null) return null + val usage = details.obj("usage") ?: details + fun read(vararg keys: String): Int? = keys.firstNotNullOfOrNull { key -> + val value = (usage[key] as? JsonPrimitive)?.doubleOrNull ?: (usage[key] as? JsonPrimitive)?.intOrNull?.toDouble() + value?.takeIf { it > 0 }?.toInt() + } + val input = read("input", "inputTokens", "input_tokens") + val output = read("output", "outputTokens", "output_tokens") + val cacheRead = read("cacheRead", "cache_read", "cacheReadTokens") + val cacheWrite = read("cacheWrite", "cache_write", "cacheWriteTokens") + val parts = listOfNotNull( + input?.let { "in ${formatCompactNumber(it.toDouble())}" }, + output?.let { "out ${formatCompactNumber(it.toDouble())}" }, + cacheRead?.let { "cache R ${formatCompactNumber(it.toDouble())}" }, + cacheWrite?.let { "cache W ${formatCompactNumber(it.toDouble())}" }, + ) + return parts.joinToString(" · ").ifEmpty { null } +} + +/** TUI-style job duration: `45s`, `31m30s`, `2h5m`. */ +fun formatJobDuration(ms: Double?): String? { + if (ms == null || ms < 1000) return null + val totalSeconds = (ms / 1000).toLong() + val hours = totalSeconds / 3600 + val minutes = (totalSeconds % 3600) / 60 + val seconds = totalSeconds % 60 + return when { + hours > 0 -> if (minutes > 0) "${hours}h${minutes}m" else "${hours}h" + minutes > 0 -> if (seconds > 0) "${minutes}m${seconds}s" else "${minutes}m" + else -> "${seconds}s" + } +} + +internal fun JsonObject.str(key: String): String? = + (this[key] as? JsonPrimitive)?.contentOrNull + +internal fun JsonObject.num(key: String): Double? = + (this[key] as? JsonPrimitive)?.doubleOrNull + +internal fun JsonObject.bool(key: String): Boolean? = + (this[key] as? JsonPrimitive)?.booleanOrNull + +internal fun JsonObject.int(key: String): Int? = + (this[key] as? JsonPrimitive)?.intOrNull + +internal fun JsonObject.obj(key: String): JsonObject? = this[key] as? JsonObject + +internal fun JsonObject.arr(key: String): JsonArray? = this[key] as? JsonArray diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/TranscriptTime.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/TranscriptTime.kt new file mode 100644 index 000000000..d8aedf76b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/chat/TranscriptTime.kt @@ -0,0 +1,202 @@ +package io.github.kahme247.ompweb.ui.chat + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.database.ContentObserver +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.text.format.DateFormat +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.currentStateAsState +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.chat.DayBucket +import io.github.kahme247.ompweb.chat.TimeBucket +import io.github.kahme247.ompweb.chat.TranscriptFormat +import java.text.SimpleDateFormat +import java.time.ZoneId +import java.util.Date +import kotlinx.coroutines.delay + +/** + * The locale-aware shapes of a transcript timestamp. `android.text.format + * .DateFormat.getTimeFormat` follows the user's 24-hour setting, and the date + * patterns follow the locale, while [TranscriptFormat] decides which shape a + * timestamp needs (today, yesterday, this year, older). + */ +@Immutable +class ChatTimeFormatter( + private val timeFormat: java.text.DateFormat, + private val sameYearFormat: java.text.SimpleDateFormat, + private val anyYearFormat: java.text.SimpleDateFormat, + private val fullFormat: java.text.DateFormat, + private val zone: ZoneId, + private val todayLabel: String, + private val yesterdayLabel: String, +) { + /** The time alone, e.g. `09:41` or `9:41 AM`. */ + fun time(timestamp: Long): String = timeFormat.format(Date(timestamp)) + + /** The header stamp: `09:41`, `Yesterday 09:41`, `26 Oct, 09:41`, `26 Oct 2025`. */ + fun header(timestamp: Long, now: Long): String = when ( + TranscriptFormat.timeBucket(timestamp, now, zone) + ) { + TimeBucket.Today -> time(timestamp) + TimeBucket.Yesterday -> "$yesterdayLabel ${time(timestamp)}" + TimeBucket.SameYear -> "${sameYearFormat.format(Date(timestamp))}, ${time(timestamp)}" + TimeBucket.OlderYear -> anyYearFormat.format(Date(timestamp)) + } + + /** The day separator: `Today`, `Yesterday`, otherwise a date. */ + fun daySeparator(timestamp: Long, now: Long): String = when ( + TranscriptFormat.dayBucket(timestamp, now, zone) + ) { + DayBucket.Today -> todayLabel + DayBucket.Yesterday -> yesterdayLabel + DayBucket.Date -> anyYearFormat.format(Date(timestamp)) + } + + /** + * The full date and time shown when a timestamp is tapped. The time is part + * of it: an older-than-this-year header shows the date only, so this dialog + * is the only place that can show the clock time for those turns. + */ + fun full(timestamp: Long): String { + val date = Date(timestamp) + return "${fullFormat.format(date)}, ${timeFormat.format(date)}" + } +} + +/** + * [ChatTimeFormatter] for the current locale, 12/24-hour setting and time zone. + * + * A time-zone or 12/24-hour change does not recreate the composition, so the + * screen observes both (broadcast + settings observer) and publishes a revision + * through [LocalTimeFormatRevision]; the formatter's caches key on it. The date + * patterns come from the locale skeleton (`getBestDateTimePattern`), so en-US + * shows "Oct 26" instead of the German "26 Oct". + */ +@Composable +internal fun rememberChatTimeFormatter(): ChatTimeFormatter { + val context = LocalContext.current + // LocalConfiguration is observable state, so a locale change re-creates + // the formatter instead of keeping the process default. + val configuration = LocalConfiguration.current + val locale = configuration.locales[0] + val revision = LocalTimeFormatRevision.current + val zone = remember(revision) { ZoneId.systemDefault() } + val today = stringResource(R.string.chat_day_today) + val yesterday = stringResource(R.string.chat_day_yesterday) + return remember(context, locale, zone, today, yesterday, revision) { + ChatTimeFormatter( + timeFormat = DateFormat.getTimeFormat(context), + sameYearFormat = SimpleDateFormat( + DateFormat.getBestDateTimePattern(locale, "dMMM"), + locale, + ), + anyYearFormat = SimpleDateFormat( + DateFormat.getBestDateTimePattern(locale, "yMMMd"), + locale, + ), + fullFormat = DateFormat.getLongDateFormat(context), + zone = zone, + todayLabel = today, + yesterdayLabel = yesterday, + ) + } +} + +/** + * The transcript's shared wall clock. The chat screen provides a minute tick, + * so "Today"/"Yesterday" and the header shapes keep following midnight while + * the app stays open; the screenshot harness leaves it unset and the pinned + * [LocalChatClock] applies. + */ +internal val LocalChatNow = compositionLocalOf { null } + +/** Bumped when the time zone or the user's 12/24-hour setting changes. */ +internal val LocalTimeFormatRevision = compositionLocalOf { 0 } + +/** + * Watches the two settings the formatter caches (zone and hour cycle). Neither + * recreates the composition, so their value is a revision the formatter keys + * on instead of holding the old zone until the process restarts. + */ +@Composable +internal fun rememberTimeFormatRevision(): Int { + val context = LocalContext.current + var revision by remember { mutableIntStateOf(0) } + DisposableEffect(context) { + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + revision++ + } + } + val filter = IntentFilter().apply { + addAction(Intent.ACTION_TIMEZONE_CHANGED) + addAction(Intent.ACTION_TIME_CHANGED) + } + ContextCompat.registerReceiver( + context, + receiver, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + val observer = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean) { + revision++ + } + } + context.contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.TIME_12_24), + false, + observer, + ) + onDispose { + context.unregisterReceiver(receiver) + context.contentResolver.unregisterContentObserver(observer) + } + } + return revision +} + +/** + * The shared minute clock of the transcript: day labels and header buckets read + * it, so a screen open over midnight re-labels itself without a new message. + * Ticks only while the screen is STARTED. + */ +@Composable +internal fun rememberMinuteNow(): Long { + val clock = LocalChatClock.current + val started = LocalLifecycleOwner.current.lifecycle + .currentStateAsState() + .value + .isAtLeast(Lifecycle.State.STARTED) + var now by remember(clock) { mutableLongStateOf(clock()) } + LaunchedEffect(clock, started) { + if (!started) return@LaunchedEffect + while (true) { + val value = clock() + now = value + delay(60_000L - value % 60_000L) + } + } + return now +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/CompletionSound.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/CompletionSound.kt new file mode 100644 index 000000000..4a193e8e0 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/CompletionSound.kt @@ -0,0 +1,38 @@ +package io.github.kahme247.ompweb.ui.composer + +import android.media.AudioManager +import android.media.ToneGenerator + +/** + * The terminal-agent-end tone (web `hooks/useAudio.ts`): one short beep on the + * notification stream, played only when `omp-sound-enabled` is on. + * + * A plain function type keeps the view model JVM-testable; the Android + * implementation below is created once per app container. + */ +fun interface CompletionSound { + fun play() +} + +/** The tone is short and fire-and-forget; a device without one stays silent. */ +class AndroidCompletionSound : CompletionSound { + + private val tone = try { + ToneGenerator(AudioManager.STREAM_NOTIFICATION, VOLUME_PERCENT) + } catch (_: RuntimeException) { + null + } + + override fun play() { + try { + tone?.startTone(ToneGenerator.TONE_PROP_BEEP, TONE_MILLIS) + } catch (_: RuntimeException) { + // Audio focus or a released stream: silence is the correct fallback. + } + } + + private companion object { + const val VOLUME_PERCENT = 70 + const val TONE_MILLIS = 140 + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerAttachments.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerAttachments.kt new file mode 100644 index 000000000..e5506a61f --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerAttachments.kt @@ -0,0 +1,224 @@ +package io.github.kahme247.ompweb.ui.composer + +import android.content.ClipboardManager +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.provider.OpenableColumns +import android.util.Base64 +import androidx.core.content.FileProvider +import io.github.kahme247.ompweb.domain.AttachedTextFile +import io.github.kahme247.ompweb.domain.ComposerImage +import io.github.kahme247.ompweb.domain.MAX_ATTACHED_IMAGE_BYTES +import io.github.kahme247.ompweb.domain.MAX_ATTACHED_TEXT_BYTES +import io.github.kahme247.ompweb.domain.isTextAttachmentFile +import io.github.kahme247.ompweb.domain.readCappedOrNull +import io.github.kahme247.ompweb.ui.files.FileTransfer +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull + +/** What one picker/share batch produced; [skipped] counts unusable candidates. */ +data class AttachmentBatch( + val images: List = emptyList(), + val files: List = emptyList(), + val skipped: Int = 0, +) + +/** + * Android side of the web composer's attachment pickers: + * + * - text files are read as UTF-8 and handed to `ComposerController.addTextFiles` + * (which owns the 4 MB / 10-file budget and banners), + * - images become base64 `ComposerImage`s. Photos that would exceed the 5 MB + * decoded cap (or exceed 2048 px) are downscaled and re-encoded as JPEG, + * because a modern camera file is always larger than the RPC accepts, + * - the camera writes into `cacheDir/camera` through the app's FileProvider, + * and clipboard images go through the same reader as picked ones. + */ +object ComposerAttachments { + + suspend fun readShare(context: Context, uris: List): AttachmentBatch { + val images = ArrayList() + val files = ArrayList() + var skipped = 0 + for (uri in uris) { + val mime = context.contentResolver.getType(uri).orEmpty() + val name = FileTransfer.queryDisplayName(context, uri) ?: uri.lastPathSegment ?: "shared" + when { + mime.startsWith("image/") -> { + val image = readImage(context, uri) + if (image != null) images += image else skipped++ + } + isTextAttachmentFile(name, mime) -> { + val file = readTextFile(context, uri) + if (file != null) files += file else skipped++ + } + else -> skipped++ + } + } + return AttachmentBatch(images = images, files = files, skipped = skipped) + } + + fun readTextFiles(context: Context, uris: List): AttachmentBatch { + val files = uris.mapNotNull { readTextFile(context, it) } + return AttachmentBatch(files = files, skipped = uris.size - files.size) + } + + suspend fun readImages(context: Context, uris: List): AttachmentBatch { + val images = uris.mapNotNull { readImage(context, it) } + return AttachmentBatch(images = images, skipped = uris.size - images.size) + } + + /** A picked image: raw when it fits, downscaled JPEG otherwise. */ + fun readImage(context: Context, uri: Uri): ComposerImage? { + val bytes = try { + context.contentResolver.openInputStream(uri)?.use { stream -> + readCappedOrNull(stream, MAX_IMAGE_READ_BYTES) + } + } catch (_: SecurityException) { + null + } catch (_: IOException) { + null + } ?: return null + // The declared type is provider metadata; the bytes decide what is + // actually sent, and anything non-raster is re-encoded as JPEG. + val mime = detectImageMime(bytes) ?: return downscale(bytes) + if (bytes.size <= MAX_ATTACHED_IMAGE_BYTES) { + return ComposerImage(Base64.encodeToString(bytes, Base64.NO_WRAP), mime) + } + return downscale(bytes) + } + + /** Raster formats the RPC accepts raw, identified by their signature. */ + fun detectImageMime(bytes: ByteArray): String? = when { + bytes.size > 3 && bytes[0] == 0xFF.toByte() && bytes[1] == 0xD8.toByte() && bytes[2] == 0xFF.toByte() -> + "image/jpeg" + bytes.size > 8 && bytes[0] == 0x89.toByte() && bytes[1] == 0x50.toByte() && + bytes[2] == 0x4E.toByte() && bytes[3] == 0x47.toByte() -> "image/png" + bytes.size > 12 && ascii(bytes, 0, 4) == "RIFF" && ascii(bytes, 8, 4) == "WEBP" -> "image/webp" + bytes.size > 6 && ascii(bytes, 0, 4) == "GIF8" -> "image/gif" + else -> null + } + + private fun ascii(bytes: ByteArray, offset: Int, length: Int): String = + String(bytes, offset, length, Charsets.US_ASCII) + + /** + * Decode with a bounded sample size, then compress in decreasing quality + * until the decoded size fits the per-image budget. Returns null when even + * the smallest attempt is too large (or undecodable). + */ + fun downscale(bytes: ByteArray): ComposerImage? { + val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds) + if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null + var sample = 1 + while (bounds.outWidth / sample > MAX_IMAGE_EDGE || bounds.outHeight / sample > MAX_IMAGE_EDGE) { + sample *= 2 + } + val options = BitmapFactory.Options().apply { inSampleSize = sample } + val decoded = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) ?: return null + var bitmap = decoded + val longest = maxOf(bitmap.width, bitmap.height) + if (longest > MAX_IMAGE_EDGE) { + val scale = MAX_IMAGE_EDGE.toFloat() / longest + bitmap = Bitmap.createScaledBitmap( + decoded, + (decoded.width * scale).toInt().coerceAtLeast(1), + (decoded.height * scale).toInt().coerceAtLeast(1), + true, + ) + if (bitmap !== decoded) decoded.recycle() + } + try { + for (quality in intArrayOf(85, 70, 55, 40)) { + val out = ByteArrayOutputStream() + if (!bitmap.compress(Bitmap.CompressFormat.JPEG, quality, out)) continue + val encoded = out.toByteArray() + if (encoded.size <= MAX_ATTACHED_IMAGE_BYTES) { + return ComposerImage(Base64.encodeToString(encoded, Base64.NO_WRAP), "image/jpeg") + } + } + return null + } finally { + bitmap.recycle() + } + } + + fun readTextFile(context: Context, uri: Uri): AttachedTextFile? { + if ((declaredSize(context, uri) ?: 0L) > MAX_ATTACHED_TEXT_BYTES) return null + val bytes = try { + context.contentResolver.openInputStream(uri)?.use { stream -> + readCappedOrNull(stream, MAX_TEXT_READ_BYTES) + } + } catch (_: SecurityException) { + null + } catch (_: IOException) { + null + } ?: return null + if (bytes.isEmpty() || bytes.any { it == 0.toByte() }) return null + val name = FileTransfer.queryDisplayName(context, uri) ?: uri.lastPathSegment ?: "attachment.txt" + val mime = context.contentResolver.getType(uri) ?: "text/plain" + val content = String(bytes, Charsets.UTF_8) + return AttachedTextFile(name = name, mimeType = mime, content = content, size = bytes.size) + } + + private fun declaredSize(context: Context, uri: Uri): Long? = try { + context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null) + ?.use { cursor -> + if (cursor.moveToFirst() && !cursor.isNull(0)) cursor.getLong(0) else null + } + } catch (_: SecurityException) { + null + } catch (_: IllegalArgumentException) { + null + } + + /** Clipboard image as a draft attachment (`paste image` in the plus menu). */ + suspend fun clipboardImage(context: Context): ComposerImage? { + val uri = clipboardImageUri(context) ?: return null + return withContext(Dispatchers.IO) { readImage(context, uri) } + } + + suspend fun hasClipboardImage(context: Context): Boolean = clipboardImageUri(context) != null + + /** + * Clipboard access is a cross-process binder call and can hang; the menu + * probe must never block the input path, so it runs on IO with a timeout. + */ + private suspend fun clipboardImageUri(context: Context): Uri? = withContext(Dispatchers.IO) { + withTimeoutOrNull(CLIPBOARD_TIMEOUT_MS) { + val manager = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + ?: return@withTimeoutOrNull null + val clip = manager.primaryClip?.takeIf { it.itemCount > 0 } ?: return@withTimeoutOrNull null + val uri = clip.getItemAt(0).uri ?: return@withTimeoutOrNull null + val mime = context.contentResolver.getType(uri) + uri.takeIf { mime == null || mime.startsWith("image/") } + } + } + + /** A fresh camera target; the provider authority matches the manifest entry. */ + fun newCameraTarget(context: Context): CameraTarget { + val dir = File(context.cacheDir, "camera").apply { mkdirs() } + val file = File(dir, "photo-${System.currentTimeMillis()}.jpg") + val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) + return CameraTarget(file = file, uri = uri) + } + + private const val MAX_IMAGE_EDGE = 2048 + + /** Large camera files are downscaled, so read well past the decoded cap. */ + private const val MAX_IMAGE_READ_BYTES = 32 * 1024 * 1024 + + /** Reading past the text budget means the file is over it; a capped read must not look complete. */ + private const val MAX_TEXT_READ_BYTES = MAX_ATTACHED_TEXT_BYTES + + private const val CLIPBOARD_TIMEOUT_MS = 500L +} + +data class CameraTarget(val file: File, val uri: Uri) diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerBar.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerBar.kt new file mode 100644 index 000000000..20fb3248b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerBar.kt @@ -0,0 +1,1028 @@ +package io.github.kahme247.ompweb.ui.composer + +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.KeyboardArrowDown +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalMinimumInteractiveComponentSize +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.chat.AutoRetry +import io.github.kahme247.ompweb.chat.QueueSnapshot +import io.github.kahme247.ompweb.domain.AttachedTextFile +import io.github.kahme247.ompweb.domain.ComposerImage +import io.github.kahme247.ompweb.domain.DictationState +import io.github.kahme247.ompweb.domain.SttAfter +import io.github.kahme247.ompweb.domain.SubmitDuringRunBehavior +import io.github.kahme247.ompweb.domain.streamingSubmitBehavior +import io.github.kahme247.ompweb.domain.ToolPreset +import io.github.kahme247.ompweb.domain.WordGhost +import io.github.kahme247.ompweb.ui.composer.controls.ComposerControlsRow +import io.github.kahme247.ompweb.ui.composer.controls.ContextDetailSheet +import io.github.kahme247.ompweb.ui.composer.controls.ComposerControlsViewModel +import io.github.kahme247.ompweb.ui.composer.controls.ghostTextTransformation +import io.github.kahme247.ompweb.ui.composer.dictation.DictationViewModel +import io.github.kahme247.ompweb.ui.composer.dictation.LiveDictationStrip +import io.github.kahme247.ompweb.ui.composer.dictation.MicButton +import io.github.kahme247.ompweb.ui.composer.dictation.RecordingDeck +import io.github.kahme247.ompweb.ui.composer.dictation.ReportVisibility +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpDropdownMenu +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * The shared composer surface (`components/ChatInput.tsx`): attachments, the + * slash/at/history palettes, the queue bar, status and auto-retry banners, word + * prediction, dictation and the primary Send/Queue/Stop action. + * + * The chat screen and the new-session pane render the same composable; both + * own the view model that routes the actions. + */ +@Composable +fun ComposerBar( + composer: ComposerController, + controls: ComposerControlsViewModel, + dictation: DictationViewModel, + ghost: WordGhost?, + placeholder: String, + streaming: Boolean, + stopping: Boolean, + bashRunning: Boolean, + queue: QueueSnapshot, + autoRetry: AutoRetry?, + submitDuringRun: SubmitDuringRunBehavior, + /** A submit is waiting for the live run state; the action reads "Sending…". */ + submitting: Boolean = false, + onSend: () -> Unit, + onQueue: () -> Unit, + onStop: () -> Unit, + onAbortRetry: () -> Unit, + onAcceptGhost: () -> Unit, + onHistory: () -> Unit, + onQueueEdit: (String, String) -> Unit, + onQueueDelete: (String, String) -> Unit, + onQueueSteer: (String) -> Unit, + modifier: Modifier = Modifier, + autoFocus: Boolean = false, + inputEnabled: Boolean = true, + /** Web `onMinimize`: Esc in an idle, empty composer collapses it into the pill. */ + onMinimize: (() -> Unit)? = null, + /** + * Cap of the text field (`ChatLayout.plan`). The field scrolls internally + * beyond it, so a long draft can never push the transcript out of the pane. + */ + textFieldMaxHeight: Dp = 180.dp, + /** Lines the field shows before it scrolls (height and lines cap together). */ + textFieldMaxLines: Int = 9, + /** + * The model / thinking / tools row. Hidden while the keyboard is open on a + * phone so the transcript keeps its height; the row returns as soon as the + * IME closes (back), and it is reachable before opening the keyboard. + */ + showControlsRow: Boolean = true, +) { + val state by composer.state.collectAsStateWithLifecycle() + val controlsState by controls.state.collectAsStateWithLifecycle() + val dictationState by dictation.state.collectAsStateWithLifecycle() + // The composer shell stays mounted while dictation is Live, unlike the mic + // button and the recording deck, so report ON_START/ON_STOP here: a + // backgrounded live capture must commit its partial and stop listening. + ReportVisibility(dictation) + val colors = LocalOmpColors.current + val context = LocalContext.current + val resources = LocalResources.current + val scope = rememberCoroutineScope() + val focusRequester = remember { FocusRequester() } + + var field by remember { + mutableStateOf( + TextFieldValue( + text = state.text, + selection = TextRange(state.caret.coerceIn(0, state.text.length)), + ), + ) + } + var plusOpen by remember { mutableStateOf(false) } + var queueExpanded by remember { mutableStateOf(false) } + var deleteTarget by remember { mutableStateOf?>(null) } + var cameraTarget by remember { mutableStateOf(null) } + + // Programmatic edits (palette accept, history, restore) carry an id; user + // typing never does, so the field keeps the IME composition intact. + LaunchedEffect(state.edit) { + val edit = state.edit ?: return@LaunchedEffect + if (field.text != edit.text || field.selection.start != edit.caret) { + field = TextFieldValue(edit.text, TextRange(edit.caret.coerceIn(0, edit.text.length))) + } + } + LaunchedEffect(state.draft.value) { + // Draft restore from another owner (recoverDraft) without an edit id. + if (state.edit == null && field.text != state.draft.value) { + field = TextFieldValue( + state.draft.value, + TextRange(state.caret.coerceIn(0, state.draft.value.length)), + ) + } + } + LaunchedEffect(autoFocus) { + if (autoFocus) focusRequester.requestFocus() + } + + // A prediction only paints for the exact draft state it was made for. + val visibleGhost = ghost?.takeIf { + it.text == state.text && it.caret == field.selection.start && field.text == state.text + } + var clipboardHasImage by remember { mutableStateOf(false) } + LaunchedEffect(plusOpen) { + if (plusOpen) clipboardHasImage = ComposerAttachments.hasClipboardImage(context) + } + + fun applyBatch(batch: AttachmentBatch) { + if (batch.images.isNotEmpty()) composer.addImages(batch.images) + if (batch.files.isNotEmpty()) composer.addTextFiles(batch.files) + if (batch.skipped > 0) { + composer.showNotice( + resources.getString(R.string.composer_attach_file_skipped, batch.skipped), + ) + } + } + + val documentPicker = rememberLauncherForActivityResult( + ActivityResultContracts.OpenMultipleDocuments(), + ) { uris -> + if (uris.isEmpty()) return@rememberLauncherForActivityResult + composer.setReadingAttachments(true) + scope.launch { + val batch = withContext(Dispatchers.IO) { ComposerAttachments.readTextFiles(context, uris) } + composer.setReadingAttachments(false) + applyBatch(batch) + } + } + val photoPicker = rememberLauncherForActivityResult( + ActivityResultContracts.PickMultipleVisualMedia(MAX_IMAGES_PER_PICK), + ) { uris -> + if (uris.isEmpty()) return@rememberLauncherForActivityResult + composer.setReadingAttachments(true) + scope.launch { + val batch = withContext(Dispatchers.IO) { ComposerAttachments.readImages(context, uris) } + composer.setReadingAttachments(false) + applyBatch(batch) + } + } + val cameraLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.TakePicture(), + ) { saved -> + val target = cameraTarget + cameraTarget = null + if (!saved || target == null) { + target?.file?.delete() + return@rememberLauncherForActivityResult + } + composer.setReadingAttachments(true) + scope.launch { + val image = withContext(Dispatchers.IO) { ComposerAttachments.readImage(context, target.uri) } + target.file.delete() + composer.setReadingAttachments(false) + if (image != null) composer.addImages(listOf(image)) else { + Toast.makeText(context, R.string.composer_camera_failed, Toast.LENGTH_SHORT).show() + } + } + } + + fun pasteImage() { + scope.launch { + val image = ComposerAttachments.clipboardImage(context) + if (image != null) composer.addImages(listOf(image)) else { + Toast.makeText(context, R.string.composer_paste_unavailable, Toast.LENGTH_SHORT).show() + } + } + } + + fun primary() { + // A submit that is still resolving the live state owns the action; a + // second tap must not route the same draft again. + if (submitting) return + // On-device recognition writes its partials into the field, so the + // primary action ends the take (the text stays) — the strip under the + // field carries the explicit Stop/Cancel. + if (dictationState is DictationState.Live) { + dictation.stop() + return + } + val recording = dictationState as? DictationState.Recording + if (recording != null) { + val after = if (!streaming) { + SttAfter.Send + } else if (submitDuringRun == SubmitDuringRunBehavior.STEER) { + SttAfter.Steer + } else { + SttAfter.FollowUp + } + dictation.finish(after) + return + } + if (streaming) { + // Same choice as Enter (web `ChatInput`: `omp-web:submit-during-run`). + // A phone keyboard cannot submit, so the button must not force a + // follow-up behind the user's Steer preference. + if (streamingSubmitBehavior(state.hasContent, submitDuringRun) == SubmitDuringRunBehavior.STEER) { + onSend() + } else { + onQueue() + } + } else { + onSend() + } + } + + val queued = remember(queue) { + queue.followUp.map { "followUp" to it } + queue.steering.map { "steering" to it } + } + val dictationBusy = dictationState is DictationState.Uploading || dictationState is DictationState.Transcribing + val recording = dictationState is DictationState.Recording + val liveListening = dictationState is DictationState.Live + val primaryEnabled = inputEnabled && !dictationBusy && !submitting && + (recording || liveListening || state.hasContent || streaming) + val primaryLabel = when { + liveListening -> stringResource(R.string.dictation_live_stop) + dictationState is DictationState.Recording -> stringResource(R.string.chat_send) + submitting -> stringResource(R.string.chat_sending) + streaming -> when (streamingSubmitBehavior(state.hasContent, submitDuringRun)) { + SubmitDuringRunBehavior.STEER -> stringResource(R.string.composer_steer) + // Empty composer = Stop, which the label below overrides. + else -> stringResource(R.string.composer_queue) + } + state.bashMode -> stringResource(R.string.composer_run_shell) + else -> stringResource(R.string.chat_send) + } + + Column(modifier = modifier.fillMaxWidth()) { + if (queued.isNotEmpty()) { + QueueBar( + entries = queued, + expanded = queueExpanded, + onToggle = { queueExpanded = !queueExpanded }, + onEdit = onQueueEdit, + onDelete = { kind, text -> deleteTarget = kind to text }, + onSteer = onQueueSteer, + ) + } + autoRetry?.let { retry -> + AutoRetryBanner(retry = retry, onAbort = onAbortRetry) + } + + Surface( + color = colors.panel, + contentColor = colors.text, + shape = RoundedCornerShape(12.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, colors.border), + modifier = Modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)) { + if (state.images.isNotEmpty() || state.files.isNotEmpty() || state.readingAttachments) { + AttachmentStrip( + images = state.images, + files = state.files, + reading = state.readingAttachments, + onRemoveImage = composer::removeImage, + onRemoveFile = composer::removeFile, + ) + } + + state.slash?.let { slash -> + SlashPalette( + commands = slash.commands, + activeIndex = slash.activeIndex, + onPick = { index -> + composer.setSlashActive(index) + composer.acceptSlash() + }, + ) + } + state.at?.let { at -> + AtPalette( + entries = at.entries, + activeIndex = at.activeIndex, + searchingAll = at.searchingAll, + onPick = { index -> + composer.setAtActive(index) + composer.acceptAt() + }, + ) + } + state.history?.let { history -> + HistoryPalette( + entries = history.entries, + activeIndex = history.activeIndex, + onPick = { index -> + composer.setHistoryActive(index) + composer.acceptHistory() + }, + ) + } + + val dictationActive = dictationState !is DictationState.Idle && + dictationState !is DictationState.Gone && + // On-device recognition types into the field, so the field + // must stay visible; only the strip below it is added. + dictationState !is DictationState.Live + if (dictationActive) { + RecordingDeck(dictation, Modifier.fillMaxWidth().padding(vertical = 4.dp)) + } else { + BasicTextField( + value = field, + enabled = inputEnabled, + onValueChange = { next -> + val textChanged = next.text != field.text + val caretChanged = next.selection.start != field.selection.start + field = next + if (textChanged) { + composer.onTextChanged(next.text, next.selection.start) + } else if (caretChanged) { + composer.onCaretMoved(next.selection.start) + } + }, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 40.dp, max = textFieldMaxHeight) + .focusRequester(focusRequester) + .onPreviewKeyEvent { event -> + if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false + when (event.key) { + Key.Enter -> { + val open = state.slash ?: state.at ?: state.history + when (open) { + is SlashMenuUi -> composer.acceptSlash() + is AtMenuUi -> composer.acceptAt() + is HistoryMenuUi -> composer.acceptHistory() + null -> { + if (event.isShiftPressed) return@onPreviewKeyEvent false + // Web `ChatInput` handles Enter with `handleSend`/`sendQueued` + // by `omp-web:submit-during-run`; only the Send/Queue + // button always queues. `primary()` would force a + // follow-up during a run and ignore a `steer` preference. + onSend() + } + } + true + } + Key.Tab -> { + when { + state.history != null -> composer.acceptHistory() + state.slash != null -> composer.acceptSlash() + state.at != null -> composer.acceptAt() + visibleGhost != null && state.canAcceptGhost -> onAcceptGhost() + else -> return@onPreviewKeyEvent false + } + true + } + Key.DirectionUp -> { + when { + state.slash != null -> composer.moveSlash(-1) + state.at != null -> composer.moveAt(-1) + state.history != null -> composer.moveHistory(-1) + state.text.isEmpty() && !streaming -> onHistory() + else -> return@onPreviewKeyEvent false + } + true + } + Key.DirectionDown -> { + when { + state.slash != null -> composer.moveSlash(1) + state.at != null -> composer.moveAt(1) + state.history != null -> composer.moveHistory(1) + else -> return@onPreviewKeyEvent false + } + true + } + Key.DirectionRight -> { + if (visibleGhost != null && state.canAcceptGhost && + field.selection.start == field.text.length + ) { + onAcceptGhost() + true + } else { + false + } + } + Key.Escape -> { + when { + state.slash != null || state.at != null || state.history != null -> { + composer.closeMenus() + true + } + // Web `ChatInput`: Esc minimizes only an idle, + // empty composer with no menu open. + onMinimize != null && !streaming && state.text.isBlank() -> { + onMinimize() + true + } + else -> false + } + } + else -> false + } + }, + textStyle = androidx.compose.ui.text.TextStyle( + color = colors.text, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + cursorBrush = SolidColor(colors.accent), + visualTransformation = ghostTextTransformation( + suffix = visibleGhost?.suffix, + caret = field.selection.start, + textLength = field.text.length, + color = colors.textDim, + ), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Default), + maxLines = textFieldMaxLines, + decorationBox = { inner -> + Box { + if (field.text.isEmpty()) { + Text( + text = bashHint(state) ?: placeholder, + color = if (state.bashMode) colors.accent else colors.textDim, + fontSize = 14.sp, + ) + } + inner() + } + }, + ) + (dictationState as? DictationState.Live)?.let { live -> + LiveDictationStrip( + state = live, + onStop = dictation::stop, + onCancel = dictation::cancel, + modifier = Modifier.fillMaxWidth(), + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box { + IconButton( + onClick = { plusOpen = true }, + enabled = inputEnabled && !dictationBusy, + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(R.string.composer_add_attachment), + tint = colors.textMuted, + modifier = Modifier.size(20.dp), + ) + } + OmpDropdownMenu(expanded = plusOpen, onDismissRequest = { plusOpen = false }) { + DropdownMenuItem( + text = { Text(stringResource(R.string.composer_attach_file)) }, + onClick = { + plusOpen = false + documentPicker.launch(arrayOf("text/*", "application/json", "application/xml")) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.composer_attach_image)) }, + onClick = { + plusOpen = false + photoPicker.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), + ) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.composer_camera)) }, + onClick = { + plusOpen = false + val target = ComposerAttachments.newCameraTarget(context) + cameraTarget = target + cameraLauncher.launch(target.uri) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.composer_paste_image)) }, + enabled = clipboardHasImage, + onClick = { + plusOpen = false + pasteImage() + }, + ) + HorizontalDivider() + Text( + text = stringResource(R.string.composer_tools), + color = colors.textDim, + fontSize = 11.sp, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + ) + ToolPreset.entries.forEach { preset -> + DropdownMenuItem( + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(toolPresetLabel(preset)) + if (controlsState.toolPreset == preset) { + Spacer(Modifier.width(6.dp)) + Text("✓", color = colors.accent) + } + } + }, + onClick = { + plusOpen = false + controls.onToolPresetChange(preset) + }, + ) + } + } + } + if (dictationState is DictationState.Idle || dictationState is DictationState.Gone) { + MicButton(vm = dictation, enabled = inputEnabled && !dictationBusy) + } + Spacer(Modifier.weight(1f)) + if (bashRunning) { + Text( + text = stringResource(R.string.composer_bash_running), + color = colors.accent, + fontSize = 11.sp, + ) + Spacer(Modifier.width(8.dp)) + } + // Web `ChatInput.tsx` renders exactly one primary action: Stop + // while a run is live and the composer is empty, Queue once a + // typed message is waiting. The text Stop covers that queued + // case (web only offers Esc for it) and disappears as soon as + // Stop becomes the primary action. + val primaryStops = streaming && !state.hasContent && !recording && !liveListening + if (streaming && !stopping && !primaryStops) { + TextButton(onClick = onStop, modifier = Modifier.heightIn(min = 48.dp)) { + Text(stringResource(R.string.chat_stop), fontSize = 11.sp) + } + } + val label = if (primaryStops) stringResource(R.string.chat_stop) else primaryLabel + val actionable = if (primaryStops) !stopping else primaryEnabled + Surface( + color = if (actionable) colors.accentStrong else colors.panel, + contentColor = if (actionable) colors.onAccent else colors.textDim, + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .clickable(enabled = actionable && inputEnabled) { + if (primaryStops) onStop() else primary() + } + .semantics { contentDescription = label }, + ) { + Text( + text = label, + fontSize = 12.sp, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + ) + } + } + } + } + + if (state.bashMode) { + Text( + text = bashHint(state) ?: "", + color = if (state.bashExcluded) colors.textMuted else colors.accent, + fontSize = 11.sp, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 3.dp), + ) + } + + if (showControlsRow) { + ComposerControlsRow( + vm = controls, + streaming = streaming, + modifier = Modifier.padding(top = 4.dp), + ) + } + } + + if (controlsState.contextSheetOpen) { + ContextDetailSheet( + vm = controls, + onDismiss = controls::closeContextSheet, + streaming = streaming, + ) + } + + deleteTarget?.let { (kind, text) -> + OmpAlertDialog( + onDismissRequest = { deleteTarget = null }, + title = { Text(stringResource(R.string.composer_queue_delete_title)) }, + text = { Text(stringResource(R.string.composer_queue_delete_body)) }, + confirmButton = { + TextButton( + onClick = { + deleteTarget = null + onQueueDelete(kind, text) + }, + ) { Text(stringResource(R.string.composer_queue_delete_confirm)) } + }, + dismissButton = { + TextButton(onClick = { deleteTarget = null }) { + Text(stringResource(R.string.composer_cancel)) + } + }, + ) + } +} + +@Composable +private fun AutoRetryBanner(retry: AutoRetry, onAbort: () -> Unit) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = retry.message ?: retryLabel(retry.attempt, retry.maxAttempts), + color = colors.statusWarning, + fontSize = 11.sp, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + TextButton(onClick = onAbort, modifier = Modifier.heightIn(min = 48.dp)) { + Text(stringResource(R.string.composer_auto_retry_stop), fontSize = 11.sp) + } + } +} + +@Composable +private fun QueueBar( + entries: List>, + expanded: Boolean, + onToggle: () -> Unit, + onEdit: (String, String) -> Unit, + onDelete: (String, String) -> Unit, + onSteer: (String) -> Unit, +) { + val colors = LocalOmpColors.current + val shown = if (expanded) entries else entries.take(1) + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 2.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .clickable(onClick = onToggle) + .semantics { role = Role.Button } + .padding(horizontal = 6.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = if (expanded) { + stringResource(R.string.composer_queue_count, entries.size) + } else { + stringResource(R.string.composer_queue_show_all, entries.size) + }, + color = colors.textMuted, + fontSize = 11.sp, + modifier = Modifier.weight(1f), + ) + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(14.dp), + ) + } + shown.forEach { (kind, text) -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 6.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = if (kind == "steering") { + stringResource(R.string.composer_queue_steer) + } else { + stringResource(R.string.composer_queue_follow_up) + }, + color = if (kind == "steering") colors.accent else colors.textDim, + fontSize = 10.sp, + modifier = Modifier.widthIn(min = 52.dp), + ) + Text( + text = text, + color = colors.text, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f).padding(horizontal = 6.dp), + ) + if (kind == "followUp") { + QueueAction( + text = stringResource(R.string.composer_steer), + color = colors.accent, + description = stringResource(R.string.composer_steer_action_desc), + onClick = { onSteer(text) }, + ) + } + QueueAction( + text = stringResource(R.string.composer_queue_edit), + color = colors.textMuted, + onClick = { onEdit(kind, text) }, + ) + QueueAction( + text = stringResource(R.string.composer_queue_delete), + color = colors.statusError, + onClick = { onDelete(kind, text) }, + ) + } + } + } +} + +/** + * One queue-bar action. It carries a button role and the theme's minimum touch + * target: as 11sp text with 2dp padding the promote-to-steer action was ~20dp + * tall, so whether a queued message steered depended on a lucky tap. + */ +@Composable +private fun QueueAction( + text: String, + color: Color, + description: String? = null, + onClick: () -> Unit, +) { + val target = LocalMinimumInteractiveComponentSize.current + Box( + modifier = Modifier + .sizeIn(minWidth = target, minHeight = target) + .clickable(onClick = onClick) + .semantics { + role = Role.Button + if (description != null) contentDescription = description + }, + contentAlignment = Alignment.Center, + ) { + Text(text = text, color = color, fontSize = 11.sp) + } +} + +@Composable +private fun AttachmentStrip( + images: List, + files: List, + reading: Boolean, + onRemoveImage: (Int) -> Unit, + onRemoveFile: (Int) -> Unit, +) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(bottom = 4.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + images.forEachIndexed { index, _ -> + AttachmentChip( + label = stringResource(R.string.chat_attached_image) + " ${index + 1}", + onRemove = { onRemoveImage(index) }, + ) + } + files.forEachIndexed { index, file -> + AttachmentChip(label = file.name, onRemove = { onRemoveFile(index) }) + } + if (reading) { + Text( + text = stringResource(R.string.composer_reading_attachments), + color = colors.textDim, + fontSize = 11.sp, + ) + } + } +} + +@Composable +private fun AttachmentChip(label: String, onRemove: () -> Unit) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .background(colors.backgroundSubtle, RoundedCornerShape(6.dp)) + .padding(start = 6.dp, end = 2.dp, top = 2.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + color = colors.textMuted, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 150.dp), + ) + IconButton(onClick = onRemove) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.composer_remove), + tint = colors.textDim, + modifier = Modifier.size(12.dp), + ) + } + } +} + +@Composable +private fun SlashPalette( + commands: List, + activeIndex: Int, + onPick: (Int) -> Unit, +) { + val colors = LocalOmpColors.current + Column( + Modifier + .fillMaxWidth() + .heightIn(max = 220.dp) + .verticalScroll(rememberScrollState()) + .padding(vertical = 2.dp), + ) { + commands.forEachIndexed { index, command -> + PaletteRow( + label = "/" + command.name, + detail = command.argumentHint ?: command.description, + active = index == activeIndex, + onClick = { onPick(index) }, + ) + } + } +} + +@Composable +private fun AtPalette( + entries: List, + activeIndex: Int, + searchingAll: Boolean, + onPick: (Int) -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + .heightIn(max = 220.dp) + .verticalScroll(rememberScrollState()) + .padding(vertical = 2.dp), + ) { + entries.forEachIndexed { index, entry -> + PaletteRow( + label = entry.path, + detail = if (searchingAll) stringResource(R.string.composer_at_searching_all) else null, + active = index == activeIndex, + onClick = { onPick(index) }, + ) + } + } +} + +@Composable +private fun HistoryPalette( + entries: List, + activeIndex: Int, + onPick: (Int) -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + .heightIn(max = 220.dp) + .verticalScroll(rememberScrollState()) + .padding(vertical = 2.dp), + ) { + entries.forEachIndexed { index, text -> + PaletteRow( + label = text, + detail = null, + active = index == activeIndex, + onClick = { onPick(index) }, + ) + } + } +} + +@Composable +private fun PaletteRow(label: String, detail: String?, active: Boolean, onClick: () -> Unit) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .background(if (active) colors.selected else colors.panel, RoundedCornerShape(6.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 8.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + color = if (active) colors.accent else colors.text, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (!detail.isNullOrBlank()) { + Spacer(Modifier.width(8.dp)) + Text( + text = detail, + color = colors.textDim, + fontSize = 11.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun retryLabel(attempt: Int?, maxAttempts: Int?): String = when { + attempt != null && maxAttempts != null -> + stringResource(R.string.composer_auto_retry, "$attempt/$maxAttempts") + attempt != null -> stringResource(R.string.composer_auto_retry, "$attempt") + else -> stringResource(R.string.composer_auto_retry_plain) +} + +@Composable +private fun bashHint(state: ComposerUiState): String? = when { + !state.bashMode -> null + state.bashExcluded -> stringResource(R.string.composer_bash_excluded_hint) + else -> stringResource(R.string.composer_bash_hint) +} + +@Composable +private fun toolPresetLabel(preset: ToolPreset): String = when (preset) { + ToolPreset.NONE -> stringResource(R.string.composer_tools_none) + ToolPreset.DEFAULT -> stringResource(R.string.composer_tools_default) + ToolPreset.FULL -> stringResource(R.string.composer_tools_full) +} + +private const val MAX_IMAGES_PER_PICK = 10 + diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerController.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerController.kt new file mode 100644 index 000000000..f439a348d --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerController.kt @@ -0,0 +1,573 @@ +package io.github.kahme247.ompweb.ui.composer + +import io.github.kahme247.ompweb.data.ChatDraft +import io.github.kahme247.ompweb.data.ComposerDraftStore +import io.github.kahme247.ompweb.domain.AttachedTextFile +import io.github.kahme247.ompweb.domain.BashInvocation +import io.github.kahme247.ompweb.domain.ComposerImage +import io.github.kahme247.ompweb.domain.MAX_ATTACHED_IMAGE_BYTES +import io.github.kahme247.ompweb.domain.SlashCommandItem +import io.github.kahme247.ompweb.domain.SlashCommandSource +import io.github.kahme247.ompweb.domain.TextAttachmentBudget +import io.github.kahme247.ompweb.domain.applyAtCompletion +import io.github.kahme247.ompweb.domain.buildEntriesFromFiles +import io.github.kahme247.ompweb.domain.buildSlashInsertText +import io.github.kahme247.ompweb.domain.composeMessageWithTextAttachments +import io.github.kahme247.ompweb.domain.describeTextAttachmentSkip +import io.github.kahme247.ompweb.domain.extractAtQuery +import io.github.kahme247.ompweb.domain.extractSlashQuery +import io.github.kahme247.ompweb.domain.filterFileEntries +import io.github.kahme247.ompweb.domain.filterSlashCommands +import io.github.kahme247.ompweb.domain.formatAttachmentBytes +import io.github.kahme247.ompweb.domain.inputHistory +import io.github.kahme247.ompweb.domain.insertAtCursor +import io.github.kahme247.ompweb.domain.parseBashInvocation +import io.github.kahme247.ompweb.domain.selectTextAttachments +import io.github.kahme247.ompweb.domain.validateImageBudget +import io.github.kahme247.ompweb.domain.validateOutgoingPrompt +import io.github.kahme247.ompweb.net.ChatApi +import io.github.kahme247.ompweb.net.FileIndexPage +import io.github.kahme247.ompweb.net.FilesApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** One resolved composer submit (text plus attachments, or a shell command). */ +data class ComposerSubmission( + val raw: String, + val prompt: String, + val images: List, + val hasAttachments: Boolean, + val bash: BashInvocation?, +) + +/** + * The input half of a composer, shared by the chat and the new-session pane: + * text, attachments, draft persistence, the `/` and `@` palettes, `↑` history + * and the `!`/`!!` shell mode. Routing (prompt/steer/queue/bash/builtin) stays + * with the owning view model because it differs per screen. + * + * Hosts feed it a draft key (`` or `new:`), the working + * directory and the session; every draft change is persisted so switching + * sessions or killing the process never loses text. + */ +class ComposerController( + private val drafts: ComposerDraftStore, + private val scope: CoroutineScope, + private val api: ChatApi, + /** The one `/api/file-index` client behind the `@` menu (shared with the explorer). */ + private val files: FilesApi, + private val strings: ComposerStrings, + private val notice: (String) -> Unit, +) { + private val _state = MutableStateFlow(ComposerUiState()) + val state: StateFlow = _state.asStateFlow() + + private var draftKey: String? = null + private var cwd: String? = null + private var sessionId: String? = null + private var running = false + + private var remoteCommands: List = emptyList() + private var dormantSkills: Set = emptySet() + private var fileIndex: FileIndexPage? = null + private var indexCwd: String? = null + private var indexLoadingCwd: String? = null + private var commandsJob: Job? = null + private var commandsRequestedFor: String? = null + private var commandsLoadedFor: String? = null + private var searchJob: Job? = null + private var searchSequence = 0L + private var editId = 0L + + /** + * Invoked after every text/caret change, typed or programmatic. The chat + * host uses it to feed word prediction; the new-session pane leaves it + * unset. Set before [bind]. + */ + var textObserver: ((String, Int) -> Unit)? = null + + init { + scope.launch { + drafts.recoveries.collect { event -> + if (event.key.isNotEmpty() && event.key == draftKey) applyStoredDraft() + } + } + } + + val currentDraftKey: String? get() = draftKey + + /** Chat: ``; new session: `new:`. Resets state and reloads the stored draft. */ + fun bind(key: String?, workingDirectory: String?, session: String?) { + val keyChanged = key != draftKey + draftKey = key + sessionId = session + if (workingDirectory != cwd) { + cwd = workingDirectory + fileIndex = null + indexCwd = null + indexLoadingCwd = null + dormantSkills = emptySet() + if (workingDirectory != null) loadSkills(workingDirectory) + } + val stored = key?.let(drafts::getDraft) ?: ChatDraft() + _state.value = ComposerUiState( + draft = stored, + caret = stored.value.length, + edit = nextEdit(stored.value, stored.value.length), + ) + if (keyChanged) { + remoteCommands = emptyList() + commandsJob?.cancel() + commandsRequestedFor = null + commandsLoadedFor = null + } + if (session != null) loadCommands(session) + publishText() + } + + /** omp's roster (engine state / live `available_commands_update`). */ + fun setRemoteCommands(commands: List) { + if (commands == remoteCommands) return + remoteCommands = commands + refreshMenus() + } + + /** Streaming hides the client built-ins from the palette, exactly like the web composer. */ + fun setRunning(running: Boolean) { + if (this.running == running) return + this.running = running + refreshMenus() + } + + // ------------------------------------------------------------- text input + + fun onTextChanged(text: String, caret: Int) { + val position = caret.coerceIn(0, text.length) + _state.value = _state.value.copy( + draft = _state.value.draft.copy(value = text), + caret = position, + bashMode = composerBashMode(text), + bashExcluded = text.trimStart().startsWith("!!"), + edit = null, + history = null, + ) + persist() + refreshMenus() + publishText() + } + + /** Selection-only move (palette stays in sync with the caret). */ + fun onCaretMoved(caret: Int) { + val position = caret.coerceIn(0, _state.value.text.length) + if (position == _state.value.caret) return + _state.value = _state.value.copy(caret = position) + refreshMenus() + publishText() + } + + fun insertAtCaret(insert: String) { + val current = _state.value + val insertion = insertAtCursor(current.text, current.caret, insert) + applyText(insertion.text, insertion.caret) + } + + /** Put text back only when the composer is empty (web `insertIfEmpty`). */ + fun insertIfEmpty(text: String) { + if (_state.value.text.isNotBlank()) return + applyText(text, text.length) + } + + fun setText(text: String) = applyText(text, text.length) + + /** Replace the whole field with a computed edit (accepted ghost, restore). */ + fun replaceText(text: String, caret: Int) = applyText(text, caret) + + fun clear() { + _state.value = ComposerUiState(edit = nextEdit("", 0)) + draftKey?.let(drafts::clearDraft) + publishText() + } + + private fun applyText(text: String, caret: Int) { + val position = caret.coerceIn(0, text.length) + _state.value = _state.value.copy( + draft = _state.value.draft.copy(value = text), + caret = position, + bashMode = composerBashMode(text), + bashExcluded = text.trimStart().startsWith("!!"), + edit = nextEdit(text, position), + ) + persist() + refreshMenus() + publishText() + } + + private fun publishText() { + val current = _state.value + textObserver?.invoke(current.text, current.caret) + } + + private fun nextEdit(text: String, caret: Int): ComposerEdit = ComposerEdit(++editId, text, caret) + + private fun persist() { + val key = draftKey ?: return + val draft = _state.value.draft + if (draft.isEmpty()) drafts.clearDraft(key) else drafts.setDraft(key, draft) + } + + /** Reload after [DraftStore.recoverDraft] merged queue text back in. */ + fun applyStoredDraft() { + val key = draftKey ?: return + val stored = drafts.getDraft(key) ?: ChatDraft() + _state.value = _state.value.copy( + draft = stored, + caret = stored.value.length, + edit = nextEdit(stored.value, stored.value.length), + slash = null, + at = null, + history = null, + ) + publishText() + } + + // -------------------------------------------------------------- palettes + + private fun refreshMenus() { + refreshSlash() + refreshAt() + } + + private fun refreshSlash() { + val current = _state.value + val query = extractSlashQuery(current.text.substring(0, current.caret)) + if (query == null) { + if (current.slash != null) _state.value = current.copy(slash = null) + return + } + if (sessionId != null && remoteCommands.isEmpty()) loadCommands(sessionId!!) + val pool = if (query.mode == io.github.kahme247.ompweb.domain.SlashMode.LEADING) { + (if (running) emptyList() else strings.builtinCommands) + remoteCommands + } else { + remoteCommands + } + val filtered = filterSlashCommands(pool, query.query, query.mode == io.github.kahme247.ompweb.domain.SlashMode.MID_PROMPT) + val ordered = if (query.mode == io.github.kahme247.ompweb.domain.SlashMode.LEADING) dormantSkillsLast(filtered) else filtered + val active = current.slash?.activeIndex?.coerceIn(0, (ordered.size - 1).coerceAtLeast(0)) ?: 0 + _state.value = current.copy( + slash = SlashMenuUi(query, ordered, active), + at = null, + history = null, + ) + } + + private fun dormantSkillsLast(commands: List): List { + if (dormantSkills.isEmpty()) return commands + val (dormant, active) = commands.partition { command -> + command.source == SlashCommandSource.SKILL && + command.name.removePrefix("skill:") in dormantSkills + } + return active + dormant + } + + fun moveSlash(delta: Int) { + val menu = _state.value.slash ?: return + if (menu.commands.isEmpty()) return + val next = Math.floorMod(menu.activeIndex + delta, menu.commands.size) + _state.value = _state.value.copy(slash = menu.copy(activeIndex = next)) + } + + fun setSlashActive(index: Int) { + val menu = _state.value.slash ?: return + if (menu.commands.isEmpty()) return + _state.value = _state.value.copy(slash = menu.copy(activeIndex = index.coerceIn(0, menu.commands.size - 1))) + } + + fun setAtActive(index: Int) { + val menu = _state.value.at ?: return + if (menu.entries.isEmpty()) return + _state.value = _state.value.copy(at = menu.copy(activeIndex = index.coerceIn(0, menu.entries.size - 1))) + } + + fun setHistoryActive(index: Int) { + val menu = _state.value.history ?: return + if (menu.entries.isEmpty()) return + _state.value = _state.value.copy( + history = menu.copy(activeIndex = index.coerceIn(0, menu.entries.size - 1)), + ) + } + + /** Surface a composer notice through the owner's channel. */ + fun showNotice(message: String) { + if (message.isNotBlank()) notice(message) + } + + /** Accept the highlighted palette row (Tab/Enter/direct tap). */ + fun acceptSlash(): Boolean { + val menu = _state.value.slash ?: return false + val command = menu.active ?: return false + val current = _state.value + val start = menu.query.start.coerceIn(0, current.text.length) + val caret = current.caret.coerceIn(0, current.text.length) + val insert = buildSlashInsertText(command.name, menu.query.mode) + val next = current.text.substring(0, start) + insert + current.text.substring(caret) + _state.value = current.copy(slash = null, at = null, history = null) + applyText(next, start + insert.length) + return true + } + + private fun refreshAt() { + val current = _state.value + val query = extractAtQuery(current.text.substring(0, current.caret)) + if (query == null) { + if (current.at != null) _state.value = current.copy(at = null) + return + } + val directory = cwd ?: run { + _state.value = current.copy(at = null) + return + } + val cached = fileIndex + if (cached == null) { + _state.value = current.copy(at = AtMenuUi(query, emptyList(), searchingAll = true), slash = null, history = null) + loadIndex(directory) + return + } + val entries = filterFileEntries(buildEntriesFromFiles(cached.files), query.query) + _state.value = current.copy( + at = AtMenuUi(query, entries, activeIndex = 0, searchingAll = cached.truncated && query.query.isNotEmpty()), + slash = null, + history = null, + ) + if (cached.truncated && query.query.isNotEmpty()) { + searchServer(directory, query.query) + } + } + + fun moveAt(delta: Int) { + val menu = _state.value.at ?: return + if (menu.entries.isEmpty()) return + val next = Math.floorMod(menu.activeIndex + delta, menu.entries.size) + _state.value = _state.value.copy(at = menu.copy(activeIndex = next)) + } + + fun acceptAt(): Boolean { + val menu = _state.value.at ?: return false + val entry = menu.active ?: return false + val current = _state.value + val next = applyAtCompletion(current.text, current.caret, menu.query, entry) + _state.value = current.copy(at = null, slash = null, history = null) + applyText(next.text, next.caret) + return true + } + + private fun loadCommands(session: String) { + if (commandsRequestedFor == session && (commandsJob?.isActive == true || commandsLoadedFor == session)) { + return + } + commandsRequestedFor = session + commandsJob = scope.launch { + val commands = try { + api.commands(session) + } catch (_: Exception) { + emptyList() + } + if (commands.isNotEmpty()) { + commandsLoadedFor = session + if (session == sessionId) setRemoteCommands(commands) + } + } + } + + private fun loadSkills(directory: String) { + scope.launch { + val names = try { + api.skills(directory).skills.filter { it.disableModelInvocation }.map { it.name }.toSet() + } catch (_: Exception) { + emptySet() + } + if (directory == cwd) { + dormantSkills = names + refreshMenus() + } + } + } + + private fun loadIndex(directory: String) { + if (indexCwd == directory && (fileIndex != null || indexLoadingCwd == directory)) return + indexCwd = directory + indexLoadingCwd = directory + scope.launch { + val listing = try { + files.index(directory) + } catch (_: Exception) { + FileIndexPage() + } + if (indexLoadingCwd == directory) indexLoadingCwd = null + if (directory == cwd) { + fileIndex = listing + refreshAt() + } + } + } + + private fun searchServer(directory: String, query: String) { + val sequence = ++searchSequence + searchJob?.cancel() + searchJob = scope.launch { + delay(FILE_SEARCH_DEBOUNCE_MS) + val listing = try { + files.index(directory, query = query, filesOnly = true) + } catch (_: Exception) { + return@launch + } + if (sequence != searchSequence || directory != cwd) return@launch + val menu = _state.value.at ?: return@launch + if (menu.query.query != query) return@launch + _state.value = _state.value.copy(at = menu.copy(entries = listing.matches, activeIndex = 0, searchingAll = false)) + } + } + + // --------------------------------------------------------------- history + + fun openHistory(userTexts: List) { + val entries = inputHistory(userTexts, HISTORY_LIMIT).asReversed() + if (entries.isEmpty()) return + _state.value = _state.value.copy( + history = HistoryMenuUi(entries, 0), + slash = null, + at = null, + ) + } + + fun moveHistory(delta: Int) { + val menu = _state.value.history ?: return + if (menu.entries.isEmpty()) return + val next = Math.floorMod(menu.activeIndex + delta, menu.entries.size) + _state.value = _state.value.copy(history = menu.copy(activeIndex = next)) + } + + fun acceptHistory(): Boolean { + val menu = _state.value.history ?: return false + val text = menu.active ?: return false + _state.value = _state.value.copy(history = null) + applyText(text, text.length) + return true + } + + fun closeMenus() { + val current = _state.value + if (current.slash == null && current.at == null && current.history == null) return + _state.value = current.copy(slash = null, at = null, history = null) + } + + // ----------------------------------------------------------- attachments + + fun addImages(images: List) { + if (images.isEmpty()) return + var accepted = _state.value.images + var error: String? = null + var skipped = 0 + for (image in images) { + val candidate = accepted + image + val budgetError = validateImageBudget(candidate) + if (budgetError != null) { + skipped++ + error = budgetError + continue + } + accepted = candidate + } + if (accepted.size != _state.value.images.size) { + setDraft(_state.value.draft.copy(images = accepted)) + } + if (skipped > 0) { + val skippedNotice = String.format( + strings.imageSkipped, + skipped, + formatAttachmentBytes(MAX_ATTACHED_IMAGE_BYTES), + ) + notice(if (skipped == images.size) error ?: skippedNotice else skippedNotice) + } + } + + fun addTextFiles(files: List) { + if (files.isEmpty()) return + val current = _state.value.files + val selection = selectTextAttachments( + files, + { it.size }, + TextAttachmentBudget( + usedBytes = current.sumOf { it.size }, + usedSlots = current.size, + ), + ) + if (selection.accepted.isNotEmpty()) { + setDraft(_state.value.draft.copy(files = current + selection.accepted)) + } + val skip = describeTextAttachmentSkip(selection) + if (skip != null) { + notice(skip) + } else if (files.size > selection.accepted.size) { + notice(String.format(strings.filesSkipped, files.size - selection.accepted.size)) + } + } + + fun removeImage(index: Int) { + val images = _state.value.images + if (index !in images.indices) return + setDraft(_state.value.draft.copy(images = images.filterIndexed { position, _ -> position != index })) + } + + fun removeFile(index: Int) { + val files = _state.value.files + if (index !in files.indices) return + setDraft(_state.value.draft.copy(files = files.filterIndexed { position, _ -> position != index })) + } + + fun setReadingAttachments(reading: Boolean) { + if (_state.value.readingAttachments == reading) return + _state.value = _state.value.copy(readingAttachments = reading) + } + + private fun setDraft(draft: ChatDraft) { + _state.value = _state.value.copy(draft = draft) + persist() + } + + // ---------------------------------------------------------------- submit + + /** + * Resolve the composer into a submission: shell commands when the draft + * starts with `!`/`!!`, otherwise the prompt with text attachments inlined. + * Returns `null` for an empty draft or when the prompt exceeds the request + * budget (the size error is raised as a notice and the draft is kept). + */ + fun resolve(overrideText: String? = null): ComposerSubmission? { + val current = _state.value + val raw = (overrideText ?: current.text).trim() + val images = current.images + val files = current.files + if (raw.isEmpty() && images.isEmpty() && files.isEmpty()) return null + val bash = if (images.isEmpty()) parseBashInvocation(raw, files.isNotEmpty()) else null + if (bash != null) { + return ComposerSubmission(raw, raw, images, files.isNotEmpty(), bash) + } + val prompt = composeMessageWithTextAttachments(raw, files) + val error = validateOutgoingPrompt(prompt, images) + if (error != null) { + notice(error) + return null + } + return ComposerSubmission(raw, prompt, images, files.isNotEmpty(), null) + } + + private companion object { + const val HISTORY_LIMIT = 50 + const val FILE_SEARCH_DEBOUNCE_MS = 150L + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerState.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerState.kt new file mode 100644 index 000000000..65393c41d --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerState.kt @@ -0,0 +1,62 @@ +package io.github.kahme247.ompweb.ui.composer + +import io.github.kahme247.ompweb.data.ChatDraft +import io.github.kahme247.ompweb.domain.AtQuery +import io.github.kahme247.ompweb.domain.FileIndexEntry +import io.github.kahme247.ompweb.domain.SlashCommandItem +import io.github.kahme247.ompweb.domain.SlashQuery + +/** A programmatic rewrite of the composer text (suggestion, history, restore). */ +data class ComposerEdit(val id: Long, val text: String, val caret: Int) + +data class SlashMenuUi( + val query: SlashQuery, + val commands: List, + val activeIndex: Int = 0, +) { + val active: SlashCommandItem? get() = commands.getOrNull(activeIndex) +} + +data class AtMenuUi( + val query: AtQuery, + val entries: List, + val activeIndex: Int = 0, + val searchingAll: Boolean = false, +) { + val active: FileIndexEntry? get() = entries.getOrNull(activeIndex) +} + +/** `↑` history palette: newest first, [activeIndex] indexes [entries]. */ +data class HistoryMenuUi( + val entries: List, + val activeIndex: Int = 0, +) { + val active: String? get() = entries.getOrNull(activeIndex) +} + +/** + * Everything a composer shows: the draft (text + attachments), the open + * autocomplete/history palettes and the `!`/`!!` shell mode. Shared by the + * chat and the new-session pane so both behave identically. + */ +data class ComposerUiState( + val draft: ChatDraft = ChatDraft(), + val caret: Int = 0, + val slash: SlashMenuUi? = null, + val at: AtMenuUi? = null, + val history: HistoryMenuUi? = null, + val bashMode: Boolean = false, + val bashExcluded: Boolean = false, + val readingAttachments: Boolean = false, + /** Last programmatic text rewrite; the field applies it when the id changes. */ + val edit: ComposerEdit? = null, +) { + val text: String get() = draft.value + val images get() = draft.images + val files get() = draft.files + val hasContent: Boolean get() = draft.value.isNotBlank() || draft.hasAttachments + val canAcceptGhost: Boolean get() = slash == null && at == null && history == null && !bashMode +} + +/** `!command` / `!!command` shell mode (web `isBashCommand`). */ +fun composerBashMode(text: String): Boolean = text.trimStart().startsWith("!") diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerStrings.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerStrings.kt new file mode 100644 index 000000000..c80dc2763 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/ComposerStrings.kt @@ -0,0 +1,128 @@ +package io.github.kahme247.ompweb.ui.composer + +import android.content.Context +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.SlashCommandItem +import io.github.kahme247.ompweb.domain.SlashCommandSource + +/** + * Localized strings the composer view models need but cannot read themselves + * (they hold no `Context`). Resolved once in `AppContainer` so the VMs stay + * plain JVM code. + */ +data class ComposerStrings( + val queuedWhileShellRunning: String = "", + val queueRestoreUncertain: String = "", + val queueDiscardFailed: String = "", + val commandRequiresArgs: String = "Usage: %1\$s %2\$s", + val advisorDisabled: String = "", + val noSession: String = "", + /** A9: why a Send on an unvalidated workspace does nothing. */ + val cwdInvalid: String = "", + val nameUsage: String = "", + val sessionRenamed: String = "Session renamed to %1\$s", + val noMessageToCopy: String = "", + val copiedLastMessage: String = "", + val compactedContext: String = "", + val reloadedResources: String = "", + val unknownCommand: String = "/%1\$s is not a command this session can run — it was sent as plain text.", + val queuedRemovalUnavailable: String = "", + val queuedPromotionUnavailable: String = "", + val shellSessionFailed: String = "", + /** The submit-time live-state read failed; the draft was kept. */ + val liveStateUnknown: String = "", + /** The chat was left (or signed out) while the submit waited; nothing was sent, the draft stays. */ + val submitAbandoned: String = "", + /** Another omp process runs the chat: the send was refused, the draft stays. */ + val foreignSendBlocked: String = "", + /** Another omp process runs the chat: a non-send command was refused (409). */ + val foreignCommandBlocked: String = "", + /** `/btw` refuses attachments; the draft (and its files) stay in the field. */ + val btwAttachmentsUnsupported: String = "", + val imageSkipped: String = "%1\$d image(s) skipped (up to %2\$s each).", + val filesSkipped: String = "%1\$d file(s) skipped.", + /** Localized `[hint]` per web-command key (`WebSlashCommand.argumentHintKey`). */ + val usageHints: Map = emptyMap(), + val builtinCommands: List = emptyList(), +) { + fun requiresArgs(command: String, argumentHintKey: String): String = + String.format(commandRequiresArgs, command, usageHints[argumentHintKey] ?: argumentHintKey) + + fun renamed(name: String): String = String.format(sessionRenamed, name) + + fun unknownCommand(name: String): String = String.format(unknownCommand, name) + + companion object { + /** The client-owned palette rows (`BUILTIN_SLASH_COMMAND_NAMES`, localized descriptions). */ + fun builtinPalette(context: Context): List { + val app = context.applicationContext + fun item(name: String, desc: Int, hint: Int? = null) = SlashCommandItem( + name = name, + description = app.getString(desc), + argumentHint = hint?.let(app::getString), + source = SlashCommandSource.BUILTIN, + ) + return listOf( + item("goal", R.string.composer_cmd_goal_desc, R.string.composer_cmd_goal_arg), + item("plan", R.string.composer_cmd_plan_desc, R.string.composer_cmd_plan_arg), + item("review", R.string.composer_cmd_review_desc, R.string.composer_cmd_review_arg), + item("fix", R.string.composer_cmd_fix_desc, R.string.composer_cmd_fix_arg), + item("test", R.string.composer_cmd_test_desc, R.string.composer_cmd_test_arg), + item("explain", R.string.composer_cmd_explain_desc, R.string.composer_cmd_explain_arg), + item("simplify", R.string.composer_cmd_simplify_desc, R.string.composer_cmd_simplify_arg), + item("commit", R.string.composer_cmd_commit_desc, R.string.composer_cmd_commit_arg), + item("advisor", R.string.composer_cmd_advisor_desc, R.string.composer_cmd_advisor_arg), + item("loop", R.string.composer_cmd_loop_desc, R.string.composer_cmd_loop_arg), + item("btw", R.string.composer_cmd_btw_desc, R.string.composer_cmd_btw_arg), + item("compact", R.string.composer_cmd_compact_desc), + item("reload", R.string.composer_cmd_reload_desc), + item("name", R.string.composer_cmd_name_desc), + item("session", R.string.composer_cmd_session_desc), + item("copy", R.string.composer_cmd_copy_desc), + ) + } + + fun from(context: Context): ComposerStrings { + val app = context.applicationContext + return ComposerStrings( + queuedWhileShellRunning = app.getString(R.string.composer_notice_queued_while_shell), + queueRestoreUncertain = app.getString(R.string.composer_notice_queue_restore_uncertain), + queueDiscardFailed = app.getString(R.string.composer_notice_queue_discard_failed), + commandRequiresArgs = app.getString(R.string.composer_notice_command_requires_args), + advisorDisabled = app.getString(R.string.composer_notice_advisor_disabled), + noSession = app.getString(R.string.composer_notice_no_session), + cwdInvalid = app.getString(R.string.cwd_invalid), + nameUsage = app.getString(R.string.composer_notice_name_usage), + sessionRenamed = app.getString(R.string.composer_notice_session_renamed), + noMessageToCopy = app.getString(R.string.composer_notice_no_message_copy), + copiedLastMessage = app.getString(R.string.composer_notice_copied), + compactedContext = app.getString(R.string.composer_notice_compacted), + reloadedResources = app.getString(R.string.composer_notice_reloaded), + unknownCommand = app.getString(R.string.composer_notice_unknown_command), + queuedRemovalUnavailable = app.getString(R.string.composer_notice_queued_removal_unavailable), + queuedPromotionUnavailable = app.getString(R.string.composer_notice_queued_promotion_unavailable), + shellSessionFailed = app.getString(R.string.composer_notice_shell_session_failed), + liveStateUnknown = app.getString(R.string.composer_notice_live_state_unknown), + submitAbandoned = app.getString(R.string.composer_notice_submit_abandoned), + foreignSendBlocked = app.getString(R.string.composer_notice_foreign_owner_blocked), + foreignCommandBlocked = app.getString(R.string.composer_notice_foreign_owner_command_blocked), + btwAttachmentsUnsupported = app.getString(R.string.btw_attachments_unsupported), + imageSkipped = app.getString(R.string.composer_attach_image_skipped), + filesSkipped = app.getString(R.string.composer_attach_file_skipped), + usageHints = mapOf( + "composer_cmd_goal_arg" to app.getString(R.string.composer_cmd_goal_arg), + "composer_cmd_plan_arg" to app.getString(R.string.composer_cmd_plan_arg), + "composer_cmd_review_arg" to app.getString(R.string.composer_cmd_review_arg), + "composer_cmd_fix_arg" to app.getString(R.string.composer_cmd_fix_arg), + "composer_cmd_test_arg" to app.getString(R.string.composer_cmd_test_arg), + "composer_cmd_explain_arg" to app.getString(R.string.composer_cmd_explain_arg), + "composer_cmd_simplify_arg" to app.getString(R.string.composer_cmd_simplify_arg), + "composer_cmd_commit_arg" to app.getString(R.string.composer_cmd_commit_arg), + "composer_cmd_advisor_arg" to app.getString(R.string.composer_cmd_advisor_arg), + "composer_cmd_loop_arg" to app.getString(R.string.composer_cmd_loop_arg), + ), + builtinCommands = builtinPalette(app), + ) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/MinimizedComposer.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/MinimizedComposer.kt new file mode 100644 index 000000000..fca8a4141 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/MinimizedComposer.kt @@ -0,0 +1,121 @@ +package io.github.kahme247.ompweb.ui.composer + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors + +/** + * The chevron above the composer that collapses it into the one-line pill + * (web `ChatWindow` minimized-composer toggle). An [IconButton], so the touch + * target is the platform's 48 dp even though the glyph is 16 dp. + */ +@Composable +internal fun MinimizeComposerButton(onMinimize: () -> Unit, modifier: Modifier = Modifier) { + val colors = LocalOmpColors.current + Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + IconButton(onClick = onMinimize) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource(R.string.composer_minimize), + tint = colors.textDim, + modifier = Modifier.size(16.dp), + ) + } + } +} + +/** + * The slim bar that replaces the full composer while minimized (web + * `MinimizedComposerBar`): one line with the running status or the draft + * preview, an expand action and Stop while the agent is running. + */ +@Composable +internal fun MinimizedComposerBar( + draftPreview: String, + hasAttachments: Boolean, + placeholder: String, + statusText: String?, + streaming: Boolean, + onExpand: () -> Unit, + onStop: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + Surface( + modifier = modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 2.dp), + color = colors.background, + contentColor = colors.text, + shape = RoundedCornerShape(10.dp), + border = BorderStroke(1.dp, colors.border), + ) { + Row( + modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .clickable(onClick = onExpand) + // One TalkBack focus stop: expand label + status/preview. + .semantics(mergeDescendants = true) {} + .padding(start = 12.dp, end = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowUp, + contentDescription = stringResource(R.string.composer_expand), + tint = colors.textDim, + modifier = Modifier.size(16.dp), + ) + Text( + text = statusText ?: draftPreview.ifBlank { placeholder }, + color = if (statusText == null && draftPreview.isNotBlank()) colors.text else colors.textMuted, + fontSize = 13.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (hasAttachments) { + Text( + text = stringResource(R.string.composer_minimized_attachments), + color = colors.textDim, + fontSize = 11.sp, + maxLines = 1, + ) + } + } + if (streaming) { + TextButton(onClick = onStop, modifier = Modifier.heightIn(min = 48.dp).padding(end = 6.dp)) { + Text(stringResource(R.string.chat_stop), fontSize = 11.sp, color = colors.accent) + } + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ComposerControlsRow.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ComposerControlsRow.kt new file mode 100644 index 000000000..1525cab65 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ComposerControlsRow.kt @@ -0,0 +1,460 @@ +package io.github.kahme247.ompweb.ui.composer.controls + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.ContextTone +import io.github.kahme247.ompweb.domain.ContextUsageInfo +import io.github.kahme247.ompweb.domain.UsageLimit +import io.github.kahme247.ompweb.domain.contextTone +import io.github.kahme247.ompweb.domain.formatAllowancePercent +import io.github.kahme247.ompweb.domain.formatSlowModeResetClock +import io.github.kahme247.ompweb.domain.usageLimitLabel +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpDropdownMenu +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily +import kotlin.math.round + +/** + * The composer's controls row: model picker trigger, reasoning ladder, Fast / + * Slow / Advisor toggles, the provider usage-limit badge and the context ring + * that opens [ContextDetailSheet]. + * + * Everything is read from [ComposerControlsViewModel]; the row never issues a + * server call itself. While [streaming] the model and reasoning controls are + * disabled, exactly like the web (`modelSelectorDisabled = isStreaming`). + */ +@Composable +fun ComposerControlsRow( + vm: ComposerControlsViewModel, + streaming: Boolean, + modifier: Modifier = Modifier, +) { + val state by vm.state.collectAsStateWithLifecycle() + val colors = LocalOmpColors.current + var pickerOpen by rememberSaveable { mutableStateOf(false) } + + Row( + modifier = modifier.fillMaxWidth().padding(horizontal = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier + .weight(1f) + .horizontalScroll(rememberScrollState()), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + RowControl( + onClick = { pickerOpen = true }, + enabled = !streaming && !(state.modelsLoading && state.models.isEmpty()), + active = false, + contentDescription = stringResource(R.string.controls_change_model), + ) { + Text( + text = state.modelLabel ?: stringResource(R.string.controls_select_model), + color = if (state.modelLabel != null) colors.textMuted else colors.textDim, + fontSize = 11.5.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 150.dp), + ) + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(12.dp), + ) + } + + if (state.thinkingSupported) { + ThinkingLevelControl(state = state, streaming = streaming, onSelect = vm::onThinkingLevelChange) + } + + if (state.fastModeSupported) { + val inert = state.fastModeEnabled && state.fastModeActive == false + RowControl( + onClick = { if (!streaming) vm.onFastModeChange(!state.fastModeEnabled) }, + enabled = !streaming, + active = state.fastModeEnabled, + tint = if (inert) colors.statusWarning else null, + contentDescription = stringResource(R.string.controls_fast_title), + ) { + Text( + text = stringResource(R.string.controls_fast), + color = when { + inert -> colors.statusWarning + state.fastModeEnabled -> colors.accent + else -> colors.textMuted + }, + fontSize = 11.5.sp, + fontWeight = FontWeight.Medium, + ) + } + } + + if (state.slowModeSupported) { + RowControl( + onClick = { if (!streaming) vm.onSlowModeChange(!state.slowModeEnabled) }, + enabled = !streaming, + active = state.slowModeEnabled, + contentDescription = stringResource( + if (state.slowModeScope == "global") { + R.string.controls_slow_title_global + } else { + R.string.controls_slow_title_session + }, + ), + ) { + Text( + text = stringResource(R.string.controls_slow), + color = if (state.slowModeEnabled) colors.accent else colors.textMuted, + fontSize = 11.5.sp, + fontWeight = FontWeight.Medium, + ) + } + } + + RowControl( + onClick = { vm.onAdvisorChange(!state.advisorEnabled) }, + enabled = true, + active = state.advisorEnabled, + contentDescription = stringResource( + if (state.advisorEnabled) { + R.string.controls_advisor_disable + } else { + R.string.controls_advisor_enable + }, + ), + ) { + Text( + text = stringResource(R.string.controls_advisor), + color = if (state.advisorEnabled) colors.accent else colors.textMuted, + fontSize = 11.5.sp, + fontWeight = FontWeight.Medium, + ) + } + + state.usageLimit?.let { limit -> + UsageLimitBadge(limit = limit, modifier = Modifier.padding(start = 2.dp)) + } + } + + ContextRingButton( + usage = state.shownContextUsage, + compacting = state.isCompacting, + onClick = vm::openContextSheet, + ) + } + + if (pickerOpen) { + ModelPickerDialog( + state = state, + onDismiss = { pickerOpen = false }, + onSelect = { provider, modelId -> + pickerOpen = false + vm.onSelectModel(provider, modelId) + }, + ) + } +} + +/** The reasoning ladder: trigger label + a card menu, hidden by the caller when unsupported. */ +@Composable +private fun ThinkingLevelControl( + state: ComposerControlsState, + streaming: Boolean, + onSelect: (String) -> Unit, +) { + val colors = LocalOmpColors.current + var open by remember { mutableStateOf(false) } + Box { + RowControl( + onClick = { if (!streaming) open = true }, + enabled = !streaming, + active = open, + contentDescription = stringResource(R.string.controls_change_reasoning), + ) { + Text( + text = levelLabel(state.thinkingLevel), + color = colors.textMuted, + fontSize = 11.5.sp, + maxLines = 1, + ) + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(12.dp), + ) + } + OmpDropdownMenu( + expanded = open, + onDismissRequest = { open = false }, + containerColor = colors.panel, + modifier = Modifier.widthIn(min = 170.dp), + ) { + Text( + text = stringResource(R.string.controls_reasoning), + color = colors.textDim, + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + ) + state.thinkingLevelOptions.forEach { level -> + val active = state.thinkingLevel == level + DropdownMenuItem( + text = { + Text( + text = levelLabel(level), + color = if (active) colors.text else colors.textMuted, + fontSize = 12.sp, + ) + }, + leadingIcon = if (active) { + { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + tint = colors.accent, + modifier = Modifier.size(13.dp), + ) + } + } else { + null + }, + onClick = { + open = false + if (!active && !streaming) onSelect(level) + }, + ) + } + Text( + text = stringResource(R.string.controls_applies_next_prompt), + color = colors.textDim, + fontSize = 10.sp, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + ) + } + } +} + +@Composable +private fun UsageLimitBadge(limit: UsageLimit, modifier: Modifier = Modifier) { + val colors = LocalOmpColors.current + val now = remember(limit) { System.currentTimeMillis() } + val label = usageLimitLabel(limit, now) + val text = when (label) { + is io.github.kahme247.ompweb.domain.UsageLimitLabel.LowPriority -> { + val time = label.resetAtMillis?.let(::resetClock) + val percent = label.allowancePercent?.let(::formatAllowancePercent) + when { + time != null && percent != null -> + stringResource(R.string.controls_usage_low_priority_allowance, time, percent) + + time != null -> stringResource(R.string.controls_usage_low_priority, time) + percent != null -> + stringResource(R.string.controls_usage_low_priority_allowance_only, percent) + + else -> stringResource(R.string.controls_usage_low_priority_no_reset) + } + } + + is io.github.kahme247.ompweb.domain.UsageLimitLabel.WrapUp -> { + val time = label.resetAtMillis?.let(::resetClock) + when { + label.extraUsage -> stringResource(R.string.controls_usage_wrap_up_extra) + time != null -> stringResource(R.string.controls_usage_wrap_up_reset, time) + else -> stringResource(R.string.controls_usage_wrap_up) + } + } + } + Surface( + color = colors.statusWarning.copy(alpha = 0.12f), + shape = RoundedCornerShape(7.dp), + modifier = modifier.heightIn(min = 24.dp), + ) { + Box(modifier = Modifier.padding(horizontal = 8.dp), contentAlignment = Alignment.Center) { + Text( + text = text, + color = colors.statusWarning, + fontSize = 11.sp, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +/** The session-information ring: percent inside, compacting spinner, tap opens the sheet. */ +@Composable +private fun ContextRingButton( + usage: ContextUsageInfo?, + compacting: Boolean, + onClick: () -> Unit, +) { + val colors = LocalOmpColors.current + val percent = usage?.percent + val tone = when (contextTone(percent)) { + ContextTone.ERROR -> colors.statusError + ContextTone.WARNING -> colors.statusWarning + ContextTone.NORMAL -> colors.textDim + } + Box( + modifier = Modifier + .size(30.dp) + .clip(CircleShape) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + if (compacting) { + CircularProgressIndicator( + modifier = Modifier.size(15.dp), + strokeWidth = 2.dp, + color = colors.accent, + ) + } else { + Canvas(Modifier.fillMaxSize()) { + val stroke = 2.5.dp.toPx() + val radius = (size.minDimension - stroke) / 2f + val center = Offset(size.width / 2f, size.height / 2f) + drawCircle( + color = colors.border, + radius = radius, + center = center, + style = Stroke(width = stroke), + ) + if (percent != null) { + val sweep = 360f * (percent.coerceIn(0.0, 100.0) / 100.0).toFloat() + drawArc( + color = tone, + startAngle = -90f, + sweepAngle = sweep, + useCenter = false, + topLeft = Offset( + center.x - radius, + center.y - radius, + ), + size = Size(radius * 2, radius * 2), + style = Stroke(width = stroke, cap = StrokeCap.Round), + ) + } + } + if (percent != null) { + Text( + text = round(percent).toInt().toString(), + color = tone, + fontSize = 7.sp, + fontWeight = FontWeight.Bold, + fontFamily = OmpMonoFontFamily, + ) + } + } + } +} + +/** One flat control: a compact tappable label with an active tint. */ +@Composable +private fun RowControl( + onClick: () -> Unit, + enabled: Boolean, + active: Boolean, + contentDescription: String, + tint: Color? = null, + content: @Composable () -> Unit, +) { + val colors = LocalOmpColors.current + Surface( + color = if (active) colors.selected else Color.Transparent, + shape = RoundedCornerShape(7.dp), + modifier = Modifier + // 48 dp: the web row is compact, but the Android touch target and + // a 200 % font scale need the full minimum height. + .heightIn(min = 48.dp) + .clickable( + enabled = enabled, + onClickLabel = contentDescription, + onClick = onClick, + ), + ) { + Row( + modifier = Modifier.padding(horizontal = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + ) { + content() + } + } +} + +/** Web `thinkingDisplayLabel`: the level name, headline-cased. */ +private fun levelLabel(level: String): String = + level.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + +private fun resetClock(millis: Long): String = + formatSlowModeResetClock(millis, System.currentTimeMillis()) + +/** A provider avatar with the provider's initials (`ProviderBadge`). */ +@Composable +internal fun ProviderBadge(id: String, size: Int = 16) { + val colors = LocalOmpColors.current + Box( + modifier = Modifier + .size(size.dp) + .border(1.dp, colors.border, RoundedCornerShape(4.dp)) + .background(colors.background), + contentAlignment = Alignment.Center, + ) { + Text( + text = io.github.kahme247.ompweb.domain.providerInitials(id), + color = colors.textDim, + fontSize = (size * 0.42f).sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + ) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ComposerControlsState.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ComposerControlsState.kt new file mode 100644 index 000000000..454f315b8 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ComposerControlsState.kt @@ -0,0 +1,105 @@ +package io.github.kahme247.ompweb.ui.composer.controls + +import io.github.kahme247.ompweb.domain.CompactResult +import io.github.kahme247.ompweb.domain.ContextUsageInfo +import io.github.kahme247.ompweb.domain.ModelCapacity +import io.github.kahme247.ompweb.domain.ModelKey +import io.github.kahme247.ompweb.domain.ModelOption +import io.github.kahme247.ompweb.domain.SessionStats +import io.github.kahme247.ompweb.domain.ToolPreset +import io.github.kahme247.ompweb.domain.UsageLimit +import io.github.kahme247.ompweb.domain.selectableThinkingLevels +import io.github.kahme247.ompweb.net.ElsewhereSession + +/** What a pending new chat hands to `POST /api/agent/new` (`ensure_session`). */ +data class SpawnOptions( + val provider: String?, + val modelId: String?, + /** null means "auto": leave omp's own setting untouched. */ + val thinkingLevel: String?, + /** null omits the field (full toolset); an empty list is `none`. */ + val toolNames: List?, + val advisor: Boolean, +) + +/** + * A `409 session_running_elsewhere` refused a composer control command: another + * omp process owns this chat. The chat turns this into its ownership banner and + * a command-specific message (the send wording would be wrong here). + */ +data class ForeignCommandRefusal(val elsewhere: ElsewhereSession? = null) + +/** + * Everything the composer controls row and the context sheet render. The view + * model owns it; the app's composer only reads it (plus [ComposerControlsRow]). + */ +data class ComposerControlsState( + /** null while the chat is still an unsent new session. */ + val sessionId: String? = null, + val cwd: String = "", + /** The visible, name-sorted catalog rows (`omp-composer-models` applied). */ + val models: List = emptyList(), + val modelsLoading: Boolean = false, + val modelsError: String? = null, + /** The model the composer shows: user pick, live `get_state.model`, session file entry. */ + val model: ModelKey? = null, + /** Display name of [model] (live name, catalog name, else the id). */ + val modelName: String? = null, + val thinkingLevel: String = "auto", + /** null = neither the catalog nor the live model knows the ladder (generic fallback). */ + val availableThinkingLevels: List? = null, + val fastModeSupported: Boolean = false, + val fastModeEnabled: Boolean = false, + /** omp's per-family tier: false means enabled but inert for this model. */ + val fastModeActive: Boolean? = null, + val slowModeSupported: Boolean = false, + val slowModeEnabled: Boolean = false, + /** `global` = persisted omp setting shared by every session; `session` = this chat's flex tier. */ + val slowModeScope: String? = null, + val advisorEnabled: Boolean = false, + val toolPreset: ToolPreset = ToolPreset.FULL, + val usageLimit: UsageLimit? = null, + /** Live context totals; the sheet falls back to [sessionStats]' value. */ + val contextUsage: ContextUsageInfo? = null, + val modelCapacity: ModelCapacity? = null, + /** omp's reported output throughput; null while not generating. */ + val tokensPerSecond: Double? = null, + /** Rolling average of the reported throughput (`ContextDetailPanel` speed line). */ + val speedAverage: Double? = null, + val isCompacting: Boolean = false, + val compactResult: CompactResult? = null, + val compactError: String? = null, + val sessionStats: SessionStats? = null, + val statsLoading: Boolean = false, + /** True while the context sheet is open; the chat renders it on this flag. */ + val contextSheetOpen: Boolean = false, + /** Last command failure, for the chat's notice surface; [ComposerControlsViewModel.clearError]. */ + val error: String? = null, + /** + * A command was refused because another omp process owns the chat (409 + * `session_running_elsewhere`). The chat shows its ownership banner and a + * command-specific message instead of the send wording; it clears the field + * via [ComposerControlsViewModel.clearForeignRefusal]. + */ + val foreignRefusal: ForeignCommandRefusal? = null, +) { + /** The picker trigger label (web `currentName` / "Select model"). */ + val modelLabel: String? + get() = model?.let { key -> + modelName?.takeIf { it.isNotBlank() } + ?: models.firstOrNull { it.provider == key.provider && it.modelId == key.modelId }?.displayName + ?: key.modelId + } + + /** The reasoning dropdown's options (web `selectableThinkingLevels`). */ + val thinkingLevelOptions: List + get() = selectableThinkingLevels(availableThinkingLevels) + + /** A model that cannot think has nothing to choose — the row hides the control. */ + val thinkingSupported: Boolean + get() = thinkingLevelOptions.any { it != "auto" && it != "off" } + + /** The context panel's totals: live `contextUsage`, else the on-disk snapshot. */ + val shownContextUsage: ContextUsageInfo? + get() = contextUsage ?: sessionStats?.contextUsage +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ComposerControlsViewModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ComposerControlsViewModel.kt new file mode 100644 index 000000000..836bdfc0b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ComposerControlsViewModel.kt @@ -0,0 +1,809 @@ +package io.github.kahme247.ompweb.ui.composer.controls + +import io.github.kahme247.ompweb.data.ChatSettings +import io.github.kahme247.ompweb.domain.ModelCapacity +import io.github.kahme247.ompweb.domain.ModelKey +import io.github.kahme247.ompweb.domain.ModelOption +import io.github.kahme247.ompweb.domain.ThinkingModelMeta +import io.github.kahme247.ompweb.domain.ToolPreset +import io.github.kahme247.ompweb.domain.buildModelOptions +import io.github.kahme247.ompweb.domain.normalizeThinkingLevel +import io.github.kahme247.ompweb.domain.readCompactResult +import io.github.kahme247.ompweb.domain.resolveAvailableThinkingLevels +import io.github.kahme247.ompweb.net.AgentConfigSnapshot +import io.github.kahme247.ompweb.net.ApiError +import io.github.kahme247.ompweb.net.ComposerControlsApi +import io.github.kahme247.ompweb.net.ModelsResponse +import io.github.kahme247.ompweb.net.parseAgentConfig +import io.github.kahme247.ompweb.net.parseSessionStats +import io.github.kahme247.ompweb.net.parseThinkingModel +import io.github.kahme247.ompweb.net.userMessage +import kotlin.math.round +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.put + +/** + * The composer's model/thinking/fast/slow/advisor/usage/context controls. + * + * Port of `hooks/useAgentSession.ts` (model, thinking, fast/slow, advisor, + * compact, session stats — the parts `ChatInput` renders) plus the composer + * slices of `components/ChatInput.tsx` and `components/ComposerPanels.tsx` + * (`ContextDetailPanel`). + * + * Resolution order mirrors the web: the user's explicit pick wins until omp + * reports a different resolved model, then the live `get_state.model` (omp may + * fall back to the default model when the recorded one disappeared), then the + * session file's entry, then the catalog default for a new chat. Live answers + * stay `null`-able on purpose: without an omp process the catalog backs the + * Fast/Slow toggles and the reasoning ladder. + */ +class ComposerControlsViewModel( + private val api: ComposerControlsApi, + private val settings: ChatSettings, + private val scope: CoroutineScope, +) { + + private val _state = MutableStateFlow(ComposerControlsState()) + val state: StateFlow = _state.asStateFlow() + + private var catalog: ModelsResponse = ModelsResponse() + private var catalogLoaded = false + + /** Monotonic token for authoritative live-state syncs (web `authoritativeModelSeqRef`). */ + private var syncSeq = 0 + + // Live answers from omp. null = "no live answer": the catalog backs the control. + private var liveModel: ThinkingModelMeta? = null + private var liveSlowSupported: Boolean? = null + private var liveSlowEnabled: Boolean? = null + private var liveSlowScope: String? = null + + /** The user's explicit pick that no live snapshot has superseded yet. */ + private var pickedModel: ModelKey? = null + + /** The model recorded in the session file (web `data.context.model`). */ + private var sessionModel: ModelKey? = null + private var sessionModelName: String? = null + + /** Choices of a brand-new chat that has no omp process to command yet. */ + private var preSpawnFast: Boolean? = null + private var preSpawnSlow: Boolean? = null + + private var catalogJob: Job? = null + private var liveJob: Job? = null + private var statsJob: Job? = null + private var catalogFence = 0 + + private val speedSamples = ArrayDeque() + private var lastReportedSpeed: Double? = null + + /** True once omp answered the ladder itself; the session file stops mattering. */ + private var liveThinkingReceived = false + + /** + * Follows the composer's scope. `sessionId == null` is the unsent new chat: + * picks stay pending and are handed to `POST /api/agent/new` through + * [pendingSpawnOptions]. Never spawns an omp process. + */ + fun bind(sessionId: String?, cwd: String) { + val current = _state.value + val sameScope = current.sessionId == sessionId && current.cwd == cwd + if (sameScope) { + if (!catalogLoaded && catalogJob?.isActive != true) loadCatalog(cwd) + return + } + // Only a brand-new chat this instance prepared still holds unsaved + // pre-spawn picks. A cold-started bind of an existing session has none + // (`current` is empty), so it must read the store instead of carrying + // the defaults over — that made every cold-started session look + // spawned and forgot its advisor flag (B18). + val spawned = current.sessionId == null && sessionId != null && ( + current.advisorEnabled || preSpawnFast != null || preSpawnSlow != null + ) + statsJob?.cancel() + resetLive() + speedSamples.clear() + lastReportedSpeed = null + if (!spawned) { + pickedModel = null + sessionModel = null + sessionModelName = null + preSpawnFast = null + preSpawnSlow = null + liveThinkingReceived = false + } + _state.value = ComposerControlsState( + sessionId = sessionId, + cwd = cwd, + models = current.models, + modelsLoading = current.modelsLoading, + modelsError = current.modelsError, + model = if (spawned) current.model else null, + modelName = if (spawned) current.modelName else null, + thinkingLevel = if (spawned) current.thinkingLevel else "auto", + advisorEnabled = when { + sessionId == null -> current.advisorEnabled + // A pre-spawn toggle is not persisted yet; the choice this + // instance carries is the truth for the new id. + spawned -> current.advisorEnabled + else -> settings.snapshot.value.advisorSessions.contains(sessionId) + }, + toolPreset = ToolPreset.fromWire(settings.snapshot.value.toolPreset), + ) + refreshDerived() + loadCatalog(cwd) + if (sessionId != null) { + refreshLiveState(sessionId) + if (spawned) { + // Persist the carried choice for this id, so the chat's own + // controls instance and a later cold start see it too. + settings.markSessionCreated(sessionId, current.advisorEnabled) + applyPreSpawnChoices(sessionId) + } + } + } + + /** + * The session the new-chat pane just created exists under [sessionId]. + * Hands the pane's unsaved picks over to the shared store before the chat + * binds the id (the chat composer is a *different* + * [ComposerControlsViewModel] instance) and applies the Fast/Slow toggles + * `POST /api/agent/new` cannot carry. + */ + fun onSessionCreated(sessionId: String) { + if (sessionId.isBlank() || _state.value.sessionId != null) return + settings.markSessionCreated(sessionId, _state.value.advisorEnabled) + applyPreSpawnChoices(sessionId) + } + + /** + * The session file's `thinkingLevel`, shown until omp reports the live one + * (an idle session has no process to ask). Ignored once a live answer arrived. + */ + fun onSessionThinking(level: String?) { + if (liveThinkingReceived) return + val normalized = normalizeThinkingLevel(level) + _state.update { it.copy(thinkingLevel = normalized) } + } + + /** The session file's model entry, for the trigger before any omp process runs. */ + fun onSessionModel(provider: String, modelId: String, name: String? = null) { + if (provider.isBlank() || modelId.isBlank()) return + sessionModel = ModelKey(provider, modelId) + sessionModelName = name?.takeIf { it.isNotBlank() } + refreshDerived() + } + + /** + * One `get_state` snapshot: `GET /api/agent/{id}` (`{running, alive, state}`, + * also with `state` missing) or a bare state object. Call it for every + * snapshot the chat receives. + */ + fun onAgentState(payload: JsonObject) { + val stateObject = payload["state"] as? JsonObject + val token = beginSync() + if (stateObject == null) { + // No omp process: the catalog backs Slow again, as on the web. + liveSlowSupported = null + refreshDerived() + return + } + applyConfig(parseAgentConfig(payload), childAlive(payload, stateObject), token) + } + + /** + * One session SSE frame. Only the config-shaped frames matter here; every + * other frame is the chat's business. + */ + fun onFrame(frame: JsonObject) { + when (frame.string("type")) { + "thinking_level_changed" -> { + syncSeq += 1 + liveThinkingReceived = true + val level = normalizeThinkingLevel(frame.string("thinkingLevel")) + _state.update { it.copy(thinkingLevel = level) } + } + + "model_changed" -> { + val sessionId = _state.value.sessionId + if (sessionId != null) refreshLiveState(sessionId) + } + + "config_update" -> { + val model = parseThinkingModel(frame["model"]) + if (model != null) { + syncSeq += 1 + applyAuthoritativeModel(model) + } else if (frame.containsKey("thinkingLevel")) { + syncSeq += 1 + } + frame.string("thinkingLevel")?.let { level -> + liveThinkingReceived = true + _state.update { it.copy(thinkingLevel = normalizeThinkingLevel(level)) } + } + refreshDerived() + } + + "auto_compaction_start" -> { + _state.update { + it.copy(isCompacting = true, compactError = null, compactResult = null) + } + } + + "auto_compaction_end" -> { + val error = frame.string("errorMessage")?.takeIf { it.isNotBlank() } + val finished = frame.boolean("aborted") != true && frame.boolean("skipped") != true + _state.update { + when { + error != null -> it.copy( + isCompacting = false, + compactError = error, + compactResult = null, + ) + + finished -> it.copy( + isCompacting = false, + compactResult = readCompactResult(frame["result"], "auto"), + ) + + else -> it.copy(isCompacting = false) + } + } + if (finished) refreshSessionStats() + } + } + } + + // --- Model ------------------------------------------------------------- + + fun onSelectModel(provider: String, modelId: String) { + if (provider.isBlank() || modelId.isBlank()) return + val key = ModelKey(provider, modelId) + pickedModel = key + _state.update { it.copy(model = key, modelName = null, error = null) } + refreshDerived() + val sessionId = _state.value.sessionId ?: return + scope.launch { + try { + send( + sessionId, + buildJsonObject { + put("type", "set_model") + put("provider", provider) + put("modelId", modelId) + }, + ) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + fail(e) + } finally { + // A command against an idle-disposed session restarts omp, which + // re-resolves the model: the composer must show what actually runs. + if (_state.value.sessionId == sessionId) refreshLiveState(sessionId) + } + } + } + + // --- Thinking ---------------------------------------------------------- + + fun onThinkingLevelChange(level: String) { + val normalized = normalizeThinkingLevel(level) + _state.update { it.copy(thinkingLevel = normalized, error = null) } + // "auto" leaves omp's own setting untouched, exactly like the web. + if (normalized == "auto") return + val sessionId = _state.value.sessionId ?: return + scope.launch { + try { + send( + sessionId, + buildJsonObject { + put("type", "set_thinking_level") + put("level", normalized) + }, + ) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + fail(e) + } finally { + if (_state.value.sessionId == sessionId) refreshLiveState(sessionId) + } + } + } + + /** + * Ctrl/Cmd+Alt+M: `cycle_model` passthrough (web `rpc-manager` + * `PASSTHROUGH_COMMANDS`); omp picks the next model and the live refresh + * shows what it chose. + */ + fun cycleModel() { + val sessionId = _state.value.sessionId ?: return + scope.launch { + try { + send(sessionId, buildJsonObject { put("type", "cycle_model") }) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + fail(e) + } finally { + if (_state.value.sessionId == sessionId) refreshLiveState(sessionId) + } + } + } + + /** Ctrl/Cmd+Alt+T: `cycle_thinking_level` passthrough. */ + fun cycleThinkingLevel() { + val sessionId = _state.value.sessionId ?: return + scope.launch { + try { + send(sessionId, buildJsonObject { put("type", "cycle_thinking_level") }) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + fail(e) + } finally { + if (_state.value.sessionId == sessionId) refreshLiveState(sessionId) + } + } + } + + // --- Fast / Slow ------------------------------------------------------- + + fun onFastModeChange(enabled: Boolean) { + _state.update { it.copy(fastModeEnabled = enabled, error = null) } + val sessionId = _state.value.sessionId + if (sessionId == null) { + preSpawnFast = enabled + return + } + scope.launch { + try { + val result = send( + sessionId, + buildJsonObject { + put("type", "set_fast_mode") + put("enabled", enabled) + }, + ) + val root = result as? JsonObject + _state.update { + it.copy( + fastModeEnabled = root.boolean("enabled") ?: enabled, + fastModeActive = root.boolean("active") ?: it.fastModeActive, + ) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + fail(e) + } finally { + if (_state.value.sessionId == sessionId) refreshLiveState(sessionId) + } + } + } + + fun onSlowModeChange(enabled: Boolean) { + _state.update { it.copy(slowModeEnabled = enabled, error = null) } + val sessionId = _state.value.sessionId + if (sessionId == null) { + preSpawnSlow = enabled + return + } + scope.launch { + try { + val result = send( + sessionId, + buildJsonObject { + put("type", "set_slow_mode") + put("enabled", enabled) + }, + ) + val applied = (result as? JsonObject).boolean("enabled") ?: enabled + preSpawnSlow = null + _state.update { it.copy(slowModeEnabled = applied) } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + fail(e) + } finally { + if (_state.value.sessionId == sessionId) refreshLiveState(sessionId) + } + } + } + + // --- Advisor / tool preset --------------------------------------------- + + fun onAdvisorChange(enabled: Boolean) { + _state.update { it.copy(advisorEnabled = enabled) } + val sessionId = _state.value.sessionId ?: return + scope.launch { settings.setAdvisorEnabled(sessionId, enabled) } + } + + fun onToolPresetChange(preset: ToolPreset) { + _state.update { it.copy(toolPreset = preset) } + scope.launch { settings.setToolPreset(preset) } + } + + // --- Context sheet ----------------------------------------------------- + + fun openContextSheet() { + _state.update { it.copy(contextSheetOpen = true) } + refreshSessionStats() + } + + fun closeContextSheet() { + _state.update { it.copy(contextSheetOpen = false) } + } + + fun refreshSessionStats() { + val sessionId = _state.value.sessionId ?: return + statsJob?.cancel() + _state.update { it.copy(statsLoading = true) } + statsJob = scope.launch { + try { + val payload = send(sessionId, buildJsonObject { put("type", "get_session_stats") }) + if (_state.value.sessionId != sessionId) return@launch + _state.update { + it.copy( + sessionStats = parseSessionStats(payload) ?: it.sessionStats, + statsLoading = false, + ) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + if (_state.value.sessionId == sessionId) { + _state.update { it.copy(statsLoading = false, error = e.userMessage()) } + } + } + } + } + + fun compact() { + val sessionId = _state.value.sessionId ?: return + if (_state.value.isCompacting) return + _state.update { + it.copy(isCompacting = true, compactError = null, compactResult = null, error = null) + } + scope.launch { + try { + val result = send(sessionId, buildJsonObject { put("type", "compact") }) + if (_state.value.sessionId != sessionId) return@launch + _state.update { + it.copy( + isCompacting = false, + compactResult = readCompactResult(result, "manual"), + ) + } + refreshSessionStats() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + if (_state.value.sessionId == sessionId) { + _state.update { + it.copy(isCompacting = false, compactError = e.userMessage()) + } + } + } + } + } + + fun abortCompaction() { + val sessionId = _state.value.sessionId ?: return + scope.launch { + try { + send(sessionId, buildJsonObject { put("type", "abort_compaction") }) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + fail(e) + } + } + } + + // --- Spawn ------------------------------------------------------------- + + /** + * What `POST /api/agent/new` must carry so the new omp child starts with + * the composer's choices (the route applies both via RPC after spawn). + */ + fun pendingSpawnOptions(): SpawnOptions { + val current = _state.value + return SpawnOptions( + provider = current.model?.provider, + modelId = current.model?.modelId, + thinkingLevel = current.thinkingLevel.takeIf { it != "auto" }, + toolNames = current.toolPreset.toolNames(), + advisor = current.advisorEnabled, + ) + } + + fun clearError() { + _state.update { it.copy(error = null) } + } + + /** The chat consumed the ownership refusal (banner + message shown). */ + fun clearForeignRefusal() { + _state.update { it.copy(foreignRefusal = null) } + } + + // --- Internals --------------------------------------------------------- + + private fun resetLive() { + liveModel = null + liveSlowSupported = null + liveSlowEnabled = null + liveSlowScope = null + } + + private fun beginSync(): Int = ++syncSeq + + /** + * Whether an omp child exists for the session: `alive` says the process is + * there, `running` whether a turn is in flight. A server that predates the + * split reports no `alive`; its `running` (then a bare state object) was the + * "a child answers" signal, so it stays the fallback. + */ + private fun childAlive(payload: JsonObject, stateObject: JsonObject?): Boolean = + payload.boolean("alive") ?: payload.boolean("running") ?: (stateObject != null) + + private fun refreshLiveState(sessionId: String? = _state.value.sessionId) { + if (sessionId == null) return + liveJob?.cancel() + val token = beginSync() + liveJob = scope.launch { + try { + val payload = api.agentState(sessionId) + if (_state.value.sessionId != sessionId) return@launch + applyConfig( + parseAgentConfig(payload), + childAlive(payload, payload["state"] as? JsonObject), + token, + ) + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + // Best effort: keep the last known values; the next snapshot corrects them. + } + } + } + + private fun applyConfig(config: AgentConfigSnapshot, alive: Boolean, token: Int) { + if (token != syncSeq) return + syncSeq += 1 + if (config.thinkingLevel != null) liveThinkingReceived = true + config.model?.let { applyAuthoritativeModel(it) } + _state.update { current -> + var next = current + if (config.fastModeEnabled != null) next = next.copy(fastModeEnabled = config.fastModeEnabled) + if (config.fastModeActive != null) next = next.copy(fastModeActive = config.fastModeActive) + next.copy( + thinkingLevel = config.thinkingLevel?.let(::normalizeThinkingLevel) ?: next.thinkingLevel, + usageLimit = config.usageLimit, + contextUsage = config.contextUsage, + isCompacting = config.isCompacting ?: next.isCompacting, + ) + } + // `alive` (the child exists), not `running` (a turn is in flight): an + // idle wrapper still answers `get_state`, so its Slow support is the + // live value — the catalog only backs a session without a child. + liveSlowSupported = if (alive) (config.slowModeSupported ?: false) else null + liveSlowEnabled = config.slowModeEnabled ?: false + liveSlowScope = config.slowModeScope + applySpeed(config.tokensPerSecond) + refreshDerived() + } + + /** + * A runtime model switch (retry fallback, prewalk hand-off, `/model`) + * supersedes the user's last pick; the composer shows what omp actually runs. + */ + private fun applyAuthoritativeModel(model: ThinkingModelMeta) { + liveModel = model + val pick = pickedModel + if (pick != null && (pick.provider != model.provider || pick.modelId != model.modelId)) { + pickedModel = null + } + } + + private fun applySpeed(reported: Double?) { + if (reported == null || !reported.isFinite() || reported <= 0.0) { + if (lastReportedSpeed != null) { + lastReportedSpeed = null + _state.update { it.copy(tokensPerSecond = null) } + } + return + } + // Distinct reported values feed the rolling average; repeated polls of + // the same value are ignored (web `ChatWindow` speed samples). + val quantized = round(reported * 10) / 10.0 + if (quantized == lastReportedSpeed) return + lastReportedSpeed = quantized + speedSamples.addLast(quantized) + while (speedSamples.size > SPEED_SAMPLE_WINDOW) speedSamples.removeFirst() + _state.update { + it.copy( + tokensPerSecond = quantized, + speedAverage = speedSamples.average(), + ) + } + } + + private fun loadCatalog(cwd: String) { + catalogJob?.cancel() + val fence = ++catalogFence + _state.update { it.copy(modelsLoading = true, modelsError = null) } + catalogJob = scope.launch { + try { + val response = api.models(cwd) + if (fence != catalogFence) return@launch + catalog = response + catalogLoaded = true + refreshDerived() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + if (fence == catalogFence) { + _state.update { + it.copy(modelsLoading = false, modelsError = e.userMessage()) + } + } + } + } + } + + /** Recomputes every value that depends on (catalog, live state, picks). */ + private fun refreshDerived() { + val current = _state.value + val visible = buildModelOptions( + catalog.modelList.map { ModelOption(it.provider, it.id, it.name) }, + settings.snapshot.value.composerModels, + ) + val displayKey = pickedModel + ?: liveModel?.let { ModelKey(it.provider, it.modelId) } + ?: sessionModel + ?: if (current.sessionId == null) defaultCatalogModel() else null + val entry = displayKey?.let { key -> + catalog.modelList.firstOrNull { it.provider == key.provider && it.id == key.modelId } + } + val live = liveModel + val liveName = if (live != null && displayKey != null && + live.provider == displayKey.provider && live.modelId == displayKey.modelId + ) { + live.name?.takeIf { it.isNotBlank() } + } else { + null + } + val modelName = liveName + ?: entry?.name?.takeIf { it.isNotBlank() } + ?: sessionModelName?.takeIf { displayKey == sessionModel } + val catalogLevels = displayKey?.let { catalog.thinkingLevels["${it.provider}:${it.modelId}"] } + val availableLevels = displayKey?.let { key -> + resolveAvailableThinkingLevels(catalogLevels, key, live) + } + // Pre-spawn (or executable) fallback: omp answers Slow support per model. + val catalogSlow = if (liveSlowSupported == null) entry?.takeIf { it.supportsSlowMode } else null + val slowSupported = liveSlowSupported ?: (catalogSlow != null) + val slowEnabled = when { + preSpawnSlow != null -> preSpawnSlow!! + catalogSlow != null -> catalogSlow.provider == "anthropic" && catalog.anthropicSlowMode + else -> liveSlowEnabled ?: false + } + val slowScope = when { + catalogSlow != null -> if (catalogSlow.provider == "anthropic") "global" else "session" + else -> liveSlowScope + } + val capacity = entry?.let { + if ((it.contextWindow ?: 0) > 0 || (it.maxTokens ?: 0) > 0) { + ModelCapacity(it.contextWindow, it.maxTokens) + } else { + null + } + } + _state.value = current.copy( + models = visible, + modelsLoading = if (catalogLoaded) false else current.modelsLoading, + model = displayKey, + modelName = modelName, + availableThinkingLevels = availableLevels, + fastModeSupported = entry?.supportsFastMode == true, + slowModeSupported = slowSupported, + slowModeEnabled = slowEnabled, + slowModeScope = slowScope, + modelCapacity = capacity, + ) + } + + private fun defaultCatalogModel(): ModelKey? { + val preferred = catalog.defaultModel?.let { fallback -> + catalog.modelList.firstOrNull { + it.provider == fallback.provider && it.id == fallback.modelId + } + } + val entry = preferred ?: catalog.modelList.firstOrNull() + return entry?.let { ModelKey(it.provider, it.id) } + } + + /** + * A new chat that just got its session id: apply the Fast/Slow toggles no + * spawn field covers (`POST /api/agent/new` only takes + * model/thinking/tools/advisor). The advisor choice travels through + * [ChatSettings.markSessionCreated] instead; the commands name + * [sessionId] explicitly, so the pane that created it still applies them + * after the chat took over. + */ + private fun applyPreSpawnChoices(sessionId: String) { + val fast = preSpawnFast + val slow = preSpawnSlow + preSpawnFast = null + preSpawnSlow = null + if (fast == null && slow == null) return + scope.launch { + try { + if (fast != null) { + send( + sessionId, + buildJsonObject { + put("type", "set_fast_mode") + put("enabled", fast) + }, + ) + } + if (slow != null) { + send( + sessionId, + buildJsonObject { + put("type", "set_slow_mode") + put("enabled", slow) + }, + ) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + fail(e) + } finally { + if (_state.value.sessionId == sessionId) refreshLiveState(sessionId) + } + } + } + + private suspend fun send(sessionId: String, payload: JsonObject): JsonElement? = + api.agentCommand(sessionId, payload, _state.value.advisorEnabled) + + private fun fail(error: Throwable) { + // Another omp process owns the chat: the shell shows the ownership + // banner and says the command was not applied. The generic message + // ("your message was not sent") is send wording and would be wrong here. + if (error is ApiError.SessionRunningElsewhere) { + _state.update { it.copy(foreignRefusal = ForeignCommandRefusal(error.elsewhere)) } + return + } + _state.update { it.copy(error = error.userMessage()) } + } + + private companion object { + /** Web `ChatWindow` keeps the last 32 distinct throughput samples. */ + const val SPEED_SAMPLE_WINDOW = 32 + } +} + +private fun JsonObject?.boolean(key: String): Boolean? = + (this?.get(key) as? JsonPrimitive)?.booleanOrNull + +private fun JsonObject.string(key: String): String? = + (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.contentOrNull diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ContextDetailSheet.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ContextDetailSheet.kt new file mode 100644 index 000000000..8c11d5f5f --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ContextDetailSheet.kt @@ -0,0 +1,488 @@ +package io.github.kahme247.ompweb.ui.composer.controls + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.ContextTone +import io.github.kahme247.ompweb.domain.ContextUsageInfo +import io.github.kahme247.ompweb.domain.SessionStats +import io.github.kahme247.ompweb.domain.cacheHitRate +import io.github.kahme247.ompweb.domain.contextTone +import io.github.kahme247.ompweb.domain.formatCost +import io.github.kahme247.ompweb.domain.formatPercent +import io.github.kahme247.ompweb.domain.formatTokenCount +import io.github.kahme247.ompweb.ui.render.copyTextToClipboard +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.LocalOmpDisplayPreferences +import io.github.kahme247.ompweb.ui.theme.OmpDialogContent +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily +import java.util.Locale + +/** + * The context bottom sheet: `ContextDetailPanel` from + * `components/ComposerPanels.tsx` — context bar, window/capacity, speed, then + * session totals from `get_session_stats`, with the compact action + * (and its abort) in the footer. + * + * [streaming] only disables Compact while the agent is mid-turn, matching the + * web's `disabled={isStreaming && !isCompacting}`. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ContextDetailSheet( + vm: ComposerControlsViewModel, + onDismiss: () -> Unit, + streaming: Boolean = false, +) { + val state by vm.state.collectAsStateWithLifecycle() + val colors = LocalOmpColors.current + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val wide = LocalConfiguration.current.screenWidthDp >= 600 + val context = LocalContext.current + val displayPreferences = LocalOmpDisplayPreferences.current + val copiedText = stringResource(R.string.controls_copied) + var copied by remember { mutableStateOf(null) } + + fun copy(label: String, text: String) { + if (copyTextToClipboard(context, label, text, sensitive = false)) copied = label + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = colors.panel, + contentColor = colors.text, + ) { + OmpDialogContent(displayPreferences) { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 620.dp) + .verticalScroll(rememberScrollState()) + .padding(start = 16.dp, end = 16.dp, bottom = 20.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.controls_context_title), + color = colors.text, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f), + ) + if (copied != null) { + Text( + text = copiedText, + color = colors.accent, + fontSize = 11.sp, + modifier = Modifier.padding(end = 6.dp), + ) + } + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.controls_close), + tint = colors.textDim, + modifier = Modifier.size(16.dp), + ) + } + } + + val sessionId = state.sessionId + if (sessionId == null) { + Text( + text = stringResource(R.string.controls_context_empty), + color = colors.textDim, + fontSize = 12.sp, + modifier = Modifier.padding(vertical = 16.dp), + ) + return@Column + } + + ContextSummary( + usage = state.shownContextUsage, + window = state.modelCapacity?.contextWindow ?: state.shownContextUsage?.contextWindow, + maxTokens = state.modelCapacity?.maxTokens, + tokensPerSecond = state.tokensPerSecond, + speedAverage = state.speedAverage, + ) + + state.sessionStats?.let { stats -> + val sections = listOf<@Composable () -> Unit>( + { + StatsSection(title = stringResource(R.string.controls_section_session_info)) { + InfoRow( + label = stringResource(R.string.controls_stat_name), + value = stats.sessionName?.takeIf { it.isNotBlank() } + ?: stringResource(R.string.controls_in_memory), + ) + val file = stats.sessionFile + if (file != null) { + InfoRow( + label = stringResource(R.string.controls_stat_file), + value = file, + mono = true, + onCopy = { copy(file, file) }, + copyLabel = stringResource(R.string.controls_copy_file), + ) + } + InfoRow( + label = stringResource(R.string.controls_stat_id), + value = stats.sessionId.ifBlank { sessionId }, + mono = true, + onCopy = { + copy(stats.sessionId.ifBlank { sessionId }, stats.sessionId.ifBlank { sessionId }) + }, + copyLabel = stringResource(R.string.controls_copy_id), + ) + } + }, + { + StatsSection(title = stringResource(R.string.controls_section_messages)) { + InfoRow( + label = stringResource(R.string.controls_stat_user), + value = stats.userMessages.toString(), + ) + InfoRow( + label = stringResource(R.string.controls_stat_assistant), + value = stats.assistantMessages.toString(), + ) + InfoRow( + label = stringResource(R.string.controls_stat_tool_calls), + value = stats.toolCalls.toString(), + ) + InfoRow( + label = stringResource(R.string.controls_stat_tool_results), + value = stats.toolResults.toString(), + ) + InfoRow( + label = stringResource(R.string.controls_stat_total), + value = stats.totalMessages.toString(), + emphasize = true, + ) + } + }, + { + StatsSection(title = stringResource(R.string.controls_section_tokens)) { + TokenRows(stats = stats) + } + }, + ) + if (wide) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + Column(modifier = Modifier.weight(1f)) { + sections[0]() + Spacer(Modifier.height(6.dp)) + } + Column(modifier = Modifier.weight(1f)) { + sections[1]() + Spacer(Modifier.height(6.dp)) + } + } + sections[2]() + } else { + sections.forEach { it() } + } + } ?: if (state.statsLoading) { + Text( + text = stringResource(R.string.controls_context_loading), + color = colors.textDim, + fontSize = 12.sp, + modifier = Modifier.padding(vertical = 16.dp), + ) + } else { + Spacer(Modifier.height(8.dp)) + } + + state.compactResult?.let { result -> + val verb = if (result.reason.isBlank()) { + stringResource(R.string.controls_compacted) + } else { + stringResource(R.string.controls_compacted_with_reason, result.reason) + } + Text( + text = stringResource( + R.string.controls_compact_result, + verb, + formatTokenCount(result.tokensBefore), + formatTokenCount(result.estimatedTokensAfter), + formatTokenCount(result.savedTokens), + ), + color = colors.textMuted, + fontSize = 11.sp, + modifier = Modifier.padding(top = 10.dp), + ) + } + state.compactError?.let { error -> + Text( + text = error, + color = colors.statusError, + fontSize = 11.sp, + modifier = Modifier.padding(top = 10.dp), + ) + } + + val compactEnabled = !streaming || state.isCompacting + Row( + modifier = Modifier.fillMaxWidth().padding(top = 14.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (state.isCompacting) { + Button( + onClick = { vm.abortCompaction() }, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = colors.selected, + contentColor = colors.text, + ), + ) { + Text(stringResource(R.string.controls_stop_compaction), fontSize = 12.sp) + } + } else { + Button( + onClick = { vm.compact() }, + enabled = compactEnabled, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = colors.accent, + contentColor = colors.onAccent, + ), + ) { + Text(stringResource(R.string.controls_compact), fontSize = 12.sp) + } + } + TextButton(onClick = onDismiss) { + Text( + text = stringResource(R.string.controls_close), + color = colors.textMuted, + fontSize = 12.sp, + ) + } + } + } + } + } +} + +@Composable +private fun ContextSummary( + usage: ContextUsageInfo?, + window: Int?, + maxTokens: Int?, + tokensPerSecond: Double?, + speedAverage: Double?, +) { + val colors = LocalOmpColors.current + val percent = usage?.percent + val tone = when (contextTone(percent)) { + ContextTone.ERROR -> colors.statusError + ContextTone.WARNING -> colors.statusWarning + ContextTone.NORMAL -> colors.accent + } + Column(modifier = Modifier.padding(top = 10.dp)) { + if (usage != null) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = buildString { + append(formatTokenCount(usage.tokens ?: 0)) + append(" / ") + append(formatTokenCount(usage.contextWindow)) + if (percent != null) { + append(" (") + append(formatPercent(percent)) + append(')') + } + }, + color = colors.text, + fontSize = 12.sp, + fontFamily = OmpMonoFontFamily, + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp) + .height(5.dp) + .background(colors.border, RoundedCornerShape(3.dp)), + ) { + Box( + modifier = Modifier + .fillMaxWidth(((percent ?: 0.0).coerceIn(0.0, 100.0) / 100.0).toFloat()) + .height(5.dp) + .background(tone, RoundedCornerShape(3.dp)), + ) + } + } + val chips = buildList { + window?.let { add(stringResource(R.string.controls_window_size, formatTokenCount(it))) } + maxTokens?.let { add(stringResource(R.string.controls_max_output, formatTokenCount(it))) } + val speed = tokensPerSecond + if (speed != null && speed > 0) { + val current = formatSpeed(speed) + add( + if (speedAverage != null && speedAverage > 0) { + stringResource(R.string.controls_speed, current, formatSpeed(speedAverage)) + } else { + stringResource(R.string.controls_speed_current, current) + }, + ) + } + } + if (chips.isNotEmpty()) { + Text( + text = chips.joinToString(" · "), + color = colors.textDim, + fontSize = 10.5.sp, + modifier = Modifier.padding(top = 6.dp), + ) + } + } +} + +@Composable +private fun StatsSection(title: String, content: @Composable () -> Unit) { + Column(modifier = Modifier.padding(top = 12.dp)) { + Text( + text = title, + color = LocalOmpColors.current.textDim, + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + ) + Column(modifier = Modifier.padding(top = 2.dp)) { content() } + } +} + +@Composable +private fun TokenRows(stats: SessionStats) { + val colors = LocalOmpColors.current + val tokens = stats.tokens + InfoRow(label = stringResource(R.string.controls_stat_input), value = formatTokenCount(tokens.input)) + InfoRow(label = stringResource(R.string.controls_stat_output), value = formatTokenCount(tokens.output)) + if (tokens.cacheRead > 0) { + InfoRow( + label = stringResource(R.string.controls_stat_cache_read), + value = formatTokenCount(tokens.cacheRead), + ) + } + if (tokens.cacheWrite > 0) { + InfoRow( + label = stringResource(R.string.controls_stat_cache_write), + value = formatTokenCount(tokens.cacheWrite), + ) + } + InfoRow( + label = stringResource(R.string.controls_stat_total), + value = formatTokenCount(tokens.total), + emphasize = true, + ) + val hitRate = cacheHitRate(tokens.input, tokens.cacheRead) + if (hitRate != null) { + InfoRow( + label = stringResource(R.string.controls_stat_cache_rate), + value = String.format(Locale.US, "%.1f%%", hitRate), + ) + } + formatCost(stats.cost)?.let { cost -> + InfoRow( + label = stringResource(R.string.controls_stat_cost), + value = cost, + mono = true, + valueColor = colors.textMuted, + ) + } +} + +@Composable +private fun InfoRow( + label: String, + value: String, + mono: Boolean = false, + emphasize: Boolean = false, + valueColor: Color? = null, + onCopy: (() -> Unit)? = null, + copyLabel: String? = null, +) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .then( + if (onCopy != null) { + Modifier + .clickable(onClick = onCopy) + .semantics { if (copyLabel != null) contentDescription = copyLabel } + } else { + Modifier + }, + ) + .padding(vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + color = colors.textDim, + fontSize = 11.sp, + modifier = Modifier.width(96.dp), + ) + Text( + text = value, + color = valueColor ?: if (emphasize) colors.text else colors.textMuted, + fontSize = 11.sp, + fontWeight = if (emphasize) FontWeight.SemiBold else FontWeight.Normal, + fontFamily = if (mono) OmpMonoFontFamily else null, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } +} + +/** `formatSpeed` from the web panel: one decimal below 10, none above. */ +private fun formatSpeed(value: Double): String = + if (value >= 10) { + String.format(Locale.US, "%.0f", value) + } else { + String.format(Locale.US, "%.1f", value) + } diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/GhostText.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/GhostText.kt new file mode 100644 index 000000000..ca16ca08c --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/GhostText.kt @@ -0,0 +1,86 @@ +package io.github.kahme247.ompweb.ui.composer.controls + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + +/** + * Inline ghost text for the composer field, the Android counterpart of + * `components/GhostMirror.tsx`: the suffix is spliced in at the caret and + * painted in [color], and every offset past the caret shifts by its length so + * the caret and selection stay where the user put them. + * + * Only pass a suffix for the draft that produced it ([WordPredictor] clears it + * otherwise); an empty suffix, no suffix, or a caret outside the text yields + * the identity transformation. + */ +fun ghostTextTransformation( + suffix: String?, + caret: Int, + textLength: Int, + color: Color = Color.Unspecified, +): VisualTransformation { + if (suffix.isNullOrEmpty() || caret !in 0..textLength) return IdentityTransformation + return GhostTransformation(suffix, caret, color) +} + +private object IdentityTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText = + TransformedText(text, OffsetMapping.Identity) +} + +private class GhostTransformation( + private val suffix: String, + private val caret: Int, + private val color: Color, +) : VisualTransformation { + + private val mapping = object : OffsetMapping { + private val ghostEnd = caret + suffix.length + + override fun originalToTransformed(offset: Int): Int = + if (offset <= caret) offset else offset + suffix.length + + override fun transformedToOriginal(offset: Int): Int = when { + offset <= caret -> offset + // Anywhere inside the ghost is "before it", i.e. at the caret. + offset <= ghostEnd -> caret + else -> offset - suffix.length + } + } + + override fun filter(text: AnnotatedString): TransformedText { + if (text.isEmpty() || caret !in 0..text.length) { + return TransformedText(text, OffsetMapping.Identity) + } + val ghostEnd = caret + suffix.length + // Shift every existing range past the caret so the caller's styling survives. + fun shift(index: Int): Int = if (index <= caret) index else index + suffix.length + val spanStyles = text.spanStyles.mapNotNull { range -> + val start = shift(range.start) + val end = shift(range.end) + if (start > end) null else AnnotatedString.Range(range.item, start, end) + } + val paragraphStyles = text.paragraphStyles.map { range -> + AnnotatedString.Range(range.item, shift(range.start), shift(range.end)) + } + val ghostStyle = if (color == Color.Unspecified) { + emptyList() + } else { + listOf(AnnotatedString.Range(SpanStyle(color = color), caret, ghostEnd)) + } + val transformed = text.text.substring(0, caret) + suffix + text.text.substring(caret) + return TransformedText( + AnnotatedString(transformed, spanStyles + ghostStyle, paragraphStyles), + mapping, + ) + } + + override fun equals(other: Any?): Boolean = other is GhostTransformation && + other.suffix == suffix && other.caret == caret && other.color == color + + override fun hashCode(): Int = 31 * (31 * suffix.hashCode() + caret) + color.hashCode() +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ModelPickerDialog.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ModelPickerDialog.kt new file mode 100644 index 000000000..c90d596ce --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/ModelPickerDialog.kt @@ -0,0 +1,420 @@ +package io.github.kahme247.ompweb.ui.composer.controls + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.ui.theme.OmpDialog +import androidx.compose.ui.window.DialogProperties +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.ModelKey +import io.github.kahme247.ompweb.domain.ModelOption +import io.github.kahme247.ompweb.domain.ProviderModelGroup +import io.github.kahme247.ompweb.domain.filterModelOptions +import io.github.kahme247.ompweb.domain.groupModelOptionsByProvider +import io.github.kahme247.ompweb.domain.orderProviderGroups +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors + +/** + * The nested model picker (`components/ChatInput-model-picker.tsx`): providers + * on the rail, that provider's models in the pane, search flattens across + * providers. Narrow screens start on the rail and open one provider, wide + * screens show both panes, exactly like the web's `isMobile` split. + * + * "Add Providers" is deliberately absent: the web opens Settings → Models from + * it, and this app has no Models screen before Phase 10. + */ +@Composable +internal fun ModelPickerDialog( + state: ComposerControlsState, + onDismiss: () -> Unit, + onSelect: (provider: String, modelId: String) -> Unit, +) { + val colors = LocalOmpColors.current + val narrow = LocalConfiguration.current.screenWidthDp < 600 + var query by rememberSaveable { mutableStateOf("") } + var selectedProvider by rememberSaveable { mutableStateOf(null) } + + OmpDialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface( + color = colors.panel, + shape = RoundedCornerShape(14.dp), + modifier = Modifier + .fillMaxWidth(if (narrow) 0.94f else 0.7f) + .heightIn(max = 560.dp), + ) { + Column(modifier = Modifier.padding(12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.controls_change_model), + color = colors.text, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f), + ) + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.controls_close), + tint = colors.textDim, + modifier = Modifier.size(18.dp), + ) + } + } + + OutlinedTextField( + value = query, + onValueChange = { query = it }, + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + singleLine = true, + textStyle = TextStyle(fontSize = 13.sp), + leadingIcon = { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(15.dp), + ) + }, + placeholder = { + Text( + text = stringResource(R.string.controls_search_models), + color = colors.textDim, + fontSize = 13.sp, + ) + }, + ) + + state.modelsError?.let { error -> + Text( + text = error, + color = colors.statusError, + fontSize = 11.sp, + modifier = Modifier.padding(top = 6.dp), + ) + } + + val searching = query.isNotBlank() + val options = remember(state.models, query) { + filterModelOptions(state.models, query) + } + val groups = remember(options, state.model) { + orderProviderGroups( + groupModelOptionsByProvider(options), + state.model?.provider, + ) + } + val activeProvider = resolveActiveProvider(groups, selectedProvider, state.model?.provider) + val showRail = !searching && (!narrow || selectedProvider == null) + val showModels = searching || !narrow || selectedProvider != null + + Box(modifier = Modifier.padding(top = 8.dp)) { + when { + state.modelsLoading && state.models.isEmpty() -> LoadingRow() + state.models.isEmpty() -> EmptyRow( + stringResource(R.string.controls_no_available_models), + ) + + showRail && !showModels -> ProviderRail( + groups = groups, + activeProvider = activeProvider, + currentProvider = state.model?.provider, + modifier = Modifier.fillMaxWidth(), + onPick = { selectedProvider = it }, + ) + + else -> Row(modifier = Modifier.fillMaxWidth()) { + if (showRail) { + ProviderRail( + groups = groups, + activeProvider = activeProvider, + currentProvider = state.model?.provider, + modifier = Modifier.weight(0.38f), + onPick = { selectedProvider = it }, + ) + } + ModelList( + modifier = Modifier.weight(if (showRail) 0.62f else 1f), + groups = if (searching) { + groups + } else { + groups.filter { it.provider == activeProvider } + }, + searching = searching, + current = state.model, + onBack = if (narrow && showRail) { + null + } else if (narrow) { + { selectedProvider = null } + } else { + null + }, + onSelect = onSelect, + ) + } + } + } + } + } + } +} + +@Composable +private fun ProviderRail( + groups: List, + activeProvider: String?, + currentProvider: String?, + onPick: (String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { + if (groups.isEmpty()) { + item { EmptyRow(stringResource(R.string.controls_no_available_models)) } + } + items(groups, key = { it.provider }) { group -> + ProviderRow( + group = group, + active = group.provider == activeProvider, + current = group.provider == currentProvider, + onClick = { onPick(group.provider) }, + ) + } + } +} + +@Composable +private fun ModelList( + groups: List, + searching: Boolean, + current: ModelKey?, + onBack: (() -> Unit)?, + onSelect: (String, String) -> Unit, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + LazyColumn(modifier = modifier) { + if (onBack != null) { + item(key = "back") { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onBack) + .padding(horizontal = 6.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowLeft, + contentDescription = stringResource(R.string.controls_back_to_providers), + tint = colors.textDim, + modifier = Modifier.size(14.dp), + ) + Text( + text = groups.firstOrNull()?.provider + ?: stringResource(R.string.controls_providers), + color = colors.textMuted, + fontSize = 12.sp, + ) + } + } + } + if (groups.all { it.options.isEmpty() }) { + item(key = "empty") { + EmptyRow( + stringResource( + if (searching) { + R.string.controls_no_matching_models + } else { + R.string.controls_no_available_models + }, + ), + ) + } + } + groups.forEach { group -> + if (searching) { + item(key = "label:${group.provider}") { + Text( + text = group.provider, + color = colors.textDim, + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 5.dp), + ) + } + } + items(group.options, key = { "${group.provider}:${it.modelId}" }) { option -> + ModelRow( + option = option, + active = current != null && + current.provider == option.provider && current.modelId == option.modelId, + showProvider = searching, + onClick = { onSelect(option.provider, option.modelId) }, + ) + } + } + } +} + +@Composable +private fun ProviderRow( + group: ProviderModelGroup, + active: Boolean, + current: Boolean, + onClick: () -> Unit, +) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .background(if (active) colors.selected else colors.panel, RoundedCornerShape(7.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 6.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + ProviderBadge(group.provider, size = 15) + Text( + text = group.provider, + color = if (current) colors.text else colors.textMuted, + fontSize = 12.sp, + fontWeight = if (current) FontWeight.SemiBold else FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Icon( + imageVector = Icons.Filled.KeyboardArrowRight, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(12.dp), + ) + } +} + +@Composable +private fun ModelRow( + option: ModelOption, + active: Boolean, + showProvider: Boolean, + onClick: () -> Unit, +) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .background(if (active) colors.selected else colors.panel, RoundedCornerShape(7.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 6.dp, vertical = 9.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + Box(modifier = Modifier.size(12.dp), contentAlignment = Alignment.Center) { + if (active) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + tint = colors.accent, + modifier = Modifier.size(11.dp), + ) + } + } + Text( + text = option.displayName, + color = if (active) colors.text else colors.textMuted, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (showProvider) { + Text( + text = option.provider, + color = colors.textDim, + fontSize = 10.sp, + maxLines = 1, + ) + } + } +} + +@Composable +private fun LoadingRow() { + val colors = LocalOmpColors.current + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 18.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + ) { + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + strokeWidth = 2.dp, + color = colors.accent, + ) + Text( + text = stringResource(R.string.controls_loading_models), + color = colors.textDim, + fontSize = 12.sp, + ) + } +} + +@Composable +private fun EmptyRow(text: String) { + Text( + text = text, + color = LocalOmpColors.current.textDim, + fontSize = 12.sp, + modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp, vertical = 18.dp), + ) +} + +/** Web `activeProvider`: explicit pick, else the current model's provider, else the first. */ +private fun resolveActiveProvider( + groups: List, + selected: String?, + currentProvider: String?, +): String? { + if (selected != null && groups.any { it.provider == selected }) return selected + if (currentProvider != null && groups.any { it.provider == currentProvider }) return currentProvider + return groups.firstOrNull()?.provider +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/WordPredictor.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/WordPredictor.kt new file mode 100644 index 000000000..5674f0cb1 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/controls/WordPredictor.kt @@ -0,0 +1,204 @@ +package io.github.kahme247.ompweb.ui.composer.controls + +import android.content.Context +import android.content.res.Configuration +import android.hardware.input.InputManager +import android.view.InputDevice +import io.github.kahme247.ompweb.data.ChatSettings +import io.github.kahme247.ompweb.domain.GhostAdvance +import io.github.kahme247.ompweb.domain.GhostInsertion +import io.github.kahme247.ompweb.domain.WORD_PREDICTION_DEBOUNCE_MS +import io.github.kahme247.ompweb.domain.WORD_PREDICTION_MAX_DRAFT_LENGTH +import io.github.kahme247.ompweb.domain.WORD_PREDICTION_UNSUPPORTED_RETRY_MS +import io.github.kahme247.ompweb.domain.WordCompletionMode +import io.github.kahme247.ompweb.domain.WordGhost +import io.github.kahme247.ompweb.domain.acceptGhost +import io.github.kahme247.ompweb.domain.advanceGhost +import io.github.kahme247.ompweb.domain.atLineEnd +import io.github.kahme247.ompweb.domain.isUnsupportedPredictionError +import io.github.kahme247.ompweb.domain.wordCompletionActive +import io.github.kahme247.ompweb.net.ComposerControlsApi +import io.github.kahme247.ompweb.net.parsePredictedSuffix +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +/** + * Which input devices the composer sees. The web's `auto` mode goes by + * `(pointer: fine)`; on Android the equivalent is a hardware keyboard + * (`Configuration.keyboard != KEYBOARD_NOKEYS`) or a connected mouse. + */ +data class WordInputCapabilities( + val hardwareKeyboard: Boolean = false, + val finePointer: Boolean = false, +) + +/** Reads the current device capabilities; cheap enough to call per keystroke. */ +fun wordInputCapabilities(context: Context): WordInputCapabilities { + val app = context.applicationContext + val hardwareKeyboard = + app.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS + val inputManager = app.getSystemService(Context.INPUT_SERVICE) as? InputManager + val finePointer = inputManager?.inputDeviceIds?.any { id -> + val device = inputManager.getInputDevice(id) ?: return@any false + device.sources and InputDevice.SOURCE_MOUSE == InputDevice.SOURCE_MOUSE + } == true + return WordInputCapabilities(hardwareKeyboard, finePointer) +} + +/** + * Composer word prediction: one `predict_word` per settled keystroke burst, + * ghost text only while the caret ends its line, accept/typed-past outcomes + * reported back as `predict_word_feedback`. + * + * Port of `hooks/useWordPrediction.ts`. Predictions never spawn an omp child + * (the route answers `{suffix: null}` without a live process) and a brand-new + * chat sends none at all — there is no session id yet. An omp without + * `predict_word` pauses the engine for a minute instead of asking on every key. + */ +class WordPredictor( + private val api: ComposerControlsApi, + private val settings: ChatSettings, + private val scope: CoroutineScope, + /** Read per keystroke; [wordInputCapabilities] supplies the Android answer. */ + private val capabilities: () -> WordInputCapabilities = { WordInputCapabilities() }, + private val clock: () -> Long = { System.currentTimeMillis() }, +) { + + private val _ghost = MutableStateFlow(null) + + /** The ghost for the latest draft state (text, caret and suffix), or null. */ + val ghost: StateFlow = _ghost.asStateFlow() + + private var sessionId: String? = null + private var lastText: String? = null + private var lastCaret: Int = -1 + private var job: Job? = null + + /** Monotonic token; every draft change invalidates in-flight predictions. */ + private var seq = 0 + private var unsupportedUntil = 0L + + /** + * Feed every draft/caret change. `caret` is the selection start; a ghost + * predicted for another text/caret is carried (typing that matches it) or + * dropped, exactly like `advanceGhost`. + */ + fun onTextChanged(sessionId: String?, text: String, caret: Int) { + if (sessionId != this.sessionId) { + this.sessionId = sessionId + setGhost(null) + lastText = null + lastCaret = -1 + } + val at = caret.coerceIn(0, text.length) + // `onValueChange` and a selection callback can report the same keystroke twice. + if (lastText == text && lastCaret == at) return + lastText = text + lastCaret = at + val token = cancelScheduled() + + var carried: WordGhost? = null + _ghost.value?.let { current -> + val advanced: GhostAdvance = advanceGhost(current, text, at) + if (advanced.typedPast) report(current, accepted = false) + carried = advanced.ghost + } + + val sid = sessionId + val mode = WordCompletionMode.fromWire(settings.snapshot.value.wordCompletion) + val caps = capabilities() + if (sid == null || + !wordCompletionActive(mode, caps.hardwareKeyboard, caps.finePointer) || + !atLineEnd(text, at) + ) { + setGhost(null) + return + } + setGhost(carried) + if (text.length > WORD_PREDICTION_MAX_DRAFT_LENGTH) return + if (clock() < unsupportedUntil) return + job = scope.launch { + delay(WORD_PREDICTION_DEBOUNCE_MS) + if (token != seq) return@launch + try { + val answer = api.agentCommand( + sid, + buildJsonObject { + put("type", "predict_word") + put("text", text) + put("cursor", at) + }, + advisor = false, + ) + val predicted = parsePredictedSuffix(answer) + // A null answer while typing through a shown ghost keeps that ghost. + if (token == seq && predicted != null) { + setGhost(WordGhost(text, at, predicted)) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + if (isUnsupportedPredictionError(e.message)) { + unsupportedUntil = clock() + WORD_PREDICTION_UNSUPPORTED_RETRY_MS + } + } + } + } + + /** + * Take the ghost for insertion (reporting the accept), or null when none is + * showing or it belongs to another draft state. Use the returned + * [GhostInsertion] to replace the draft: its `text`/`caret` are the whole + * field after the insertion, so a stale field cannot splice twice. + */ + fun accept(text: String, caret: Int): GhostInsertion? { + val current = _ghost.value ?: return null + if (current.text != text || current.caret != caret) return null + cancelScheduled() + setGhost(null) + report(current, accepted = true) + return acceptGhost(current) + } + + private fun cancelScheduled(): Int { + seq += 1 + job?.cancel() + job = null + return seq + } + + private fun setGhost(next: WordGhost?) { + _ghost.value = next + } + + private fun report(current: WordGhost, accepted: Boolean) { + val sid = sessionId ?: return + scope.launch { + try { + api.agentCommand( + sid, + buildJsonObject { + put("type", "predict_word_feedback") + put("text", current.text) + put("cursor", current.caret) + put("suggestion", current.suffix) + put("accepted", accepted) + }, + advisor = false, + ) + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + // Feedback is best effort: a lost sample never disturbs typing. + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/AndroidDictationStore.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/AndroidDictationStore.kt new file mode 100644 index 000000000..af2dfb6bd --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/AndroidDictationStore.kt @@ -0,0 +1,113 @@ +package io.github.kahme247.ompweb.ui.composer.dictation + +import android.content.Context +import androidx.core.content.edit +import io.github.kahme247.ompweb.domain.DictationStore +import io.github.kahme247.ompweb.domain.LocalRecording +import io.github.kahme247.ompweb.domain.SttAfter +import java.io.File +import java.security.SecureRandom + +/** + * [DictationStore] on the app's private storage. + * + * The owner token is the web's `omp-web:stt-owner-token`: one identity per + * install that owns the jobs this app uploads, gets the first claim and alone + * gets the send/queue choice back. The kept recording is the local copy the + * retry path re-uploads when the server lost the job (restart, expiry); it + * lives in the cache and is deleted as soon as the transcript is delivered or + * discarded. + */ +class AndroidDictationStore(context: Context) : DictationStore { + + private val appContext = context.applicationContext + private val preferences = appContext.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) + private val directory = File(appContext.cacheDir, DIRECTORY) + + override fun ownerToken(): String { + val existing = preferences.getString(KEY_OWNER_TOKEN, null) + if (!existing.isNullOrBlank()) return existing + val token = randomToken() + preferences.edit { putString(KEY_OWNER_TOKEN, token) } + return token + } + + override fun scratchFile(name: String): File { + ensureDirectory() + return File(directory, name.substringAfterLast('/').substringAfterLast('\\')) + } + + override fun keepRecording(scope: String, file: File, mimeType: String, after: SttAfter) { + // A new recording replaces the previous kept one (web: pendingAudioRef). + clearRecording() + preferences.edit { + putString(KEY_KEPT_SCOPE, scope) + putString(KEY_KEPT_PATH, file.absolutePath) + putString(KEY_KEPT_MIME, mimeType) + putString(KEY_KEPT_AFTER, after.name) + } + } + + override fun savedRecording(scope: String): LocalRecording? { + if (preferences.getString(KEY_KEPT_SCOPE, null) != scope) return null + val path = preferences.getString(KEY_KEPT_PATH, null) ?: return null + val file = File(path) + if (!file.isFile || file.length() == 0L) { + clearRecording() + return null + } + val mimeType = preferences.getString(KEY_KEPT_MIME, null) ?: AUDIO_MP4 + val after = preferences.getString(KEY_KEPT_AFTER, null) + ?.let { name -> SttAfter.entries.firstOrNull { it.name == name } } + ?: SttAfter.Insert + return LocalRecording(file, mimeType, after) + } + + override fun clearRecording() { + val path = preferences.getString(KEY_KEPT_PATH, null) + if (path != null) File(path).delete() + preferences.edit { + remove(KEY_KEPT_PATH) + remove(KEY_KEPT_SCOPE) + remove(KEY_KEPT_MIME) + remove(KEY_KEPT_AFTER) + } + } + + /** + * Delete recordings and playback copies left behind by process death. The + * kept recording (the retry fallback) is not touched. + */ + fun purgeOrphans() { + val kept = preferences.getString(KEY_KEPT_PATH, null)?.let { File(it).absolutePath } + val files = directory.listFiles() ?: return + for (file in files) { + if (file.isFile && file.absolutePath != kept) file.delete() + } + } + + private fun ensureDirectory() { + if (!directory.isDirectory) directory.mkdirs() + } + + private fun randomToken(): String { + val bytes = ByteArray(16) + SecureRandom().nextBytes(bytes) + // Mask each byte: a signed byte would otherwise format as a negative + // long ("ffffff80") and stretch the token past the server's 128-char cap. + return bytes.joinToString("") { byte -> "%02x".format(byte.toInt() and 0xff) } + } + + private companion object { + const val PREFERENCES = "ompweb-dictation" + + /** The web's sessionStorage key, kept verbatim so the intent is obvious. */ + const val KEY_OWNER_TOKEN = "omp-web:stt-owner-token" + const val KEY_KEPT_PATH = "kept-path" + const val KEY_KEPT_SCOPE = "kept-scope" + const val KEY_KEPT_MIME = "kept-mime" + const val KEY_KEPT_AFTER = "kept-after" + const val DIRECTORY = "dictation" + const val AUDIO_MP4 = "audio/mp4" + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/AndroidOnDeviceStt.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/AndroidOnDeviceStt.kt new file mode 100644 index 000000000..076291765 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/AndroidOnDeviceStt.kt @@ -0,0 +1,185 @@ +package io.github.kahme247.ompweb.ui.composer.dictation + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.speech.RecognitionListener +import android.speech.RecognizerIntent +import android.speech.SpeechRecognizer +import android.os.Bundle + +/** + * [OnDeviceStt] over Android's [SpeechRecognizer]. + * + * Only the on-device recognizer is ever built (`createOnDeviceSpeechRecognizer`, + * API 31+); without an installed on-device model the class reports + * [OnDeviceSttError.NoRecognizer] instead of falling back to the default + * recognizer, which may send the audio over the network while the UI promises + * on-device processing. The intent asks for partial results, a short silence + * window (the utterance ends itself) and offline preference. + * + * `RECORD_AUDIO` is requested by the composer before dictation starts, so this + * class never asks for permissions itself. + */ +class AndroidOnDeviceStt(context: Context) : OnDeviceStt { + + private val appContext = context.applicationContext + private val main = Handler(Looper.getMainLooper()) + + private var recognizer: SpeechRecognizer? = null + private var listener: OnDeviceStt.Listener? = null + + override fun isOnDeviceAvailable(): Boolean = runCatching { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + SpeechRecognizer.isOnDeviceRecognitionAvailable(appContext) + }.getOrDefault(false) + + // The composer requests RECORD_AUDIO before dictation starts; lint cannot + // see across that call boundary. + @SuppressLint("MissingPermission") + override fun start(languageTag: String, listener: OnDeviceStt.Listener) { + if (!onMainThread()) { + main.post { start(languageTag, listener) } + return + } + release() + this.listener = listener + if (!isOnDeviceAvailable()) { + this.listener = null + listener.onError(OnDeviceSttError.NoRecognizer) + return + } + // The recognizer factory itself needs API 31: spell the level out so + // lint sees the guard instead of only the `isOnDeviceAvailable` call. + val created = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + runCatching { + SpeechRecognizer.createOnDeviceSpeechRecognizer(appContext) + }.getOrNull() + } else { + null + } + if (created == null) { + this.listener = null + listener.onError(OnDeviceSttError.NoRecognizer) + return + } + recognizer = created + created.setRecognitionListener(RecognitionAdapter()) + val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) + putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true) + putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1) + putExtra(RecognizerIntent.EXTRA_LANGUAGE, languageTag) + putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, appContext.packageName) + // End the utterance on a pause instead of listening until the tap. + // The extras are spelled as integers: recognizer implementations + // read them with getIntExtra (a Long silently falls back to their + // default and would never end the take on silence). + putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS, MIN_SPEECH_MS) + putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, SILENCE_MS) + putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS, SILENCE_MS) + putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, true) + } + val started = runCatching { created.startListening(intent) }.isSuccess + if (!started) { + release() + listener.onError(OnDeviceSttError.NoRecognizer) + } + } + + override fun stop() { + onMain { current -> runCatching { current.stopListening() } } + } + + override fun cancel() { + onMain { current -> runCatching { current.cancel() } } + release() + } + + override fun release() { + if (!onMainThread()) { + main.post { release() } + return + } + val current = recognizer + recognizer = null + listener = null + if (current != null) { + runCatching { current.setRecognitionListener(null) } + runCatching { current.destroy() } + } + } + + private fun onMain(block: (SpeechRecognizer) -> Unit) { + if (!onMainThread()) { + main.post { onMain(block) } + return + } + recognizer?.let(block) + } + + private fun onMainThread(): Boolean = Looper.myLooper() == Looper.getMainLooper() + + /** Forwards the callbacks, then destroys the recognizer exactly once. */ + private inner class RecognitionAdapter : RecognitionListener { + + override fun onPartialResults(partialResults: Bundle?) { + val text = firstResult(partialResults) ?: return + if (text.isBlank()) return + listener?.onPartial(text) + } + + override fun onResults(results: Bundle?) { + val text = firstResult(results).orEmpty() + val target = listener + release() + target?.onFinal(text) + } + + override fun onError(error: Int) { + val target = listener + release() + target?.onError(errorOf(error)) + } + + override fun onReadyForSpeech(params: Bundle?) = Unit + + override fun onBeginningOfSpeech() = Unit + + override fun onRmsChanged(rmsdB: Float) = Unit + + override fun onBufferReceived(buffer: ByteArray?) = Unit + + override fun onEndOfSpeech() = Unit + + override fun onEvent(eventType: Int, params: Bundle?) = Unit + } + + private fun firstResult(bundle: Bundle?): String? = + runCatching { + bundle?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.firstOrNull() + }.getOrNull() + + private companion object { + const val MIN_SPEECH_MS = 500 + const val SILENCE_MS = 1_500 + + fun errorOf(error: Int): OnDeviceSttError = when (error) { + SpeechRecognizer.ERROR_NO_MATCH, + SpeechRecognizer.ERROR_SPEECH_TIMEOUT, + -> OnDeviceSttError.NoSpeech + SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> OnDeviceSttError.Permission + SpeechRecognizer.ERROR_NETWORK, + SpeechRecognizer.ERROR_NETWORK_TIMEOUT, + -> OnDeviceSttError.Offline + SpeechRecognizer.ERROR_LANGUAGE_NOT_SUPPORTED, + SpeechRecognizer.ERROR_LANGUAGE_UNAVAILABLE, + -> OnDeviceSttError.LanguageUnavailable + SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> OnDeviceSttError.Busy + else -> OnDeviceSttError.Failed + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/DictationViewModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/DictationViewModel.kt new file mode 100644 index 000000000..dc92d185a --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/DictationViewModel.kt @@ -0,0 +1,995 @@ +package io.github.kahme247.ompweb.ui.composer.dictation + +import android.annotation.SuppressLint +import android.content.Context +import android.media.MediaPlayer +import android.media.MediaRecorder +import android.os.SystemClock +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.data.SettingsSnapshot +import io.github.kahme247.ompweb.domain.DictationEngine +import io.github.kahme247.ompweb.domain.DictationEnginePreference +import io.github.kahme247.ompweb.domain.DictationMachine +import io.github.kahme247.ompweb.domain.DictationPhase +import io.github.kahme247.ompweb.domain.DictationPlayback +import io.github.kahme247.ompweb.domain.DictationResult +import io.github.kahme247.ompweb.domain.DictationState +import io.github.kahme247.ompweb.domain.DictationStep +import io.github.kahme247.ompweb.domain.DictationStore +import io.github.kahme247.ompweb.domain.LiveDictationBuffer +import io.github.kahme247.ompweb.domain.LocalRecording +import io.github.kahme247.ompweb.domain.SttAfter +import io.github.kahme247.ompweb.domain.TextInsertion +import io.github.kahme247.ompweb.domain.dictationLanguageTag +import io.github.kahme247.ompweb.domain.resolveDictationEngine +import io.github.kahme247.ompweb.net.ApiError +import io.github.kahme247.ompweb.net.SttApi +import io.github.kahme247.ompweb.net.SttRetryOutcome +import io.github.kahme247.ompweb.net.SttStartOutcome +import io.github.kahme247.ompweb.net.captureApi +import io.github.kahme247.ompweb.net.userMessage +import java.io.File +import java.util.Locale +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * The words this flow shows for its own failures (server messages pass + * through unchanged). Defaults keep the view model usable in tests; + * production passes [from]. + */ +data class DictationStrings( + val micDenied: String = "Microphone access is required for dictation", + val micUnavailable: String = "Recording is not available on this device", + val noSpeech: String = "No speech detected", + val failed: String = "Transcription failed", + val timedOut: String = "Transcription timed out", + val jobMissing: String = "Transcription job not found", + val busy: String = "Too many transcriptions in progress", + val uploadInterrupted: String = "Recording not transcribed yet", + val noRecognizer: String = + "Speech recognition on this phone is not available (needs Android 12+ and the offline " + + "language pack). Use server transcription or install the language pack.", + val offline: String = "On-device speech recognition could not run. Try again.", + val languageUnavailable: String = "This language is not installed for on-device recognition", + val recognizerBusy: String = "Speech recognition is busy", + val recognizerFailed: String = "Speech recognition failed", +) { + companion object { + fun from(context: Context): DictationStrings { + val app = context.applicationContext + return DictationStrings( + micDenied = app.getString(R.string.dictation_error_mic_denied), + micUnavailable = app.getString(R.string.dictation_error_mic_unavailable), + noSpeech = app.getString(R.string.dictation_error_no_speech), + failed = app.getString(R.string.dictation_error_failed), + timedOut = app.getString(R.string.dictation_error_timeout), + jobMissing = app.getString(R.string.dictation_error_job_missing), + busy = app.getString(R.string.dictation_error_busy), + uploadInterrupted = app.getString(R.string.dictation_error_upload_interrupted), + noRecognizer = app.getString(R.string.dictation_error_no_recognizer), + offline = app.getString(R.string.dictation_error_offline), + languageUnavailable = app.getString(R.string.dictation_error_language), + recognizerBusy = app.getString(R.string.dictation_error_recognizer_busy), + recognizerFailed = app.getString(R.string.dictation_error_recognizer_failed), + ) + } + + } +} + +/** + * Dictation for one composer: records with [MediaRecorder], hands the audio to + * the server job of `/api/stt`, follows it, and delivers exactly one transcript + * through [results]. The ownership/claim/retry/timeout rules live in the pure + * [DictationMachine]; this class owns the Android recorders, the HTTP calls and + * the timers. + * + * Port of `hooks/useDictation.ts`: poll 500 ms, adopt this scope's newest job + * every 4 s while visible and idle, claim with a per-instance token (the owner + * token identifies the app installation), 6-minute follow deadline, retry with + * the server's kept audio and fall back to the local copy. + * + * With "Dictation engine" On-device (or Auto without a connected server) the + * recorder and the job protocol are bypassed: Android's [OnDeviceStt] feeds + * partial transcripts that [LiveDictationBuffer] splices into the composer at + * the caret, ending on silence or a tap. Server dictation stays exactly as it + * was. + */ +class DictationViewModel( + private val api: SttApi, + private val store: DictationStore, + private val scope: CoroutineScope, + private val strings: DictationStrings = DictationStrings(), + /** Monotonic clock for the machine's deadlines; JVM tests replace the Android one. */ + private val clock: () -> Long = { SystemClock.elapsedRealtime() }, + /** Settings snapshot: engine choice, language, whether a server is configured. */ + private val settings: StateFlow? = null, + /** The phone's own recognizer; null (JVM tests) keeps on-device dictation unavailable. */ + private val onDevice: OnDeviceStt? = null, + /** Device locale for the default recognition language. */ + private val deviceLocale: Locale = Locale.getDefault(), +) { + + private val machine = DictationMachine(clock) + + /** Per composer instance: only the first claim token gets the transcript. */ + private val claimToken: String = UUID.randomUUID().toString().replace("-", "") + + private val ownerToken: String = store.ownerToken() + + private val _state = MutableStateFlow(DictationState.Idle) + val state: StateFlow = _state.asStateFlow() + + private val _playback = MutableStateFlow(DictationPlayback.Idle) + val playback: StateFlow = _playback.asStateFlow() + + // A buffered channel, not a lossy SharedFlow: the transcript is user data, + // so it must survive a composition gap between delivery and the composer's + // collector instead of being dropped. + private val resultChannel = Channel(capacity = Channel.BUFFERED) + val results: Flow = resultChannel.receiveAsFlow() + + /** Composer scope: a session id or `new:`. */ + private var scopeKey: String? = null + private var visible = true + + private var recorder: MediaRecorder? = null + private var recordingFile: File? = null + private var localRecording: LocalRecording? = null + /** Downloaded server copy for playback; deleted the moment playback ends. */ + private var playbackFile: File? = null + private var level = 0f + + private var tickerJob: Job? = null + private var followJob: Job? = null + private var adoptJob: Job? = null + private var retryJob: Job? = null + + /** + * The upload in flight, if any. Uploads are never aborted (web + * `uploadRef`): once sent, the server may already have started the job, so + * its 202 is always read. [UploadToken.discard] ends that job, [detached] + * leaves it adoptable for this scope. + */ + private var uploadToken: UploadToken? = null + private var playbackJob: Job? = null + private var player: MediaPlayer? = null + + /** The server answered 501 on an upload: `Auto` uses the phone from then on. */ + private var serverSttUnconfigured = false + + // --- On-device recognition --------------------------------------------- + + private val liveBuffer = LiveDictationBuffer() + private var liveActive = false + private var livePartial = "" + private var liveStartedAt = 0L + private var liveJob: Job? = null + /** Safety net when the recognizer never answers a stop/cancel. */ + private var liveStopJob: Job? = null + + /** The composer field that receives the live partials; set by its owner. */ + private var liveFieldText: () -> String = { "" } + private var liveFieldCaret: () -> Int = { 0 } + private var liveFieldApply: ((String, Int) -> Unit)? = null + + private class UploadToken { + @Volatile var discard = false + @Volatile var detached = false + } + + /** + * Connects the composer field that receives live on-device partials. The + * view model never holds the field itself: it reads text and caret right + * before each edit and hands the finished text/caret to [apply]. + */ + fun attachLiveField(text: () -> String, caret: () -> Int, apply: (String, Int) -> Unit) { + liveFieldText = text + liveFieldCaret = caret + liveFieldApply = apply + } + + // --- Screen lifecycle --------------------------------------------------- + + /** Binds the composer scope; another scope's job is left adoptable on the server. */ + fun bindScope(draftKey: String) { + if (scopeKey == draftKey) return + scopeKey = draftKey + detachFromScope() + machine.reset() + publish() + startAdoptLoop() + } + + fun setVisible(visible: Boolean) { + this.visible = visible + if (visible) { + startAdoptLoop() + } else if (liveActive) { + // A backgrounded composer cannot show the strip; keep what was + // spoken so far as draft text. + commitLive(spoken = livePartial) + } + } + + private fun detachFromScope() { + // The composer already switched to the new draft when bindScope runs, + // so restoring here would paste the live text into another session's + // draft. Drop what was not yet committed instead. + cancelLive(restore = false) + followJob?.cancel() + followJob = null + adoptJob?.cancel() + adoptJob = null + retryJob?.cancel() + retryJob = null + // An upload in flight is not aborted (web): the server may already have + // started its job, and the job stays adoptable for this scope. + uploadToken?.detached = true + uploadToken = null + tickerJob?.cancel() + tickerJob = null + stopRecorder(deleteFile = true) + stopPlayback() + localRecording = null + } + + // --- Capture ------------------------------------------------------------ + + fun start(after: SttAfter) { + if (liveActive) return + if ( + machine.phase == DictationPhase.Recording || + machine.phase == DictationPhase.Uploading || + machine.phase == DictationPhase.Transcribing + ) { + return + } + // A new recording replaces the job the deck was showing (web: start()). + machine.jobId?.let { discardRemote(it) } + retryJob?.cancel() + retryJob = null + stopPlayback() + tickerJob?.cancel() + stopRecorder(deleteFile = true) + localRecording = null + store.clearRecording() + if (nextEngine() == DictationEngine.ON_DEVICE) { + startOnDevice() + return + } + if (!machine.start(after)) return + if (!startRecorder()) { + machine.fail(strings.micUnavailable) + publish() + return + } + publish() + startTicker() + } + + /** + * Which engine the next capture uses: the setting, resolved against the + * connected server (see [resolveDictationEngine]). Public for the mic + * rationale, which must not claim the voice goes to the server when the + * phone transcribes it. + */ + fun nextEngine(): DictationEngine { + val snapshot = settings?.value + return resolveDictationEngine( + preference = DictationEnginePreference.fromWire(snapshot?.dictationEngine), + // No settings (JVM tests) means the server path, exactly as before. + serverConfigured = snapshot == null || snapshot.origin != null, + serverSttUnconfigured = serverSttUnconfigured, + onDeviceAvailable = onDeviceModelAvailable, + ) + } + + /** + * Cached per view model: the recognizer query is a binder round trip and + * must not run on every composition of the mic rationale. + */ + private val onDeviceModelAvailable: Boolean by lazy { + val speech = onDevice ?: return@lazy false + runCatching { speech.isOnDeviceAvailable() }.getOrDefault(false) + } + + /** + * Whether an installed on-device model can serve dictation right now. The + * mic rationale must not promise on-device processing when [start] would + * fail with [DictationStrings.noRecognizer]. + */ + val onDeviceReady: Boolean get() = onDeviceModelAvailable + + private fun startOnDevice() { + val speech = onDevice + if (speech == null || !runCatching { speech.isOnDeviceAvailable() }.getOrDefault(false)) { + machine.reset() + machine.fail(strings.noRecognizer) + publish() + return + } + machine.reset() + liveBuffer.reset() + livePartial = "" + liveStartedAt = clock() + liveActive = true + publish() + val language = dictationLanguageTag(settings?.value?.dictationLanguage, deviceLocale) + speech.start(language, liveListener) + startLiveTicker() + } + + /** Recognizer callbacks: main thread, in order, exactly one final or error. */ + private val liveListener = object : OnDeviceStt.Listener { + override fun onPartial(text: String) { + if (!liveActive) return + val next = text.trim() + livePartial = next + applyLive(liveBuffer.onPartial(liveFieldText(), liveFieldCaret(), next)) + publish() + } + + override fun onFinal(text: String) { + if (!liveActive) return + val spoken = text.trim().ifEmpty { livePartial } + commitLive(spoken) + } + + override fun onError(error: OnDeviceSttError) { + if (!liveActive) return + val spoken = livePartial + val message = when (error) { + OnDeviceSttError.NoRecognizer -> strings.noRecognizer + OnDeviceSttError.Permission -> strings.micDenied + OnDeviceSttError.Offline -> strings.offline + OnDeviceSttError.LanguageUnavailable -> strings.languageUnavailable + OnDeviceSttError.Busy -> strings.recognizerBusy + OnDeviceSttError.NoSpeech -> if (spoken.isEmpty()) strings.noSpeech else null + OnDeviceSttError.Failed -> strings.recognizerFailed + } + commitLive(spoken = spoken, message = message) + } + } + + /** + * Ends a live capture: [spoken] replaces the visible live text (or is + * inserted once when no partial ever arrived), then the state is Idle or + * the [message] failure. + */ + private fun commitLive(spoken: String, message: String? = null) { + if (!liveActive) return + liveActive = false + val insertion = liveBuffer.onFinal(liveFieldText(), liveFieldCaret(), spoken) + applyLive(insertion) + liveBuffer.reset() + livePartial = "" + liveJob?.cancel() + liveJob = null + liveStopJob?.cancel() + liveStopJob = null + onDevice?.release() + if (message != null) { + machine.fail(message) + } else if (spoken.isBlank() && insertion == null) { + machine.fail(strings.noSpeech) + } + publish() + } + + /** Ends a live capture without keeping anything (scope switch, cancel). */ + private fun cancelLive(restore: Boolean = true) { + if (!liveActive) return + liveActive = false + runCatching { onDevice?.cancel() } + if (restore) applyLive(liveBuffer.onCancel(liveFieldText())) + liveBuffer.reset() + livePartial = "" + liveJob?.cancel() + liveJob = null + liveStopJob?.cancel() + liveStopJob = null + } + + private fun applyLive(insertion: TextInsertion?) { + if (insertion == null) return + liveFieldApply?.invoke(insertion.text, insertion.caret) + } + + private fun startLiveTicker() { + liveJob?.cancel() + liveJob = scope.launch { + while (isActive && liveActive) { + publish() + delay(CAPTURE_TICK_MS) + } + } + } + + fun pause() { + if (!machine.pause()) return + runCatching { recorder?.pause() } + publish() + } + + fun resume() { + if (!machine.resume()) return + runCatching { recorder?.resume() } + publish() + } + + /** Stops the capture and transcribes with the intent chosen at start. */ + fun stop() { + if (liveActive) { + // The recognizer ends the utterance itself on silence; a tap asks + // it to wrap up and keeps the partial if no answer ever comes. + if (liveStopJob?.isActive == true) return + runCatching { onDevice?.stop() } + liveStopJob = scope.launch { + delay(LIVE_RESULT_TIMEOUT_MS) + if (liveActive) commitLive(spoken = livePartial) + } + return + } + finish(machine.after) + } + + /** + * Stops the capture with an explicit intent, so the composer's Send/Queue + * button can decide at stop time (web `stopAndFinishDictation`). + */ + fun finish(after: SttAfter) { + if (liveActive) { + // On-device text is always inserted at the caret; the field sends. + stop() + return + } + if (machine.phase != DictationPhase.Recording) return + machine.finishRecording(after) + tickerJob?.cancel() + tickerJob = null + val stopped = stopRecorder() + val file = recordingFile + recordingFile = null + publish() + if (!stopped || file == null || !file.isFile || file.length() == 0L) { + file?.delete() + machine.fail(strings.noSpeech) + publish() + return + } + val recording = LocalRecording(file, AUDIO_MIME_TYPE, machine.after) + localRecording = recording + store.keepRecording(scopeKey.orEmpty(), file, AUDIO_MIME_TYPE, machine.after) + machine.setLocalAudio(true) + publish() + upload(recording, machine.after) + } + + fun cancel() { + if (liveActive) { + cancelLive() + machine.reset() + publish() + } + val jobId = machine.jobId + tickerJob?.cancel() + tickerJob = null + followJob?.cancel() + followJob = null + retryJob?.cancel() + retryJob = null + // An upload in flight ends its job once the server answers with its id. + uploadToken?.discard = true + uploadToken = null + stopRecorder(deleteFile = true) + stopPlayback() + localRecording = null + store.clearRecording() + machine.cancelCapture() + if (jobId != null) discardRemote(jobId) + publish() + } + + fun discard() = cancel() + + /** + * Logout / server switch: stop capture and playback, delete the kept + * recording and discard any server job this composer still owns. + */ + fun clearSessionData() = cancel() + + fun onPermissionDenied() { + machine.fail(strings.micDenied) + publish() + } + + // --- Follow ------------------------------------------------------------- + + private fun upload(recording: LocalRecording, after: SttAfter) { + val key = scopeKey + val token = UploadToken() + uploadToken = token + scope.launch { + val bytes = withContext(Dispatchers.IO) { + runCatching { recording.file.readBytes() }.getOrNull() + } + if (bytes == null || bytes.isEmpty()) { + if (uploadToken === token) uploadToken = null + if (token.discard || token.detached) return@launch + machine.fail(strings.noSpeech) + publish() + return@launch + } + val answer = captureApi { + api.start( + scope = key, + owner = ownerToken, + after = after.takeIf { it != SttAfter.Insert }, + audio = bytes, + fileName = recording.file.name, + mimeType = recording.mimeType, + ) + } + if (uploadToken === token) uploadToken = null + // Cancelled during the upload: the server job that just started is + // discarded, so no other composer picks it up (web `upload.discard`). + if (token.discard) { + (answer.getOrNull() as? SttStartOutcome.Accepted)?.let { discardRemote(it.jobId) } + return@launch + } + // Left this scope meanwhile: the server job stays adoptable. + if (token.detached || scopeKey != key) return@launch + when (val outcome = answer.getOrNull()) { + is SttStartOutcome.Accepted -> { + machine.uploadAccepted(outcome.jobId) + publish() + follow(outcome.jobId) + } + is SttStartOutcome.Rejected -> { + if (outcome.notConfigured) serverSttUnconfigured = true + machine.fail(outcome.message) + publish() + } + null -> { + machine.fail(answer.exceptionOrNull()?.userMessage() ?: strings.failed) + publish() + } + } + } + } + + private fun follow(jobId: String) { + followJob?.cancel() + followJob = scope.launch { + var claiming = false + while (isActive) { + if (machine.phase != DictationPhase.Transcribing || machine.jobId != jobId) return@launch + if (claiming) { + val answer = captureApi { api.claim(jobId, claimToken, ownerToken) } + if (!isActive) return@launch + if (answer.isFailure) { + fatalMessage(answer.exceptionOrNull())?.let { message -> + machine.fail(message) + publish() + return@launch + } + } else { + val step = machine.takeClaim(answer.getOrNull()) + claiming = machine.awaitingClaim + if (settle(step)) return@launch + } + delay(DictationMachine.STT_POLL_MS) + continue + } + delay(DictationMachine.STT_POLL_MS) + if (!isActive) return@launch + val answer = captureApi { api.job(jobId) } + if (answer.isFailure) { + fatalMessage(answer.exceptionOrNull())?.let { message -> + machine.fail(message) + publish() + return@launch + } + continue + } + val step = machine.observe(answer.getOrNull()) + if (step is DictationStep.Claim) claiming = true + if (settle(step)) return@launch + } + } + } + + /** Applies a poll/claim decision; true ends the follow loop. */ + private fun settle(step: DictationStep): Boolean = when (step) { + DictationStep.Wait, DictationStep.Claim -> false + is DictationStep.Delivered -> { + localRecording = null + store.clearRecording() + stopPlayback() + publish() + resultChannel.trySend(DictationResult(step.text, step.after, step.isOwner)) + true + } + is DictationStep.Failed -> { + // Nothing can retry from this failure: the kept recording and its + // playback copy are dead weight. + if (!machine.canRetry) deleteLocalAudio() + publish() + true + } + DictationStep.StandDown -> { + localRecording = null + store.clearRecording() + publish() + true + } + } + + /** Drop the local recording (and its playback copy) once no retry can use it. */ + private fun deleteLocalAudio() { + stopPlayback() + localRecording?.file?.delete() + localRecording = null + store.clearRecording() + machine.setLocalAudio(false) + } + + // --- Adoption ----------------------------------------------------------- + + private fun startAdoptLoop() { + adoptJob?.cancel() + val key = scopeKey ?: return + adoptJob = scope.launch { + while (isActive) { + if (visible && !liveActive && machine.canAdopt() && scopeKey == key) { + val jobs = captureApi { api.jobs(key) }.getOrNull() + if (jobs != null && visible && !liveActive && scopeKey == key && machine.canAdopt()) { + if (machine.adopt(jobs)) { + publish() + if (machine.phase == DictationPhase.Transcribing) { + machine.jobId?.let { follow(it) } + } + } else { + restoreLocal(key) + } + } + } + delay(DictationMachine.STT_ADOPT_MS) + } + } + } + + /** + * Nothing on the server for this scope, but a recording of it was kept + * (the upload died with the process): offer the local re-upload. + */ + private fun restoreLocal(key: String) { + val saved = store.savedRecording(key) ?: return + localRecording = saved + machine.restoreRecording(saved.after, strings.uploadInterrupted) + publish() + } + + // --- Retry -------------------------------------------------------------- + + fun retry() { + if (machine.phase != DictationPhase.Failed || retryJob?.isActive == true) return + val jobId = machine.serverRetryJobId() + if (jobId == null) { + uploadLocalOrFail() + return + } + val key = scopeKey + retryJob = scope.launch { + val answer = captureApi { api.retry(jobId, ownerToken) } + // Cancelled, restarted or the scope left while the request was in + // flight: the answer is stale (web checks `cancelledRef`/`jobIdRef`). + if (machine.phase != DictationPhase.Failed || machine.jobId != jobId || scopeKey != key) { + return@launch + } + when (val outcome = answer.getOrNull()) { + is SttRetryOutcome.Accepted -> { + val step = machine.retryAccepted(outcome.job) + if (step != null) { + settle(step) + return@launch + } + publish() + follow(outcome.job.id) + } + SttRetryOutcome.Missing -> { + machine.retryJobMissing() + // A kept local copy can still be re-uploaded even when the + // adopted job never set `hasLocalAudio` (process restart). + uploadLocalOrFail() + } + SttRetryOutcome.Busy -> { + machine.retryRejected(strings.busy) + publish() + } + is SttRetryOutcome.Rejected -> { + if (outcome.notConfigured) serverSttUnconfigured = true + machine.retryRejected(outcome.message) + publish() + } + null -> { + machine.retryRejected(answer.exceptionOrNull()?.userMessage() ?: strings.failed) + publish() + } + } + } + } + + private fun uploadLocalOrFail() { + val key = scopeKey ?: return + val recording = localRecording ?: store.savedRecording(key) + if (recording == null) { + machine.fail(strings.jobMissing) + publish() + return + } + localRecording = recording + machine.setLocalAudio(true) + if (machine.beginUploadRetry()) { + publish() + upload(recording, recording.after) + } + } + + // --- Playback ----------------------------------------------------------- + + /** A local recording or the server's kept copy exists for playback. */ + fun canPlay(): Boolean = machine.hasLocalAudio || machine.jobId != null + + fun playPause() { + val state = _playback.value + if (state is DictationPlayback.Preparing) return + val existing = player + if (existing != null) { + if (state is DictationPlayback.Playing && !state.paused) { + runCatching { existing.pause() } + playbackJob?.cancel() + _playback.value = state.copy(paused = true) + } else { + runCatching { existing.start() } + val duration = state.durationMsOrZero() + _playback.value = DictationPlayback.Playing( + positionMs = state.positionMsOrZero().coerceAtLeast(0), + durationMs = duration, + paused = false, + ) + startPlaybackTicker() + } + return + } + val jobId = machine.jobId + val local = localRecording + if (local == null && jobId == null) return + _playback.value = DictationPlayback.Preparing + scope.launch { + val file = local?.file?.takeIf { it.isFile } ?: downloadRecording(jobId) + if (file == null) { + _playback.value = DictationPlayback.Idle + return@launch + } + preparePlayer(file, temporary = local == null) + } + } + + fun stopPlayback() { + playbackJob?.cancel() + playbackJob = null + val current = player + player = null + if (current != null) { + runCatching { current.stop() } + runCatching { current.release() } + } + playbackFile?.delete() + playbackFile = null + _playback.value = DictationPlayback.Idle + } + + private suspend fun downloadRecording(jobId: String?): File? { + if (jobId == null) return null + val audio = captureApi { api.audio(jobId) }.getOrNull() ?: return null + val extension = when (audio.mimeType) { + "audio/webm" -> "webm" + "audio/ogg" -> "ogg" + "audio/wav", "audio/x-wav" -> "wav" + else -> "m4a" + } + val file = store.scratchFile("job-$jobId.$extension") + return withContext(Dispatchers.IO) { + runCatching { file.writeBytes(audio.bytes) }.map { file }.getOrNull() + } + } + + private fun preparePlayer(file: File, temporary: Boolean) { + stopPlayback() + if (temporary) playbackFile = file + val created = MediaPlayer() + player = created + created.setOnPreparedListener { prepared -> + if (player !== prepared) return@setOnPreparedListener + prepared.start() + _playback.value = DictationPlayback.Playing(0L, prepared.duration.toLong().coerceAtLeast(0L), false) + startPlaybackTicker() + } + created.setOnCompletionListener { + if (player === created) stopPlayback() + } + created.setOnErrorListener { _, _, _ -> + if (player === created) stopPlayback() + true + } + try { + created.setDataSource(file.absolutePath) + created.prepareAsync() + } catch (_: Exception) { + stopPlayback() + } + } + + private fun startPlaybackTicker() { + playbackJob?.cancel() + playbackJob = scope.launch { + while (isActive) { + val current = _playback.value as? DictationPlayback.Playing ?: return@launch + if (current.paused) return@launch + val position = runCatching { player?.currentPosition?.toLong() ?: 0L }.getOrDefault(0L) + _playback.value = current.copy(positionMs = position) + delay(PLAYBACK_TICK_MS) + } + } + } + + // --- Recorder ----------------------------------------------------------- + + // [MicButton] requests RECORD_AUDIO (with a rationale) before it calls + // [start]; lint cannot see across that call boundary. + @SuppressLint("MissingPermission") + @Suppress("DEPRECATION") + private fun startRecorder(): Boolean { + val file = store.scratchFile("rec-${System.currentTimeMillis()}.m4a") + var created: MediaRecorder? = null + return try { + created = MediaRecorder() + created.setAudioSource(MediaRecorder.AudioSource.MIC) + created.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + created.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + created.setAudioSamplingRate(44_100) + created.setAudioEncodingBitRate(96_000) + created.setAudioChannels(1) + created.setOutputFile(file.absolutePath) + created.prepare() + created.start() + recorder = created + recordingFile = file + level = 0f + true + } catch (_: Exception) { + runCatching { created?.release() } + file.delete() + recorder = null + recordingFile = null + false + } + } + + /** Stops and releases the recorder; false when it never produced audio. */ + private fun stopRecorder(deleteFile: Boolean = false): Boolean { + val current = recorder + recorder = null + if (current == null) { + if (deleteFile) recordingFile?.delete() + return false + } + var captured = false + try { + current.stop() + captured = true + } catch (_: RuntimeException) { + // Stopped before the first frame: the file carries no audio. + } + runCatching { current.release() } + if (deleteFile) { + recordingFile?.delete() + recordingFile = null + } + return captured + } + + private fun startTicker() { + tickerJob?.cancel() + tickerJob = scope.launch { + while (isActive && machine.phase == DictationPhase.Recording) { + val amplitude = runCatching { recorder?.maxAmplitude ?: 0 }.getOrDefault(0) + level = if (machine.paused) 0f else (amplitude / MAX_AMPLITUDE).coerceIn(0f, 1f) + if (machine.elapsedMs() >= DictationMachine.MAX_RECORDING_MS && !machine.paused) { + finish(machine.after) + return@launch + } + publish() + delay(CAPTURE_TICK_MS) + } + } + } + + // --- Plumbing ----------------------------------------------------------- + + private fun discardRemote(jobId: String) { + scope.launch { captureApi { api.discard(jobId) } } + } + + private fun publish() { + if (liveActive) { + _state.value = DictationState.Live( + elapsedMs = (clock() - liveStartedAt).coerceAtLeast(0L), + ) + return + } + _state.value = when (machine.phase) { + DictationPhase.Idle -> DictationState.Idle + DictationPhase.Recording -> DictationState.Recording( + elapsedMs = machine.elapsedMs(), + level = level, + paused = machine.paused, + ) + DictationPhase.Uploading -> DictationState.Uploading(machine.finalElapsedMs) + DictationPhase.Transcribing -> DictationState.Transcribing(machine.finalElapsedMs) + DictationPhase.Failed -> DictationState.Failed(displayMessage(machine.message), machine.canRetry) + DictationPhase.Gone -> DictationState.Gone + } + } + + private fun displayMessage(message: String?): String = when (message) { + null -> strings.failed + DictationMachine.TIMED_OUT -> strings.timedOut + DictationMachine.JOB_NOT_FOUND -> strings.jobMissing + DictationMachine.TRANSCRIPTION_FAILED -> strings.failed + DictationMachine.NO_SPEECH -> strings.noSpeech + else -> message + } + + /** null = transient (keep polling); a message = give up with it. */ + private fun fatalMessage(error: Throwable?): String? = when (error) { + null -> null + is ApiError.Network, is ApiError.ServerMisconfigured, is ApiError.SessionSlow -> null + is ApiError.ServerResponse -> + if (error.status == 408 || error.status == 429 || error.status >= 500) null else error.userMessage() + is ApiError.Http -> + if (error.code == 408 || error.code == 429 || error.code >= 500) null else error.userMessage() + else -> error.userMessage() + } + + private fun DictationPlayback.durationMsOrZero(): Long = + (this as? DictationPlayback.Playing)?.durationMs ?: 0L + + private fun DictationPlayback.positionMsOrZero(): Long = + (this as? DictationPlayback.Playing)?.positionMs ?: 0L + + private companion object { + const val AUDIO_MIME_TYPE = "audio/mp4" + const val CAPTURE_TICK_MS = 100L + const val PLAYBACK_TICK_MS = 200L + const val MAX_AMPLITUDE = 32_767f + + /** How long a stopped on-device recognition may take to answer. */ + const val LIVE_RESULT_TIMEOUT_MS = 2_500L + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/LiveDictationStrip.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/LiveDictationStrip.kt new file mode 100644 index 000000000..776703643 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/LiveDictationStrip.kt @@ -0,0 +1,76 @@ +package io.github.kahme247.ompweb.ui.composer.dictation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.DictationState +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily + +/** + * The strip under the composer field while on-device recognition listens. The + * field itself stays visible and receives the live transcript at its caret; + * the strip only carries the state and the two exits: stop (keeps the text) + * and cancel (removes the live text again). + */ +@Composable +fun LiveDictationStrip( + state: DictationState.Live, + onStop: () -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + Row( + modifier = modifier + .fillMaxWidth() + .padding(top = 2.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Box( + Modifier + .size(8.dp) + .clip(CircleShape) + .background(colors.statusError), + ) + Text( + text = stringResource(R.string.dictation_live_listening), + style = MaterialTheme.typography.labelMedium, + color = colors.textMuted, + ) + Text( + text = formatTime(state.elapsedMs), + color = colors.text, + fontFamily = OmpMonoFontFamily, + fontSize = 12.sp, + ) + Box(Modifier.weight(1f)) + TextButton(onClick = onStop) { + Text(stringResource(R.string.dictation_live_stop), style = MaterialTheme.typography.labelLarge) + } + TextButton(onClick = onCancel) { + Text( + text = stringResource(R.string.dictation_live_cancel), + style = MaterialTheme.typography.labelLarge, + color = colors.statusError, + ) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/OnDeviceStt.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/OnDeviceStt.kt new file mode 100644 index 000000000..058e9a2ae --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/OnDeviceStt.kt @@ -0,0 +1,67 @@ +package io.github.kahme247.ompweb.ui.composer.dictation + +/** Why on-device recognition ended without an error message from the server. */ +enum class OnDeviceSttError { + /** No recognizer installed (no Google app on this phone). */ + NoRecognizer, + + /** `RECORD_AUDIO` is missing. */ + Permission, + + /** The recognizer needs a network connection (a cloud recognizer without data). */ + Offline, + + /** The recognizer does not support the picked language (on-device models are per language). */ + LanguageUnavailable, + + /** Another app is using the microphone/recognizer. */ + Busy, + + /** Silence at the start: nothing was said. */ + NoSpeech, + + /** Anything else (client/server/audio failure). */ + Failed, +} + +/** + * The platform speech recognizer behind on-device dictation, as an interface so + * the view-model logic runs in JVM tests. [Listener] callbacks arrive on the + * main thread, in order; the implementation destroys its recognizer after the + * final transcript or an error, so a new [start] always builds a fresh one. + */ +interface OnDeviceStt { + + /** + * This phone has an installed on-device recognition model + * (`SpeechRecognizer.isOnDeviceRecognitionAvailable`, API 31+). A cloud + * recognizer does not count: "on this phone" must never upload audio. + */ + fun isOnDeviceAvailable(): Boolean + + /** + * Starts listening on the on-device recognizer in [languageTag] (BCP-47, + * e.g. `de-DE`). Reports [Listener.onPartial] while the user talks and + * exactly one of [Listener.onFinal] / [Listener.onError] afterwards; with + * [isOnDeviceAvailable] false it reports [OnDeviceSttError.NoRecognizer] + * instead of falling back to a network recognizer. + */ + fun start(languageTag: String, listener: Listener) + + /** End the utterance and wait for the final transcript. */ + fun stop() + + /** Abort without a transcript and destroy the recognizer. */ + fun cancel() + + /** Destroys the recognizer if one is still alive (idempotent). */ + fun release() + + interface Listener { + fun onPartial(text: String) + + fun onFinal(text: String) + + fun onError(error: OnDeviceSttError) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/RecordingDeck.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/RecordingDeck.kt new file mode 100644 index 000000000..6b4ad8271 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/composer/dictation/RecordingDeck.kt @@ -0,0 +1,544 @@ +package io.github.kahme247.ompweb.ui.composer.dictation + +import android.Manifest +import android.content.pm.PackageManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Warning +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.DictationEngine +import io.github.kahme247.ompweb.domain.DictationMachine +import io.github.kahme247.ompweb.domain.DictationPlayback +import io.github.kahme247.ompweb.domain.DictationState +import io.github.kahme247.ompweb.domain.SttAfter +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily +import java.util.Locale +import kotlin.math.max + +/** + * The composer's mic button (web `ChatInput` dictation toggle). It requests + * `RECORD_AUDIO` with a rationale before the first recording; screen + * visibility (adoption loop, ON_STOP for live dictation) is reported by the + * composer shell, which stays mounted in every dictation state. + */ +@Composable +fun MicButton( + vm: DictationViewModel, + enabled: Boolean, + modifier: Modifier = Modifier, +) { + val state by vm.state.collectAsStateWithLifecycle() + val context = LocalContext.current + val colors = LocalOmpColors.current + var asking by remember { mutableStateOf(false) } + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (granted) vm.start(SttAfter.Insert) else vm.onPermissionDenied() + } + val busy = state !is DictationState.Idle && state !is DictationState.Gone + val available = enabled && !busy + val label = stringResource(R.string.dictation_start) + + Box( + modifier = modifier + .size(28.dp) + .clip(RoundedCornerShape(7.dp)) + .clickable(enabled = available, role = Role.Button, onClickLabel = label) { + val granted = ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED + if (granted) vm.start(SttAfter.Insert) else asking = true + } + .semantics { contentDescription = label }, + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = MicIcon, + contentDescription = null, + tint = if (available) colors.textMuted else colors.textDim, + modifier = Modifier.size(16.dp), + ) + } + + if (asking) { + val usesPhone = vm.nextEngine() == DictationEngine.ON_DEVICE + // "On this phone" without an installed model fails on start, so the + // dialog shows that error instead of promising on-device processing + // and asking for a microphone it cannot use. + val missingModel = usesPhone && !vm.onDeviceReady + val rationale = when { + !usesPhone -> R.string.dictation_permission_rationale + missingModel -> R.string.dictation_error_no_recognizer + else -> R.string.dictation_permission_rationale_on_device + } + OmpAlertDialog( + onDismissRequest = { asking = false }, + title = { Text(stringResource(R.string.dictation_permission_title)) }, + text = { Text(stringResource(rationale)) }, + confirmButton = { + if (missingModel) { + TextButton(onClick = { asking = false }) { + Text(stringResource(R.string.common_close)) + } + } else { + TextButton( + onClick = { + asking = false + launcher.launch(Manifest.permission.RECORD_AUDIO) + }, + ) { Text(stringResource(R.string.dictation_permission_allow)) } + } + }, + dismissButton = if (missingModel) { + null + } else { + { + TextButton( + onClick = { + asking = false + vm.onPermissionDenied() + }, + ) { Text(stringResource(R.string.dictation_permission_later)) } + } + }, + ) + } +} + +/** + * The recording deck that replaces the text field while dictation is active + * (web `components/RecordingDeck.tsx`): timer + level waveform + pause/stop/ + * cancel while capturing, an indeterminate bar while uploading/transcribing + * (with playback), and message + play/retry/discard after a failure. + */ +@Composable +fun RecordingDeck(vm: DictationViewModel, modifier: Modifier = Modifier) { + val state by vm.state.collectAsStateWithLifecycle() + val playback by vm.playback.collectAsStateWithLifecycle() + // Delegated state cannot be smart-cast; the failed row needs both fields. + val playing = playback as? DictationPlayback.Playing + val levels = remember { mutableStateListOf() } + + LaunchedEffect(state) { + when (val current = state) { + is DictationState.Recording -> { + levels.add(current.level) + while (levels.size > MAX_WAVE_BARS) levels.removeAt(0) + } + DictationState.Idle, DictationState.Gone -> levels.clear() + else -> Unit + } + } + + when (val current = state) { + DictationState.Idle, DictationState.Gone -> Unit + is DictationState.Live -> Unit + is DictationState.Recording -> DeckShell(modifier) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Box( + Modifier + .size(8.dp) + .clip(CircleShape) + .background(if (current.paused) LocalOmpColors.current.textMuted else LocalOmpColors.current.statusError), + ) + Text( + text = formatTime(current.elapsedMs), + color = LocalOmpColors.current.text, + fontFamily = OmpMonoFontFamily, + fontSize = 12.sp, + ) + Waveform(levels, current.paused, Modifier.weight(1f).height(24.dp)) + DeckButton( + description = stringResource( + if (current.paused) R.string.dictation_resume else R.string.dictation_pause, + ), + tone = if (current.paused) DeckTone.Accent else DeckTone.Neutral, + onClick = { if (current.paused) vm.resume() else vm.pause() }, + ) { + if (current.paused) { + Icon(MicIcon, contentDescription = null, tint = LocalOmpColors.current.accent, modifier = Modifier.size(14.dp)) + } else { + PauseGlyph(LocalOmpColors.current.textMuted) + } + } + DeckButton( + description = stringResource(R.string.dictation_stop), + onClick = { vm.stop() }, + ) { + StopGlyph(LocalOmpColors.current.textMuted) + } + DeckButton( + description = stringResource(R.string.dictation_cancel), + tone = DeckTone.Danger, + onClick = { vm.cancel() }, + ) { + Icon(Icons.Filled.Close, contentDescription = null, tint = LocalOmpColors.current.statusError, modifier = Modifier.size(14.dp)) + } + } + CaptureProgress(current.elapsedMs) + } + + is DictationState.Uploading -> DeckShell(modifier) { + BusyRow( + label = stringResource(R.string.dictation_uploading), + vm = vm, + playback = playback, + trailing = formatTime(current.elapsedMs), + ) + } + + is DictationState.Transcribing -> DeckShell(modifier) { + BusyRow( + label = stringResource(R.string.dictation_transcribing), + vm = vm, + playback = playback, + trailing = formatTime(current.elapsedMs), + ) + } + + is DictationState.Failed -> DeckShell(modifier) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (vm.canPlay()) { + DeckButton( + description = stringResource( + if (playing?.paused == false) { + R.string.dictation_pause_playback + } else { + R.string.dictation_play + }, + ), + onClick = { vm.playPause() }, + ) { + PlayGlyph(playback, LocalOmpColors.current.textMuted) + } + } + Icon( + Icons.Filled.Warning, + contentDescription = null, + tint = LocalOmpColors.current.statusError, + modifier = Modifier.size(14.dp), + ) + Text( + text = current.message, + color = LocalOmpColors.current.text, + fontSize = 12.sp, + maxLines = 2, + modifier = Modifier.weight(1f), + ) + DeckButton( + description = stringResource(R.string.dictation_discard), + tone = DeckTone.Danger, + onClick = { vm.discard() }, + ) { + Icon(Icons.Filled.Delete, contentDescription = null, tint = LocalOmpColors.current.statusError, modifier = Modifier.size(14.dp)) + } + if (current.canRetry) { + DeckButton( + description = stringResource(R.string.dictation_retry), + tone = DeckTone.Accent, + onClick = { vm.retry() }, + ) { + Icon(Icons.Filled.Refresh, contentDescription = null, tint = LocalOmpColors.current.accent, modifier = Modifier.size(14.dp)) + } + } + } + } + } +} + +@Composable +private fun BusyRow( + label: String, + vm: DictationViewModel, + playback: DictationPlayback, + trailing: String, +) { + Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (vm.canPlay()) { + DeckButton( + description = stringResource( + if (playback is DictationPlayback.Playing && !playback.paused) { + R.string.dictation_pause_playback + } else { + R.string.dictation_play + }, + ), + onClick = { vm.playPause() }, + ) { + PlayGlyph(playback, LocalOmpColors.current.textMuted) + } + } + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + strokeWidth = 1.8.dp, + color = LocalOmpColors.current.accent, + ) + Text(text = label, color = LocalOmpColors.current.textMuted, fontSize = 12.sp, modifier = Modifier.weight(1f)) + Text( + text = if (playback is DictationPlayback.Playing) { + stringResource( + R.string.dictation_progress, + formatTime(playback.positionMs), + formatTime(playback.durationMs), + ) + } else { + trailing + }, + color = LocalOmpColors.current.text, + fontFamily = OmpMonoFontFamily, + fontSize = 12.sp, + ) + DeckButton( + description = stringResource(R.string.dictation_cancel), + tone = DeckTone.Danger, + onClick = { vm.cancel() }, + ) { + Icon(Icons.Filled.Close, contentDescription = null, tint = LocalOmpColors.current.statusError, modifier = Modifier.size(14.dp)) + } + } + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().height(2.dp), + color = LocalOmpColors.current.accent, + trackColor = LocalOmpColors.current.border, + ) + } +} + +@Composable +private fun CaptureProgress(elapsedMs: Long) { + val fraction = (elapsedMs.toFloat() / DictationMachine.MAX_RECORDING_MS).coerceIn(0f, 1f) + Column(Modifier.fillMaxWidth()) { + LinearProgressIndicator( + progress = { fraction }, + modifier = Modifier.fillMaxWidth().height(2.dp), + color = LocalOmpColors.current.statusError, + trackColor = LocalOmpColors.current.border, + ) + } +} + +@Composable +private fun DeckShell(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + content = content, + ) +} + +private enum class DeckTone { Neutral, Accent, Danger } + +@Composable +private fun DeckButton( + description: String, + onClick: () -> Unit, + tone: DeckTone = DeckTone.Neutral, + content: @Composable () -> Unit, +) { + val colors = LocalOmpColors.current + val shape = RoundedCornerShape(7.dp) + val borderColor = when (tone) { + DeckTone.Danger -> colors.statusError + DeckTone.Accent -> colors.accent + DeckTone.Neutral -> colors.border + } + val background = when (tone) { + DeckTone.Danger -> colors.statusError.copy(alpha = 0.15f) + DeckTone.Accent -> colors.accent.copy(alpha = 0.15f) + DeckTone.Neutral -> Color.Transparent + } + Box( + modifier = Modifier + .size(28.dp) + .clip(shape) + .background(background) + .border(1.dp, borderColor, shape) + .clickable(role = Role.Button, onClickLabel = description, onClick = onClick) + .semantics { contentDescription = description }, + contentAlignment = Alignment.Center, + ) { + content() + } +} + +@Composable +private fun Waveform(levels: List, paused: Boolean, modifier: Modifier) { + val color = if (paused) LocalOmpColors.current.textMuted else LocalOmpColors.current.statusError + Canvas(modifier) { + val gap = 2.dp.toPx() + val barWidth = 3.dp.toPx() + val count = max(1, ((size.width + gap) / (barWidth + gap)).toInt()).coerceAtMost(MAX_WAVE_BARS) + val visible = levels.takeLast(count) + val offset = size.width - visible.size * (barWidth + gap) + gap + visible.forEachIndexed { index, level -> + val barHeight = max(2.dp.toPx(), level * size.height) + drawRoundRect( + color = color, + topLeft = Offset(offset + index * (barWidth + gap), (size.height - barHeight) / 2f), + size = Size(barWidth, barHeight), + cornerRadius = CornerRadius(barWidth / 2f), + ) + } + } +} + +@Composable +private fun PlayGlyph(playback: DictationPlayback, tint: Color) { + val playing = playback is DictationPlayback.Playing && !playback.paused + if (playing) { + PauseGlyph(tint) + } else { + Icon(Icons.Filled.PlayArrow, contentDescription = null, tint = tint, modifier = Modifier.size(15.dp)) + } +} + +@Composable +private fun PauseGlyph(tint: Color) { + Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Box(Modifier.width(2.dp).height(9.dp).background(tint)) + Box(Modifier.width(2.dp).height(9.dp).background(tint)) + } +} + +@Composable +private fun StopGlyph(tint: Color) { + Box(Modifier.size(8.dp).clip(RoundedCornerShape(1.5.dp)).background(tint)) +} + +/** + * Screen visibility for the adoption loop and for ending a live capture when + * the composer leaves the foreground ([DictationViewModel.setVisible]). + * Composed once by the composer shell, which stays mounted for every dictation + * state — including `Live`, where neither the mic button nor the deck exists. + */ +@Composable +internal fun ReportVisibility(vm: DictationViewModel) { + val owner = LocalLifecycleOwner.current + DisposableEffect(owner, vm) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_START -> vm.setVisible(true) + Lifecycle.Event.ON_STOP -> { + // A backgrounded recorder keeps capturing without a UI; + // finish the capture so it uploads instead of leaking. + if (vm.state.value is DictationState.Recording) vm.stop() + vm.setVisible(false) + } + else -> Unit + } + } + owner.lifecycle.addObserver(observer) + vm.setVisible(owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) + onDispose { owner.lifecycle.removeObserver(observer) } + } +} + +internal fun formatTime(ms: Long): String { + val total = (ms.coerceAtLeast(0) / 1000).toInt() + val minutes = total / 60 + val seconds = total % 60 + return String.format(Locale.ROOT, "%d:%02d", minutes, seconds) +} + +private const val MAX_WAVE_BARS = 64 + +/** Material's microphone glyph (the core icon set ships no `Mic`). */ +private val MicIcon: ImageVector = ImageVector.Builder( + name = "Mic", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, +).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(12f, 14f) + curveTo(13.66f, 14f, 15f, 12.66f, 15f, 11f) + verticalLineTo(5f) + curveTo(15f, 3.34f, 13.66f, 2f, 12f, 2f) + curveTo(10.34f, 2f, 9f, 3.34f, 9f, 5f) + verticalLineTo(11f) + curveTo(9f, 12.66f, 10.34f, 14f, 12f, 14f) + close() + moveTo(17f, 11f) + curveTo(17f, 13.76f, 14.76f, 16f, 12f, 16f) + curveTo(9.24f, 16f, 7f, 13.76f, 7f, 11f) + lineTo(5f, 11f) + curveTo(5f, 14.53f, 7.61f, 17.43f, 11f, 17.92f) + verticalLineTo(21f) + lineTo(13f, 21f) + verticalLineTo(17.92f) + curveTo(16.39f, 17.43f, 19f, 14.53f, 19f, 11f) + close() + } +}.build() diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/connect/ConnectScreen.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/connect/ConnectScreen.kt new file mode 100644 index 000000000..4442e52e1 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/connect/ConnectScreen.kt @@ -0,0 +1,277 @@ +package io.github.kahme247.ompweb.ui.connect + +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.net.ProbeResult +import io.github.kahme247.ompweb.net.ServerAddress +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors + +@Composable +fun ConnectScreen( + viewModel: ConnectViewModel, + onAccepted: (ServerAddress, Boolean) -> Unit, + onTooOld: (ServerAddress, String?) -> Unit, + initialAddress: ServerAddress? = null, + initialResult: ProbeResult? = null, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val focusManager = LocalFocusManager.current + val resultRequester = remember { BringIntoViewRequester() } + + LaunchedEffect(viewModel, initialAddress, initialResult) { + viewModel.prefill(initialAddress, initialResult) + } + + LaunchedEffect(viewModel) { + viewModel.events.collect { event -> + when (event) { + is ConnectEvent.Accepted -> onAccepted(event.address, event.needsPassword) + is ConnectEvent.TooOld -> onTooOld(event.address, event.serverVersion) + } + } + } + + LaunchedEffect(state.result) { + if (state.result != null) { + withFrameNanos { } + resultRequester.bringIntoView() + } + } + + Scaffold { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .imePadding(), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column(modifier = Modifier.widthIn(max = 480.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(8.dp), + ) + Spacer(Modifier.width(12.dp)) + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + ) + } + + Spacer(Modifier.height(32.dp)) + Text( + text = stringResource(R.string.connect_heading), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.semantics { heading() }, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.connect_intro), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(Modifier.height(24.dp)) + OutlinedTextField( + value = state.input, + onValueChange = viewModel::onAddressChange, + modifier = Modifier.fillMaxWidth(), + label = { Text(stringResource(R.string.connect_address_label)) }, + placeholder = { Text(stringResource(R.string.connect_address_placeholder)) }, + singleLine = true, + isError = state.error != null, + supportingText = state.error?.let { message -> + { Text(message) } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Go, + ), + keyboardActions = KeyboardActions( + onGo = { + viewModel.connect() + focusManager.clearFocus() + }, + ), + ) + state.address?.let { address -> + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource(R.string.connect_resolved, address.origin), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Spacer(Modifier.height(16.dp)) + Button( + onClick = { + viewModel.connect() + focusManager.clearFocus() + }, + modifier = Modifier.fillMaxWidth(), + enabled = state.address != null && !state.checking, + ) { + Text(stringResource(R.string.connect_button)) + } + + if (state.checking) { + Spacer(Modifier.height(20.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth(), + ) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + ) + Spacer(Modifier.width(12.dp)) + Text( + text = stringResource(R.string.connect_checking), + style = MaterialTheme.typography.bodyMedium, + ) + } + } + + state.result?.let { result -> + Spacer(Modifier.height(24.dp)) + ProbeResultCard( + result = result, + modifier = Modifier.bringIntoViewRequester(resultRequester), + ) + } + + Spacer(Modifier.height(32.dp)) + Text( + text = stringResource(R.string.connect_help), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Start, + ) + } + } + } + } +} + +private data class ProbeCopy( + @param:StringRes val title: Int, + @param:StringRes val message: Int, +) + +@Composable +private fun ProbeResultCard(result: ProbeResult, modifier: Modifier = Modifier) { + val colors = LocalOmpColors.current + val (copy, color) = when (result) { + ProbeResult.MISCONFIGURED -> + ProbeCopy(R.string.probe_misconfigured_title, R.string.probe_misconfigured_message) to colors.statusError + ProbeResult.FORBIDDEN -> + ProbeCopy(R.string.probe_forbidden_title, R.string.probe_forbidden_message) to colors.statusWarning + ProbeResult.NOT_OMPWEB -> + ProbeCopy(R.string.probe_not_ompweb_title, R.string.probe_not_ompweb_message) to colors.statusError + ProbeResult.UNREACHABLE -> + ProbeCopy(R.string.probe_unreachable_title, R.string.probe_unreachable_message) to colors.statusError + ProbeResult.TLS_ERROR -> + ProbeCopy(R.string.probe_tls_title, R.string.probe_tls_message) to colors.statusError + // A reachable or password-protected server moves on to the next + // onboarding step; it never becomes a card. + ProbeResult.REACHABLE, ProbeResult.PASSWORD_REQUIRED -> return + } + + Surface( + modifier = modifier + .fillMaxWidth() + .semantics { liveRegion = LiveRegionMode.Polite }, + shape = RoundedCornerShape(16.dp), + color = color.copy(alpha = 0.12f), + contentColor = MaterialTheme.colorScheme.onSurface, + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(10.dp) + .background(color, CircleShape), + ) + Spacer(Modifier.width(10.dp)) + Text( + text = stringResource(copy.title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = color, + ) + } + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(copy.message), + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/connect/ConnectViewModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/connect/ConnectViewModel.kt new file mode 100644 index 000000000..b334c1f67 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/connect/ConnectViewModel.kt @@ -0,0 +1,142 @@ +package io.github.kahme247.ompweb.ui.connect + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.kahme247.ompweb.net.AppInfoProbe +import io.github.kahme247.ompweb.net.ProbeResult +import io.github.kahme247.ompweb.net.ServerAddress +import io.github.kahme247.ompweb.net.ServerAddressResult +import io.github.kahme247.ompweb.net.ServerProbe +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +data class ConnectUiState( + val input: String = "", + val address: ServerAddress? = null, + val error: String? = null, + val checking: Boolean = false, + val result: ProbeResult? = null, +) + +/** Where a successful connect attempt leads. */ +sealed interface ConnectEvent { + data class Accepted(val address: ServerAddress, val needsPassword: Boolean) : ConnectEvent + data class TooOld(val address: ServerAddress, val serverVersion: String?) : ConnectEvent +} + +/** + * Probes the typed address in two steps: `/api/sessions` first (is this + * ompweb at all?) and then `/api/app-info` (is it new enough for this app?). + * Every failure that still leaves the user with a usable screen stays on the + * connect screen as a result card. + */ +class ConnectViewModel( + private val probe: ServerProbe, +) : ViewModel() { + + private val _state = MutableStateFlow(ConnectUiState()) + val state: StateFlow = _state.asStateFlow() + + private val _events = MutableSharedFlow(extraBufferCapacity = 1) + val events: SharedFlow = _events.asSharedFlow() + + private var probeJob: Job? = null + + fun onAddressChange(value: String) { + probeJob?.cancel() + val parsed = ServerAddress.parse(value) + _state.update { + it.copy( + input = value, + address = (parsed as? ServerAddressResult.Valid)?.address, + error = (parsed as? ServerAddressResult.Invalid)?.message?.takeIf { value.isNotBlank() }, + checking = false, + result = null, + ) + } + } + + /** + * Shows the address and the result of a failed "Check again" from the + * too-old screen, so the user does not have to retype anything. + */ + fun prefill(address: ServerAddress?, result: ProbeResult?) { + if (address == null && result == null) return + _state.update { current -> + if (current.checking) { + current + } else { + current.copy( + input = address?.origin ?: current.input, + address = address ?: current.address, + error = null, + result = result, + ) + } + } + } + + fun connect() { + val address = _state.value.address ?: return + if (_state.value.checking) return + _state.update { it.copy(checking = true, result = null) } + probeJob = viewModelScope.launch { + val sessions = probe.probe(address) + when (sessions) { + ProbeResult.REACHABLE, ProbeResult.PASSWORD_REQUIRED -> { + when (val info = probe.probeAppInfo(address)) { + is AppInfoProbe.Supported -> { + settle() + _events.emit(ConnectEvent.Accepted(address, sessions == ProbeResult.PASSWORD_REQUIRED)) + } + // The auth gate ran before routing: on a password-protected + // server this says nothing about the version yet. The check + // repeats after sign-in, when the cookie exists. + AppInfoProbe.PasswordRequired -> { + settle() + _events.emit(ConnectEvent.Accepted(address, needsPassword = true)) + } + is AppInfoProbe.TooOld -> { + settle() + _events.emit(ConnectEvent.TooOld(address, info.serverVersion)) + } + else -> finish(address, info.toProbeResult()) + } + } + else -> finish(address, sessions) + } + } + } + + private fun settle() { + _state.update { it.copy(checking = false) } + } + + private fun finish(address: ServerAddress, result: ProbeResult) { + _state.update { current -> + if (current.address == address) { + current.copy(checking = false, result = result) + } else { + current.copy(checking = false) + } + } + } + + private fun AppInfoProbe.toProbeResult(): ProbeResult = when (this) { + is AppInfoProbe.Supported -> ProbeResult.REACHABLE + is AppInfoProbe.TooOld -> ProbeResult.NOT_OMPWEB + AppInfoProbe.PasswordRequired -> ProbeResult.PASSWORD_REQUIRED + AppInfoProbe.Forbidden -> ProbeResult.FORBIDDEN + AppInfoProbe.Misconfigured -> ProbeResult.MISCONFIGURED + AppInfoProbe.Unreachable -> ProbeResult.UNREACHABLE + AppInfoProbe.TlsError -> ProbeResult.TLS_ERROR + AppInfoProbe.NotOmpweb -> ProbeResult.NOT_OMPWEB + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileExplorerPane.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileExplorerPane.kt new file mode 100644 index 000000000..89f6a7065 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileExplorerPane.kt @@ -0,0 +1,870 @@ +package io.github.kahme247.ompweb.ui.files + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Warning +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.FileSearchRow +import io.github.kahme247.ompweb.domain.FileTreeRow +import io.github.kahme247.ompweb.domain.GitFileStatusKind +import io.github.kahme247.ompweb.domain.changedDirectoryPaths +import io.github.kahme247.ompweb.domain.comparableFilePath +import io.github.kahme247.ompweb.domain.flattenFileTree +import io.github.kahme247.ompweb.domain.formatFileSize +import io.github.kahme247.ompweb.domain.relativeFilePath +import io.github.kahme247.ompweb.domain.atMentionText +import io.github.kahme247.ompweb.net.MAX_FILE_SEARCH_LIMIT +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpDropdownMenu +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily +import kotlinx.coroutines.launch + +/** Test tag prefix of one tree row; the key tests focus the row by path. */ +internal const val FILE_ROW_TAG_PREFIX = "file-row:" + +/** + * The Explorer tab (web `components/FileExplorer.tsx`, lazy `?type=list` + * tree): refresh, collapse all, file search over `/api/file-index`, the + * upload flow with its conflict question, git decorations, and per-row + * actions (open, mention, download, share, copy path). + * + * The pane owns no Android plumbing beyond the SAF picker: saving, sharing and + * the clipboard stay in `FilePanel`, which also knows the active composer. + */ +@Composable +fun FileExplorerPane( + state: ExplorerUiState, + vm: FilesViewModel, + onOpenFile: (path: String) -> Unit, + onMention: (String) -> Unit, + onCopyPath: (String) -> Unit, + onDownload: (String) -> Unit, + onShare: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val cwd = state.cwd + val context = LocalContext.current + val scope = rememberCoroutineScope() + // Resolved here: a Context read inside the picker callback is not + // configuration-aware (lint `LocalContextGetResourceValueCall`). + val uploadFailedLabel = stringResource(R.string.files_upload_failed) + val uploadLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.OpenMultipleDocuments(), + ) { uris -> + if (uris.isEmpty()) return@rememberLauncherForActivityResult + scope.launch { + val pick = FileTransfer.readUploads(context, uris) + val pickError = pick.rejected.takeIf { it.isNotEmpty() } + ?.let { names -> uploadFailedLabel + ": " + names.joinToString(", ") } + // Both outcomes travel together: the banner shows the rejected + // URIs next to the progress of the files that did read. + if (pick.files.isNotEmpty()) { + vm.upload(pick.files, pickError) + } else if (pickError != null) { + vm.reportUploadError(pickError) + } + } + } + + Column(modifier.fillMaxSize()) { + ExplorerToolbar( + searchOpen = state.searchOpen, + onSearchToggle = { vm.setSearchOpen(!state.searchOpen) }, + onRefresh = vm::refreshExplorer, + onCollapseAll = vm::collapseAll, + onUpload = { uploadLauncher.launch(arrayOf("*/*")) }, + uploadEnabled = cwd != null && !state.upload.busy, + ) + if (state.searchOpen) { + ExplorerSearchBar( + query = state.searchQuery, + onQueryChange = vm::setSearchQuery, + onClose = { vm.setSearchOpen(false) }, + ) + } + UploadFeedback(state = state, vm = vm, onMention = onMention) + + val loadError = state.error + when { + cwd == null -> ExplorerPlaceholder( + title = stringResource(R.string.files_no_workspace), + hint = stringResource(R.string.files_no_workspace_hint), + ) + + loadError != null && !state.rootLoaded -> ExplorerPlaceholder( + title = stringResource(R.string.files_load_failed, loadError), + hint = null, + action = stringResource(R.string.files_retry) to vm::refreshExplorer, + ) + + state.searchOpen -> ExplorerSearchResults( + state = state, + onOpen = { row -> onOpenFile(row.path) }, + onRetry = vm::retrySearch, + ) + + else -> ExplorerTree( + state = state, + vm = vm, + onOpenFile = onOpenFile, + onMention = onMention, + onCopyPath = onCopyPath, + onDownload = onDownload, + onShare = onShare, + ) + } + } + + val conflict = state.upload.conflict + val phase = state.upload.phase + if (phase == UploadPhase.Conflict && conflict != null) { + UploadConflictDialog( + conflicts = conflict.conflicts, + nonReplaceable = conflict.nonReplaceable, + canReplace = conflict.canReplace, + onReplace = { vm.resolveUploadConflict(io.github.kahme247.ompweb.domain.UploadConflictStrategy.Overwrite) }, + onSkip = { vm.resolveUploadConflict(io.github.kahme247.ompweb.domain.UploadConflictStrategy.Skip) }, + onCancel = vm::cancelUploadConflict, + ) + } +} + +@Composable +private fun ExplorerToolbar( + searchOpen: Boolean, + onSearchToggle: () -> Unit, + onRefresh: () -> Unit, + onCollapseAll: () -> Unit, + onUpload: () -> Unit, + uploadEnabled: Boolean, +) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp, end = 4.dp, top = 4.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + IconButton(onClick = onSearchToggle) { + Icon( + imageVector = if (searchOpen) Icons.Filled.Close else Icons.Filled.Search, + contentDescription = stringResource(R.string.files_search_toggle), + tint = if (searchOpen) colors.accent else colors.textDim, + modifier = Modifier.size(16.dp), + ) + } + IconButton(onClick = onRefresh) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(R.string.files_refresh), + tint = colors.textDim, + modifier = Modifier.size(16.dp), + ) + } + TextButton(onClick = onCollapseAll, enabled = uploadEnabled) { + Text( + text = stringResource(R.string.files_collapse_all), + color = if (uploadEnabled) colors.textDim else colors.textDim.copy(alpha = 0.4f), + fontSize = 11.sp, + ) + } + Spacer(Modifier.weight(1f)) + IconButton(onClick = onUpload, enabled = uploadEnabled) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(R.string.files_upload), + tint = if (uploadEnabled) colors.accent else colors.textDim, + modifier = Modifier.size(18.dp), + ) + } + } +} + +@Composable +private fun ExplorerSearchBar(query: String, onQueryChange: (String) -> Unit, onClose: () -> Unit) { + val colors = LocalOmpColors.current + val focusManager = LocalFocusManager.current + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Box( + modifier = Modifier + .weight(1f) + .heightIn(min = 30.dp) + .clip(RoundedCornerShape(6.dp)) + .background(colors.background) + .border(1.dp, colors.border, RoundedCornerShape(6.dp)) + .padding(horizontal = 8.dp), + contentAlignment = Alignment.CenterStart, + ) { + if (query.isEmpty()) { + Text( + text = stringResource(R.string.files_search_placeholder), + color = colors.textDim, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + BasicTextField( + value = query, + onValueChange = onQueryChange, + singleLine = true, + textStyle = LocalTextStyle.current.copy(color = colors.text, fontSize = 12.sp), + cursorBrush = SolidColor(colors.accent), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }), + modifier = Modifier.fillMaxWidth(), + ) + } + TextButton(onClick = onClose) { + Text(stringResource(R.string.files_dismiss), color = colors.textDim, fontSize = 11.sp) + } + } +} + +@Composable +private fun UploadFeedback(state: ExplorerUiState, vm: FilesViewModel, onMention: (String) -> Unit) { + val colors = LocalOmpColors.current + val upload = state.upload + val background = colors.backgroundSubtle + val error = upload.error + val summary = upload.summary + if (!upload.busy && error == null && summary == null) return + Column(Modifier.fillMaxWidth()) { + // A picker rejection and the upload progress are not alternatives: + // both belong on screen at the same time. + if (error != null) { + FeedbackBanner( + text = error, + error = true, + action = if (upload.busy) { + null + } else { + stringResource(R.string.files_dismiss) to vm::dismissUploadFeedback + }, + ) + } + if (upload.busy) { + Column( + Modifier + .fillMaxWidth() + .background(background) + .padding(horizontal = 10.dp, vertical = 6.dp), + ) { + Text( + text = if (upload.phase == UploadPhase.Checking) { + stringResource(R.string.files_upload_checking) + } else { + upload.currentName?.let { name -> + stringResource(R.string.files_upload_progress, name, upload.percent) + } ?: stringResource(R.string.files_upload_checking) + }, + color = colors.textMuted, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (upload.phase == UploadPhase.Uploading) { + Spacer(Modifier.height(4.dp)) + LinearProgressIndicator( + progress = { upload.percent / 100f }, + modifier = Modifier.fillMaxWidth().height(3.dp), + color = colors.accent, + trackColor = colors.border, + ) + if (upload.total > 1) { + Text( + text = stringResource(R.string.files_upload_index, upload.index + 1, upload.total), + color = colors.textDim, + fontSize = 10.sp, + ) + } + } + } + } + if (!upload.busy && summary != null) { + val parts = buildList { + if (summary.uploaded.isNotEmpty()) add( + stringResource(R.string.files_upload_uploaded, summary.uploaded.size), + ) + if (summary.skipped.isNotEmpty()) add( + stringResource(R.string.files_upload_skipped, summary.skipped.size), + ) + if (summary.failed.isNotEmpty()) add( + stringResource(R.string.files_upload_failed_count, summary.failed.size), + ) + } + FeedbackBanner( + text = if (parts.isEmpty()) { + stringResource(R.string.files_upload_done) + } else { + stringResource(R.string.files_upload_done) + " · " + parts.joinToString(" · ") + }, + error = summary.failed.isNotEmpty(), + action = if (summary.uploaded.isNotEmpty()) { + stringResource(R.string.files_upload_mention) to { + val text = vm.uploadedMentionText() + if (text != null) onMention(text) + vm.dismissUploadFeedback() + } + } else { + null + }, + ) + // The web lists every failed file by name with the reason on its + // title; a bare count hides which file the user has to retry. + summary.failed.forEach { failure -> + Row( + Modifier + .fillMaxWidth() + .background(background) + .padding(horizontal = 10.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + tint = colors.statusError, + modifier = Modifier.size(11.dp), + ) + Spacer(Modifier.width(4.dp)) + Text( + text = "${failure.name}: ${failure.message}", + color = colors.statusError, + fontSize = 10.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} + +@Composable +private fun FeedbackBanner( + text: String, + error: Boolean, + action: Pair Unit>?, +) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .background(if (error) colors.statusError.copy(alpha = 0.12f) else colors.backgroundSubtle) + .padding(start = 10.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (error) { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + tint = colors.statusError, + modifier = Modifier.size(13.dp), + ) + Spacer(Modifier.width(6.dp)) + } + Text( + text = text, + modifier = Modifier.weight(1f), + color = if (error) colors.statusError else colors.textMuted, + fontSize = 11.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + if (action != null) { + TextButton(onClick = action.second) { + Text(action.first, color = colors.accent, fontSize = 11.sp) + } + } + } +} + +@Composable +private fun ExplorerTree( + state: ExplorerUiState, + vm: FilesViewModel, + onOpenFile: (String) -> Unit, + onMention: (String) -> Unit, + onCopyPath: (String) -> Unit, + onDownload: (String) -> Unit, + onShare: (String) -> Unit, +) { + val cwd = state.cwd ?: return + val colors = LocalOmpColors.current + val rows = remember(state.children, state.expanded, state.loading, cwd) { + flattenFileTree(cwd, state.children, state.expanded, state.loading) + } + val directoryDots = remember(state.gitFiles, cwd) { + changedDirectoryPaths(cwd, state.gitFiles.map { it.filePath }) + } + val fileStatus = remember(state.gitFiles) { + state.gitFiles.associate { comparableFilePath(it.filePath) to it.status } + } + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + // The keyboard cursor is the row the last arrow key (or the focused row) + // selected. Keeping its path instead of its index survives an expand or a + // refresh that inserts rows. + var focusedPath by remember { mutableStateOf(null) } + val focusedIndex = focusedPath + ?.let { path -> rows.indexOfFirst { !it.empty && it.path == path } } + ?.takeIf { it >= 0 } + val onKey: (KeyEvent) -> Boolean = { event -> + if (event.type != KeyEventType.KeyDown) { + false + } else { + val key = when (event.key) { + Key.DirectionUp -> FileTreeKey.Up + Key.DirectionDown -> FileTreeKey.Down + Key.MoveHome -> FileTreeKey.Home + Key.MoveEnd -> FileTreeKey.End + Key.DirectionLeft -> FileTreeKey.Left + Key.DirectionRight -> FileTreeKey.Right + Key.Enter, Key.NumPadEnter, Key.Spacebar -> FileTreeKey.Activate + else -> null + } + when (val action = key?.let { FileKeyNav.treeAction(rows, focusedIndex, it) }) { + is FileTreeAction.Focus -> { + rows.getOrNull(action.index)?.let { focusedPath = it.path } + scope.launch { listState.scrollToItem(action.index) } + true + } + + FileTreeAction.ToggleDirectory -> { + rows.getOrNull(focusedIndex ?: -1) + ?.takeIf { it.isDir } + ?.let { vm.toggleDirectory(it.path) } + focusedIndex != null + } + + FileTreeAction.OpenFile -> { + rows.getOrNull(focusedIndex ?: -1) + ?.takeIf { !it.isDir } + ?.let { onOpenFile(it.path) } + focusedIndex != null + } + + else -> false + } + } + } + val revealTarget = state.revealTarget + LaunchedEffect(revealTarget, rows) { + if (revealTarget == null) return@LaunchedEffect + val index = rows.indexOfFirst { comparableFilePath(it.path) == revealTarget } + // Not there yet (ancestors still loading): keep the target, the next + // tree emission retries, and the view model's timeout is the backstop. + if (index >= 0) { + listState.scrollToItem(index) + vm.acknowledgeReveal() + } + } + + if (rows.isEmpty() && state.rootLoaded && !state.searchOpen) { + ExplorerPlaceholder( + title = stringResource(R.string.files_empty_directory), + hint = null, + ) + return + } + + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .onPreviewKeyEvent(onKey), + ) { + items(rows, key = { row -> (if (row.empty) "empty:" else "") + row.path }) { row -> + if (row.empty) { + Text( + text = stringResource(R.string.files_empty_directory), + modifier = Modifier.padding(start = (12 + row.depth * 14).dp, top = 4.dp, bottom = 4.dp), + color = colors.textDim, + fontSize = 10.5.sp, + ) + } else { + ExplorerRow( + row = row, + selected = comparableFilePath(row.path) in state.highlight, + focused = row.path == focusedPath, + gitKind = fileStatus[comparableFilePath(row.path)], + hasChangedChildren = row.isDir && + comparableFilePath(row.path) in directoryDots, + onFocus = { focusedPath = row.path }, + onOpen = { + if (row.isDir) vm.toggleDirectory(row.path) else onOpenFile(row.path) + }, + onMention = { + onMention(atMentionText(relativeFilePath(row.path, cwd), row.isDir)) + }, + onCopyPath = { onCopyPath(relativeFilePath(row.path, cwd)) }, + onDownload = if (row.isDir) null else ({ onDownload(row.path) }), + onShare = if (row.isDir) null else ({ onShare(row.path) }), + ) + } + } + } +} + +@Composable +private fun ExplorerRow( + row: FileTreeRow, + selected: Boolean, + focused: Boolean, + gitKind: GitFileStatusKind?, + hasChangedChildren: Boolean, + onFocus: () -> Unit, + onOpen: () -> Unit, + onMention: () -> Unit, + onCopyPath: () -> Unit, + onDownload: (() -> Unit)?, + onShare: (() -> Unit)?, +) { + val colors = LocalOmpColors.current + var menuOpen by remember(row.path) { mutableStateOf(false) } + Row( + modifier = Modifier + .fillMaxWidth() + .background( + when { + selected -> colors.selected + focused -> colors.backgroundSubtle + else -> colors.background + }, + ) + .onFocusChanged { focusState -> if (focusState.isFocused) onFocus() } + .clickable( + role = Role.Button, + onClickLabel = stringResource( + if (row.isDir) R.string.files_open_directory else R.string.files_open_file, + ), + onClick = onOpen, + ) + .testTag(FILE_ROW_TAG_PREFIX + row.path) + .padding(start = (6 + row.depth * 14).dp, end = 2.dp, top = 4.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (row.isDir) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier + .size(14.dp) + .rotate(if (row.expanded) 0f else -90f), + ) + } else { + Spacer(Modifier.width(14.dp)) + } + Text( + text = row.name, + modifier = Modifier.weight(1f, fill = false), + color = colors.text, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (row.isDir) { + if (hasChangedChildren) { + Box( + Modifier + .size(5.dp) + .clip(RoundedCornerShape(50)) + .background(colors.statusModified), + ) + } + if (row.loading) { + Text(text = "…", color = colors.textDim, fontSize = 11.sp) + } + } else { + if (gitKind != null) { + Text( + text = gitKind.code, + color = if (gitKind == GitFileStatusKind.ADDED) colors.statusSuccess else colors.statusModified, + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = OmpMonoFontFamily, + ) + } + if (row.size > 0) { + Text( + text = formatFileSize(row.size), + color = colors.textDim, + fontSize = 10.sp, + ) + } + } + Box { + IconButton(onClick = { menuOpen = true }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = stringResource(R.string.files_row_actions), + tint = colors.textDim, + modifier = Modifier.size(14.dp), + ) + } + OmpDropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { + if (!row.isDir) { + DropdownMenuItem( + text = { Text(stringResource(R.string.files_open_file), fontSize = 12.sp) }, + onClick = { + menuOpen = false + onOpen() + }, + ) + } + DropdownMenuItem( + text = { Text(stringResource(R.string.files_mention), fontSize = 12.sp) }, + onClick = { + menuOpen = false + onMention() + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.files_copy_path), fontSize = 12.sp) }, + onClick = { + menuOpen = false + onCopyPath() + }, + ) + if (onDownload != null) { + DropdownMenuItem( + text = { Text(stringResource(R.string.files_download), fontSize = 12.sp) }, + onClick = { + menuOpen = false + onDownload() + }, + ) + } + if (onShare != null) { + DropdownMenuItem( + text = { Text(stringResource(R.string.files_share), fontSize = 12.sp) }, + onClick = { + menuOpen = false + onShare() + }, + ) + } + } + } + } +} + +@Composable +private fun ExplorerSearchResults( + state: ExplorerUiState, + onOpen: (FileSearchRow) -> Unit, + onRetry: () -> Unit, +) { + val colors = LocalOmpColors.current + when { + state.searchQuery.isBlank() -> ExplorerPlaceholder( + title = stringResource(R.string.files_search_placeholder), + hint = null, + ) + + state.searchFailed -> ExplorerPlaceholder( + title = stringResource(R.string.files_search_failed, state.searchQuery), + hint = null, + action = stringResource(R.string.files_retry) to onRetry, + ) + + state.searchResults.isEmpty() && !state.searchLoading -> ExplorerPlaceholder( + title = stringResource(R.string.files_search_empty), + hint = null, + ) + + else -> LazyColumn(Modifier.fillMaxSize()) { + items(state.searchResults, key = { it.path }) { row -> + Column( + Modifier + .fillMaxWidth() + .clickable( + role = Role.Button, + onClickLabel = stringResource(R.string.files_open_file), + ) { onOpen(row) } + .padding(horizontal = 10.dp, vertical = 6.dp), + ) { + Text( + text = row.name, + color = colors.text, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (row.directory.isNotEmpty()) { + Text( + text = row.directory, + color = colors.textDim, + fontSize = 10.5.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + if (state.searchTruncated) { + item(key = "truncated") { + Text( + text = stringResource(R.string.files_search_truncated, MAX_FILE_SEARCH_LIMIT), + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + color = colors.textDim, + fontSize = 10.5.sp, + ) + } + } + } + } +} + +@Composable +private fun ExplorerPlaceholder( + title: String, + hint: String?, + action: Pair Unit>? = null, +) { + val colors = LocalOmpColors.current + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = title, color = colors.text, fontSize = 12.5.sp) + if (hint != null) { + Spacer(Modifier.height(6.dp)) + Text(text = hint, color = colors.textDim, fontSize = 11.sp) + } + if (action != null) { + Spacer(Modifier.height(8.dp)) + TextButton(onClick = action.second) { + Text(action.first, color = colors.accent, fontSize = 12.sp) + } + } + } +} + +@Composable +private fun UploadConflictDialog( + conflicts: List, + nonReplaceable: List, + canReplace: Boolean, + onReplace: () -> Unit, + onSkip: () -> Unit, + onCancel: () -> Unit, +) { + val colors = LocalOmpColors.current + OmpAlertDialog( + onDismissRequest = onCancel, + title = { Text(stringResource(R.string.files_upload_conflict_title)) }, + text = { + Column { + Text( + text = stringResource( + R.string.files_upload_conflict_body, + conflicts.take(5).joinToString(", ") + + if (conflicts.size > 5) " +${conflicts.size - 5}" else "", + ), + fontSize = 13.sp, + ) + if (nonReplaceable.isNotEmpty()) { + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource( + R.string.files_upload_conflict_non_replaceable, + nonReplaceable.joinToString(", "), + ), + color = colors.textDim, + fontSize = 12.sp, + ) + } + } + }, + confirmButton = { + TextButton(onClick = onReplace, enabled = canReplace) { + Text(stringResource(R.string.files_upload_replace)) + } + }, + dismissButton = { + Row { + TextButton(onClick = onSkip) { + Text(stringResource(R.string.files_upload_skip)) + } + TextButton(onClick = onCancel) { + Text(stringResource(R.string.files_upload_cancel)) + } + } + }, + ) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileKeyNav.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileKeyNav.kt new file mode 100644 index 000000000..c13d27337 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileKeyNav.kt @@ -0,0 +1,133 @@ +package io.github.kahme247.ompweb.ui.files + +import io.github.kahme247.ompweb.domain.FileTreeRow + +/** The hardware keys the explorer tree and the files tab strip handle. */ +enum class FileTreeKey { Up, Down, Home, End, Left, Right, Activate } + +enum class FileTabKey { Left, Right, Home, End } + +/** What one tree key press does; no state is touched here. */ +sealed interface FileTreeAction { + /** Move the keyboard cursor to this row index. */ + data class Focus(val index: Int) : FileTreeAction + + /** Open or close the focused directory (web `handleActivateRow`). */ + data object ToggleDirectory : FileTreeAction + + /** Open the focused file. */ + data object OpenFile : FileTreeAction +} + +/** + * Keyboard navigation of the files tree and the tab strip, ported from the + * web (`components/FileExplorer.tsx` `handleRowKeyDown`, + * `components/TabBar.tsx` `handleTabKeyDown`). + * + * The tree is the flattened row list the pane renders; `empty` placeholder + * rows are not focusable, so Up/Down/Home/End skip them exactly like the web + * skips non-node rows. Compose keeps the focus on one row and this object only + * decides where the keyboard cursor goes, which makes every rule JVM-testable. + */ +object FileKeyNav { + + /** First focusable row (web `rows.findIndex(kind === "node")`). */ + fun firstNodeIndex(rows: List): Int? = + rows.indexOfFirst { !it.empty }.takeIf { it >= 0 } + + fun lastNodeIndex(rows: List): Int? = + rows.indexOfLast { !it.empty }.takeIf { it >= 0 } + + /** The previous focusable row before [from], or null at the top. */ + fun previousNodeIndex(rows: List, from: Int): Int? { + for (index in from - 1 downTo 0) { + if (index in rows.indices && !rows[index].empty) return index + } + return null + } + + /** The next focusable row after [from], or null at the bottom. */ + fun nextNodeIndex(rows: List, from: Int): Int? { + for (index in from + 1 until rows.size) { + if (!rows[index].empty) return index + } + return null + } + + /** + * The parent directory of [from]: the closest preceding row one level up. + * The flat order puts every parent before its children, so a collapsed + * folder's own parent precedes it too (web `ArrowLeft` branch). + */ + fun parentNodeIndex(rows: List, from: Int): Int? { + val depth = rows.getOrNull(from)?.depth ?: return null + if (depth <= 0) return null + for (index in from - 1 downTo 0) { + val candidate = rows[index] + if (!candidate.empty && candidate.depth == depth - 1) return index + } + return null + } + + fun treeAction(rows: List, focusedIndex: Int?, key: FileTreeKey): FileTreeAction? { + val current = focusedIndex?.takeIf { index -> index in rows.indices && !rows[index].empty } + return when (key) { + FileTreeKey.Up -> (if (current == null) firstNodeIndex(rows) else previousNodeIndex(rows, current)) + ?.let(FileTreeAction::Focus) + + FileTreeKey.Down -> (if (current == null) firstNodeIndex(rows) else nextNodeIndex(rows, current)) + ?.let(FileTreeAction::Focus) + + FileTreeKey.Home -> firstNodeIndex(rows)?.let(FileTreeAction::Focus) + FileTreeKey.End -> lastNodeIndex(rows)?.let(FileTreeAction::Focus) + + FileTreeKey.Right -> current?.let { index -> + val row = rows[index] + when { + !row.isDir -> null + // Expanded: the first child follows its parent in flat order. + // An empty folder renders a placeholder row instead, which a + // reader cannot put the cursor on. + row.expanded -> (index + 1) + .takeIf { it in rows.indices && !rows[it].empty } + ?.let(FileTreeAction::Focus) + + else -> FileTreeAction.ToggleDirectory + } + } + + FileTreeKey.Left -> current?.let { index -> + val row = rows[index] + when { + row.isDir && row.expanded -> FileTreeAction.ToggleDirectory + else -> parentNodeIndex(rows, index)?.let(FileTreeAction::Focus) + } + } + + FileTreeKey.Activate -> current?.let { index -> + if (rows[index].isDir) FileTreeAction.ToggleDirectory else FileTreeAction.OpenFile + } + } + } + + /** + * The tab strip's target index: Left/Right wrap like the web's tablist, + * Home/End jump to the edges. Null when there is no tab to move to. + */ + fun tabIndex(count: Int, current: Int, key: FileTabKey): Int? { + if (count <= 0) return null + if (current !in 0 until count) { + // No active entry (e.g. a closed tab): any horizontal key starts at an edge. + return when (key) { + FileTabKey.Home, FileTabKey.Left -> 0 + FileTabKey.End, FileTabKey.Right -> count - 1 + } + } + return when (key) { + FileTabKey.Left -> (current - 1 + count) % count + FileTabKey.Right -> (current + 1) % count + FileTabKey.Home -> 0 + FileTabKey.End -> count - 1 + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileOpenMode.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileOpenMode.kt new file mode 100644 index 000000000..f1f903565 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileOpenMode.kt @@ -0,0 +1,13 @@ +package io.github.kahme247.ompweb.ui.files + +/** + * The viewer mode a file tab opens in (web `DisplayMode` in + * `components/FileViewer.tsx`). Git changes open a file in [Diff]; an + * explorer row or a markdown link opens it in the default mode its type + * allows. + * + * The enum lives in `ui.files` because the git pane's `onOpenFile` callback + * hands it to this package; the pure availability rules are + * `domain.fileViewerModes`. + */ +enum class FileOpenMode { Source, Preview, Diff } diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FilePanel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FilePanel.kt new file mode 100644 index 000000000..655dfa559 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FilePanel.kt @@ -0,0 +1,475 @@ +package io.github.kahme247.ompweb.ui.files + +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.fileName +import io.github.kahme247.ompweb.domain.relativeFilePath +import io.github.kahme247.ompweb.ui.git.GitChangesPane +import io.github.kahme247.ompweb.ui.git.GitChangesViewModel +import io.github.kahme247.ompweb.ui.hosttools.openExternalUrl +import io.github.kahme247.ompweb.ui.render.GithubRepoRef +import io.github.kahme247.ompweb.ui.render.LinkTarget +import io.github.kahme247.ompweb.ui.render.copyTextToClipboard +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpDropdownMenu +import io.github.kahme247.ompweb.ui.worktrees.WorktreesPane +import io.github.kahme247.ompweb.ui.worktrees.WorktreesViewModel +import io.github.kahme247.ompweb.ui.worktrees.rememberWorktreeActions +import kotlinx.coroutines.launch + +/** + * The right panel (web `components/RightPanel.tsx` + `components/TabBar.tsx`): + * the Explorer/Git/Worktrees tabs, the optional Notifications tab on phones + * and one tab per open file. + * + * The panel is pure layout plus the device plumbing a host cannot do for it + * (SAF save, share sheet, clipboard toast); every server call stays in + * [FilesViewModel] and Slice B's view models. The shell decides whether it is + * the third pane or a full-screen sheet, and passes the active composer's + * mention target in [onMention]. + */ +@Composable +fun FilePanel( + vm: FilesViewModel, + gitChanges: GitChangesViewModel, + worktrees: WorktreesViewModel, + onMention: (String) -> Unit, + onClose: () -> Unit, + githubRepo: GithubRepoRef? = null, + notifications: (@Composable () -> Unit)? = null, + modifier: Modifier = Modifier, +) { + val state by vm.state.collectAsStateWithLifecycle() + val worktreeState by worktrees.state.collectAsStateWithLifecycle() + val worktreeActions = rememberWorktreeActions(worktrees) + val colors = LocalOmpColors.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val pathCopiedText = stringResource(R.string.files_path_copied) + val copyFailedText = stringResource(R.string.toast_copy_failed) + val savedText = stringResource(R.string.files_download_saved) + val saveFailedText = stringResource(R.string.files_download_failed) + val shareFailedText = stringResource(R.string.files_share_failed) + + fun copyPath(text: String) { + val copied = copyTextToClipboard(context, "file path", text) + Toast.makeText(context, if (copied) pathCopiedText else copyFailedText, Toast.LENGTH_SHORT).show() + } + + var pendingSave by remember { mutableStateOf?>(null) } + val saveLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("*/*"), + ) { uri -> + val target = pendingSave + pendingSave = null + if (uri == null || target == null) return@rememberLauncherForActivityResult + scope.launch { + val bytes = runCatching { vm.fileBytes(target.first, target.second) }.getOrNull() + val saved = bytes != null && FileTransfer.writeToUri(context, uri, bytes) + Toast.makeText(context, if (saved) savedText else saveFailedText, Toast.LENGTH_SHORT).show() + } + } + + fun save(path: String, sessionId: String?) { + pendingSave = path to sessionId + saveLauncher.launch(fileName(path)) + } + + fun share(path: String, sessionId: String?, binaryMime: String?) { + scope.launch { + val bytes = runCatching { vm.fileBytes(path, sessionId) }.getOrNull() + val shared = bytes != null && FileTransfer.share( + context = context, + fileName = fileName(path), + mimeType = FileTransfer.mimeForPath(path, binaryMime), + bytes = bytes, + ) + if (!shared) Toast.makeText(context, shareFailedText, Toast.LENGTH_SHORT).show() + } + } + + val cwd = state.explorer.cwd + val linkHandler: (LinkTarget) -> Unit = { target -> + when (target) { + is LinkTarget.File -> vm.openTab(target.path, line = target.line, cwd = cwd) + + is LinkTarget.External -> openExternalUrl(context, target.url) + + is LinkTarget.GithubIssue -> { + val owner = target.owner.ifEmpty { githubRepo?.owner.orEmpty() } + val repo = target.repo.ifEmpty { githubRepo?.repo.orEmpty() } + if (owner.isNotEmpty() && repo.isNotEmpty()) { + openExternalUrl(context, "https://github.com/$owner/$repo/issues/${target.number}") + } + } + + is LinkTarget.Agent, is LinkTarget.Inert -> Unit + } + } + + Column( + modifier + .fillMaxSize() + .background(colors.background), + ) { + FilePanelHeader( + tabs = state.tabs, + activeTabId = state.activeTabId, + onClose = onClose, + onCloseOthers = vm::closeOtherTabs, + onCloseAll = vm::closeAllTabs, + ) + FileTabStrip( + view = state.view, + tabs = state.tabs, + activeTabId = state.activeTabId, + hasNotifications = notifications != null, + onSelectView = vm::setView, + onSelectTab = vm::selectTab, + onCloseTab = vm::closeTab, + ) + HorizontalDivider(color = colors.border) + + when (val view = state.view) { + FilesPanelView.Explorer -> FileExplorerPane( + state = state.explorer, + vm = vm, + onOpenFile = { path -> vm.openTab(path, cwd = cwd) }, + onMention = onMention, + onCopyPath = { path -> copyPath(path) }, + onDownload = { path -> save(path, null) }, + onShare = { path -> share(path, null, null) }, + ) + + FilesPanelView.Git -> GitChangesPane( + cwd = cwd, + onOpenFile = { path, mode -> vm.openTab(path, mode = mode, cwd = cwd) }, + viewModel = gitChanges, + ) + + FilesPanelView.Worktrees -> WorktreesPane( + context = worktreeState, + actions = worktreeActions, + ) + + FilesPanelView.Notifications -> { + if (notifications != null) { + Box(Modifier.fillMaxSize()) { notifications() } + } else { + // Formerly a phone tab; a wide layout has no Notifications tab. + LaunchedEffect(Unit) { vm.setView(FilesPanelView.Explorer) } + } + } + + FilesPanelView.File -> { + val tab = state.activeTab + if (tab == null) { + LaunchedEffect(Unit) { vm.setView(FilesPanelView.Explorer) } + } else { + FileViewerPane( + tab = tab, + cwd = cwd, + githubRepo = githubRepo, + vm = vm, + onLink = linkHandler, + onMention = onMention, + onCopyPath = { copyPath(relativeFilePath(tab.path, cwd).ifEmpty { tab.path }) }, + onDownload = { path -> save(path, tab.sourceSessionId) }, + onShare = { path -> share(path, tab.sourceSessionId, tab.binaryMime) }, + onReveal = { vm.reveal(tab.path) }, + ) + } + } + } + } +} + +@Composable +private fun FilePanelHeader( + tabs: List, + activeTabId: String?, + onClose: () -> Unit, + onCloseOthers: () -> Unit, + onCloseAll: () -> Unit, +) { + val colors = LocalOmpColors.current + var menuOpen by remember { mutableStateOf(false) } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 2.dp, top = 2.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.files_panel_title), + color = colors.text, + fontSize = 12.5.sp, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.weight(1f)) + Box { + IconButton( + onClick = { menuOpen = true }, + enabled = tabs.isNotEmpty(), + modifier = Modifier.size(28.dp), + ) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = stringResource(R.string.files_tab_actions), + tint = if (tabs.isEmpty()) colors.textDim.copy(alpha = 0.4f) else colors.textMuted, + modifier = Modifier.size(16.dp), + ) + } + OmpDropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { + DropdownMenuItem( + text = { Text(stringResource(R.string.files_close_other_tabs), fontSize = 12.sp) }, + enabled = tabs.size > 1 && activeTabId != null, + onClick = { + menuOpen = false + onCloseOthers() + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.files_close_all_tabs), fontSize = 12.sp) }, + onClick = { + menuOpen = false + onCloseAll() + }, + ) + } + } + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.files_close_panel), + tint = colors.textMuted, + modifier = Modifier.size(16.dp), + ) + } + } +} + +/** Test tag prefix of one tab; the key tests address tabs by view or file id. */ +internal const val FILE_TAB_TAG_PREFIX = "files-tab:" + +/** One rendered tab: a fixed view entry or an open file (`tabId`). */ +internal data class FileTabEntry( + val key: String, + val label: String, + val view: FilesPanelView? = null, + val tabId: String? = null, +) + +@Composable +internal fun FileTabStrip( + view: FilesPanelView, + tabs: List, + activeTabId: String?, + hasNotifications: Boolean, + onSelectView: (FilesPanelView) -> Unit, + onSelectTab: (String) -> Unit, + onCloseTab: (String) -> Unit, +) { + val entries = buildList { + add(FileTabEntry("explorer", stringResource(R.string.files_tab_explorer), FilesPanelView.Explorer)) + add(FileTabEntry("git", stringResource(R.string.git_tab_title), FilesPanelView.Git)) + add(FileTabEntry("worktrees", stringResource(R.string.worktree_tab_title), FilesPanelView.Worktrees)) + if (hasNotifications) { + add(FileTabEntry("notifications", stringResource(R.string.notify_ui_title), FilesPanelView.Notifications)) + } + tabs.forEach { tab -> add(FileTabEntry(tab.id, fileName(tab.path), tabId = tab.id)) } + } + val activeKey = if (view == FilesPanelView.File && activeTabId != null) { + activeTabId + } else { + entries.firstOrNull { it.view == view }?.key + } + val requesters = remember(entries.map { it.key }) { + entries.associate { entry -> entry.key to FocusRequester() } + } + + fun activate(entry: FileTabEntry) { + if (entry.tabId != null) onSelectTab(entry.tabId) else entry.view?.let(onSelectView) + } + + val onKey: (FileTabEntry, KeyEvent) -> Boolean = { entry, event -> + if (event.type != KeyEventType.KeyDown) { + false + } else { + when (event.key) { + Key.Enter, Key.NumPadEnter, Key.Spacebar -> { + activate(entry) + true + } + + Key.DirectionLeft, Key.DirectionRight, Key.MoveHome, Key.MoveEnd -> { + val navKey = when (event.key) { + Key.DirectionLeft -> FileTabKey.Left + Key.DirectionRight -> FileTabKey.Right + Key.MoveHome -> FileTabKey.Home + else -> FileTabKey.End + } + FileKeyNav.tabIndex(entries.size, entries.indexOf(entry), navKey)?.let { target -> + val next = entries[target] + // The web's tablist selects on the way and keeps focus on + // the tab, so the next arrow starts from there. + activate(next) + requesters[next.key]?.requestFocus() + } + true + } + + Key.Delete, Key.Backspace -> { + // Only file tabs close; fixed tabs keep the key for the host. + val tabId = entry.tabId + if (tabId != null) onCloseTab(tabId) + tabId != null + } + + else -> false + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 6.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + entries.forEach { entry -> + val modifier = Modifier + .focusRequester(requesters.getValue(entry.key)) + .onPreviewKeyEvent { event -> onKey(entry, event) } + .testTag(FILE_TAB_TAG_PREFIX + entry.key) + if (entry.tabId != null) { + val tab = tabs.first { it.id == entry.tabId } + FileTabChip( + tab = tab, + selected = activeKey == entry.key, + onClick = { onSelectTab(tab.id) }, + onClose = { onCloseTab(tab.id) }, + modifier = modifier, + ) + } else { + TabChip( + label = entry.label, + selected = activeKey == entry.key, + onClick = { entry.view?.let(onSelectView) }, + modifier = modifier, + ) + } + } + } +} + +@Composable +private fun TabChip( + label: String, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + Text( + text = label, + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(if (selected) colors.selected else colors.backgroundSubtle) + .clickable(onClick = onClick) + .padding(horizontal = 8.dp, vertical = 4.dp), + color = if (selected) colors.text else colors.textMuted, + fontSize = 11.sp, + fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal, + maxLines = 1, + ) +} + +@Composable +private fun FileTabChip( + tab: FileTab, + selected: Boolean, + onClick: () -> Unit, + onClose: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + val name = fileName(tab.path) + Row( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(if (selected) colors.selected else colors.backgroundSubtle) + .clickable(onClick = onClick) + .padding(start = 8.dp, end = 2.dp, top = 3.dp, bottom = 3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = name, + color = if (selected) colors.text else colors.textMuted, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.files_close_tab, name), + tint = colors.textDim, + modifier = Modifier.size(12.dp), + ) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileTabsStore.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileTabsStore.kt new file mode 100644 index 000000000..ec41a5982 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileTabsStore.kt @@ -0,0 +1,114 @@ +package io.github.kahme247.ompweb.ui.files + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.put + +/** One open file tab as it survives a process death: its path and its file-API session. */ +data class StoredFileTab(val path: String, val sourceSessionId: String?) + +/** + * Open file tabs, persisted per server installation. The web keeps its file + * tabs in memory and loses them on reload; the app restores them on a cold + * start (phase 9 spec item 5), so a process death does not close the panel's + * files. + */ +interface FileTabStore { + suspend fun load(origin: String?): List + + suspend fun save(origin: String?, tabs: List) + + suspend fun clear() +} + +/** JVM tests and previews: tabs stay in memory. */ +class MemoryFileTabStore : FileTabStore { + private var origin: String? = null + private var tabs: List = emptyList() + + override suspend fun load(origin: String?): List = + if (this.origin == origin) tabs else emptyList() + + override suspend fun save(origin: String?, tabs: List) { + this.origin = origin + this.tabs = tabs + } + + override suspend fun clear() { + origin = null + tabs = emptyList() + } +} + +private val Context.fileTabsDataStore: DataStore by preferencesDataStore(name = "omp-file-tabs") + +/** + * DataStore-backed [FileTabStore]. The origin is stored with the paths, so + * switching servers can never reopen another installation's files (the token + * is the origin, never a path list from a different host). + */ +class PreferencesFileTabStore(context: Context) : FileTabStore { + + private val dataStore = context.applicationContext.fileTabsDataStore + private val json = Json { ignoreUnknownKeys = true } + + override suspend fun load(origin: String?): List { + val raw = dataStore.data.first()[TABS_KEY] ?: return emptyList() + val root = runCatching { json.parseToJsonElement(raw).jsonObject }.getOrNull() ?: return emptyList() + val storedOrigin = (root["origin"] as? JsonPrimitive)?.contentOrNull + if (storedOrigin != origin) return emptyList() + val tabs = root["tabs"] as? JsonArray ?: return emptyList() + return tabs.mapNotNull { element -> + val tab = element as? JsonObject ?: return@mapNotNull null + val path = (tab["path"] as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotBlank() } + ?: return@mapNotNull null + StoredFileTab(path, (tab["sourceSessionId"] as? JsonPrimitive)?.contentOrNull) + } + } + + override suspend fun save(origin: String?, tabs: List) { + val payload = buildJsonObject { + put("origin", origin?.let { JsonPrimitive(it) } ?: JsonNull) + put( + "tabs", + buildJsonArray { + tabs.forEach { tab -> + add( + buildJsonObject { + put("path", tab.path) + if (tab.sourceSessionId == null) { + put("sourceSessionId", JsonNull) + } else { + put("sourceSessionId", tab.sourceSessionId) + } + }, + ) + } + }, + ) + } + dataStore.edit { preferences -> preferences[TABS_KEY] = payload.toString() } + } + + override suspend fun clear() { + dataStore.edit { preferences -> preferences.remove(TABS_KEY) } + } + + private companion object { + val TABS_KEY = stringPreferencesKey("open-file-tabs") + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileTransfer.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileTransfer.kt new file mode 100644 index 000000000..ce6cfb945 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileTransfer.kt @@ -0,0 +1,227 @@ +package io.github.kahme247.ompweb.ui.files + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.core.content.FileProvider +import io.github.kahme247.ompweb.chat.FileLinks +import io.github.kahme247.ompweb.domain.MAX_UPLOAD_BYTES_PER_FILE +import io.github.kahme247.ompweb.domain.MAX_UPLOAD_BYTES_TOTAL +import io.github.kahme247.ompweb.net.UploadFile +import java.io.ByteArrayOutputStream +import java.io.File +import java.net.URLEncoder +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** What the upload picker produced: readable files, and names it had to skip. */ +data class UploadPick(val files: List = emptyList(), val rejected: List = emptyList()) { + val isEmpty: Boolean get() = files.isEmpty() && rejected.isEmpty() +} + +/** + * Device-side halves of the file panel: reading picked documents into bounded + * byte arrays, saving a server file through SAF, sharing one through the app's + * FileProvider, and building a `?type=read` URL for Coil. + * + * The server caps a single upload at 25 MiB and buffers every request body up + * to 26 MiB before auth, so the picker never buffers more than 25 MiB per file + * and the upload sends one file per request; the batch stops at the 100 MiB + * aggregate cap. An oversized document is reported instead of read. + */ +object FileTransfer { + + private const val READ_CHUNK = 64 * 1024 + + private val shareCleanup = Executors.newSingleThreadScheduledExecutor { runnable -> + Thread(runnable, "file-share-cleanup").apply { isDaemon = true } + } + + /** Reads every pick, skipping unreadable or > [MAX_UPLOAD_BYTES_PER_FILE] documents. */ + suspend fun readUploads( + context: Context, + uris: List, + usedNames: Set = emptySet(), + ): UploadPick = withContext(Dispatchers.IO) { + collectUploads( + picks = uris, + usedNames = usedNames, + nameOf = { uri, index -> displayName(context, uri, fallback = "file-${index + 1}") }, + read = { uri, limit -> readBounded(context, uri, limit) }, + ) + } + + /** + * The batch loop of [readUploads] without Android types, so the budget + * arithmetic is testable on the JVM. Every pick is read at most up to the + * per-file cap and whatever is left of the aggregate budget; a pick that + * does not fit is reported instead of buffered. [read] must return the + * bytes or null when the document is unreadable or larger than its limit. + */ + internal fun collectUploads( + picks: List, + nameOf: (T, Int) -> String, + read: (T, Long) -> ByteArray?, + perFileLimit: Long = MAX_UPLOAD_BYTES_PER_FILE, + totalLimit: Long = MAX_UPLOAD_BYTES_TOTAL, + usedNames: Set = emptySet(), + ): UploadPick { + val files = mutableListOf() + val rejected = mutableListOf() + val taken = usedNames.toMutableSet() + // The server rejects a batch above the aggregate cap, so nothing past + // it is buffered: every later file is reported instead of read. + var budget = totalLimit + picks.forEachIndexed { index, pick -> + val name = nameOf(pick, index) + val unique = uniqueName(name, taken) + val bytes = read(pick, minOf(perFileLimit, budget.coerceAtLeast(0L))) + if (bytes == null) { + rejected += name + } else { + budget -= bytes.size + taken += unique + files += UploadFile(unique, bytes) + } + } + return UploadPick(files, rejected) + } + + /** SAF save; returns false when the destination could not be written. */ + suspend fun writeToUri(context: Context, uri: Uri, bytes: ByteArray): Boolean = + withContext(Dispatchers.IO) { + try { + context.contentResolver.openOutputStream(uri, "wt")?.use { stream -> + stream.write(bytes) + stream.flush() + } != null + } catch (_: Exception) { + false + } + } + + /** + * Shares [bytes] as a real file. The cache copy lives under `exports/` + * (the only shareable FileProvider path) and is deleted after the receiver + * had its chance, like the transcript share. + */ + fun share(context: Context, fileName: String, mimeType: String, bytes: ByteArray): Boolean = try { + val directory = File(context.cacheDir, "exports").apply { mkdirs() } + val safeName = sanitizeShareName(fileName) + val file = File(directory, safeName) + file.writeBytes(bytes) + val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) + val intent = Intent(Intent.ACTION_SEND).apply { + type = mimeType + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(intent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + shareCleanup.schedule({ file.delete() }, 60L, TimeUnit.SECONDS) + true + } catch (_: Exception) { + false + } + + /** + * The provider's suggested name for [uri], or null when it has none. One + * cursor query for the upload picker, the composer's attachments and the + * saved transcript. + */ + fun queryDisplayName(context: Context, uri: Uri): String? = try { + val name = context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> + val index = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME) + if (index >= 0 && cursor.moveToFirst()) cursor.getString(index) else null + } + name?.takeIf { it.isNotBlank() }?.substringAfterLast('/')?.substringAfterLast('\\') + } catch (_: SecurityException) { + // A share URI from another app may not be readable here. + null + } catch (_: IllegalArgumentException) { + null + } + + fun displayName(context: Context, uri: Uri, fallback: String): String = + queryDisplayName(context, uri) ?: fallback + + /** `GET /api/files/?type=read` as a relative URL (Coil resolves the base). */ + fun readUrl(path: String, sessionId: String?, version: Int = 0): String { + val encoded = FileLinks.apiPathSegments(path).joinToString("/") { segment -> + URLEncoder.encode(segment, "UTF-8").replace("+", "%20") + } + val session = sessionId?.takeIf { it.isNotBlank() }?.let { + "&sessionId=" + URLEncoder.encode(it, "UTF-8") + }.orEmpty() + val cacheBust = if (version > 0) "&v=$version" else "" + return "/api/files/$encoded?type=read$session$cacheBust" + } + + /** + * A MIME type for sharing a viewed file. Text keeps an extension-derived + * type so mail clients and editors show it correctly. + */ + fun mimeForPath(path: String, binaryMime: String?): String { + if (binaryMime != null) return binaryMime + return when (path.substringAfterLast('.', "").lowercase()) { + "md", "markdown", "mdx" -> "text/markdown" + "json" -> "application/json" + "html", "htm" -> "text/html" + "css" -> "text/css" + "js", "mjs", "cjs" -> "text/javascript" + "ts", "kt", "java", "py", "rb", "go", "rs", "swift", "c", "cpp", "h", "hpp", + "cs", "sh", "bash", "zsh", "sql", "yaml", "yml", "toml", "xml", "txt", "log", + -> "text/plain" + "csv" -> "text/csv" + "png" -> "image/png" + "jpg", "jpeg" -> "image/jpeg" + "gif" -> "image/gif" + "webp" -> "image/webp" + "svg" -> "image/svg+xml" + "pdf" -> "application/pdf" + "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + "mp3" -> "audio/mpeg" + "wav" -> "audio/wav" + "m4a" -> "audio/mp4" + "ogg" -> "audio/ogg" + "flac" -> "audio/flac" + else -> "application/octet-stream" + } + } + + /** Reads at most [limit] bytes; null when the document is unreadable or larger. */ + private fun readBounded(context: Context, uri: Uri, limit: Long): ByteArray? = try { + context.contentResolver.openInputStream(uri)?.use { stream -> + val output = ByteArrayOutputStream() + val buffer = ByteArray(READ_CHUNK) + var total = 0L + while (true) { + val read = stream.read(buffer) + if (read < 0) break + total += read + if (total > limit) return null + output.write(buffer, 0, read) + } + output.toByteArray() + } + } catch (_: Exception) { + null + } + + private fun uniqueName(name: String, taken: MutableSet): String { + if (name !in taken) return name + val dot = name.lastIndexOf('.') + val base = if (dot > 0) name.substring(0, dot) else name + val extension = if (dot > 0) name.substring(dot) else "" + var index = 2 + while ("$base ($index)$extension" in taken) index++ + return "$base ($index)$extension" + } + + private fun sanitizeShareName(name: String): String { + val stripped = name.substringAfterLast('/').substringAfterLast('\\') + val safe = stripped.replace(Regex("[^A-Za-z0-9._-]"), "_") + return safe.ifBlank { "file.txt" } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileViewerPane.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileViewerPane.kt new file mode 100644 index 000000000..5c386a42f --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FileViewerPane.kt @@ -0,0 +1,666 @@ +package io.github.kahme247.ompweb.ui.files + +import android.graphics.Bitmap +import android.graphics.pdf.PdfRenderer +import android.os.ParcelFileDescriptor +import android.webkit.WebView +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.TextPreviewKind +import io.github.kahme247.ompweb.domain.ViewerKind +import io.github.kahme247.ompweb.domain.fileMentionText +import io.github.kahme247.ompweb.domain.fileDirectory +import io.github.kahme247.ompweb.domain.fileName +import io.github.kahme247.ompweb.domain.formatFileSize +import io.github.kahme247.ompweb.domain.relativeFilePath +import io.github.kahme247.ompweb.domain.textPreviewKind +import io.github.kahme247.ompweb.domain.viewerKind +import io.github.kahme247.ompweb.ui.render.CodeBlock +import io.github.kahme247.ompweb.ui.render.DiffView +import io.github.kahme247.ompweb.ui.render.GithubRepoRef +import io.github.kahme247.ompweb.ui.render.ImageSource +import io.github.kahme247.ompweb.ui.render.LinkTarget +import io.github.kahme247.ompweb.ui.render.LocalFileLinkBaseDir +import io.github.kahme247.ompweb.ui.render.MarkdownText +import io.github.kahme247.ompweb.ui.render.RemoteImage +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpDropdownMenu +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * One open file (web `components/FileViewer.tsx`): the header with the mode + * switch, wrap toggle and the file actions, then the body — source with + * syntax colours, markdown/HTML preview, the git diff in Phase-5's [DiffView], + * the zoomable image, the PDF through `PdfRenderer` and the server-rendered + * docx document. + * + * Every byte comes from `FilesApi`; the images use the authenticated Coil + * loader of the render layer, so a file outside the workspace works as long as + * the tab carries its `sourceSessionId`. + */ +@Composable +fun FileViewerPane( + tab: FileTab, + cwd: String?, + githubRepo: GithubRepoRef?, + vm: FilesViewModel, + onLink: (LinkTarget) -> Unit, + onMention: (String) -> Unit, + onCopyPath: () -> Unit, + onDownload: (String) -> Unit, + onShare: (String) -> Unit, + onReveal: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + val kind = viewerKind(tab.path) + val relative = relativeFilePath(tab.path, cwd) + Column(modifier.fillMaxSize()) { + ViewerHeader( + tab = tab, + relativePath = relative, + onMention = { onMention(fileMentionText(relative, tab.line)) }, + onCopyPath = onCopyPath, + onDownload = { onDownload(tab.path) }, + onShare = { onShare(tab.path) }, + onReveal = onReveal, + ) + if (tab.availableModes.isNotEmpty()) { + ViewerModeBar( + modes = tab.availableModes, + current = tab.mode, + wrap = tab.wrap, + onMode = vm::setMode, + onToggleWrap = vm::toggleWrap, + ) + } + val error = tab.error + when { + // A diff tab reads no bytes: its body must not be masked by a + // read error of a mode the user is not looking at. + kind == ViewerKind.Text && tab.mode == FileOpenMode.Diff -> TextBody( + tab = tab, + githubRepo = githubRepo, + onLink = onLink, + onRetryDiff = vm::retryDiff, + ) + + error != null -> ViewerMessage( + message = error, + action = stringResource(R.string.files_retry) to vm::retryTab, + ) + + tab.loading -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator(strokeWidth = 2.dp, color = colors.accent, modifier = Modifier.size(20.dp)) + } + + kind == ViewerKind.Image -> { + val source = remember(tab.path, tab.sourceSessionId, tab.imageVersion) { + ImageSource.Url(FileTransfer.readUrl(tab.path, tab.sourceSessionId, tab.imageVersion)) + } + Box(Modifier.fillMaxSize().padding(8.dp), contentAlignment = Alignment.Center) { + RemoteImage( + source = source, + contentDescription = fileName(tab.path), + thumbnail = false, + modifier = Modifier.fillMaxWidth(), + ) + } + } + + kind == ViewerKind.Pdf -> PdfPages(tab = tab) + + kind == ViewerKind.Docx -> { + val html = tab.docxHtml + if (html == null) { + ViewerMessage(message = stringResource(R.string.files_docx_failed), action = null) + } else { + HtmlDocument(html = html, modifier = Modifier.fillMaxSize()) + } + } + + tab.binaryMime != null -> BinaryNotice( + mime = tab.binaryMime, + size = tab.size, + onDownload = { onDownload(tab.path) }, + onShare = { onShare(tab.path) }, + ) + + else -> TextBody( + tab = tab, + githubRepo = githubRepo, + onLink = onLink, + onRetryDiff = vm::retryDiff, + ) + } + } +} + +@Composable +private fun ViewerHeader( + tab: FileTab, + relativePath: String, + onMention: () -> Unit, + onCopyPath: () -> Unit, + onDownload: () -> Unit, + onShare: () -> Unit, + onReveal: () -> Unit, +) { + val colors = LocalOmpColors.current + var menuOpen by remember(tab.path) { mutableStateOf(false) } + Column( + Modifier + .fillMaxWidth() + .background(colors.panel) + .padding(start = 10.dp, end = 2.dp, top = 4.dp, bottom = 2.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = fileName(tab.path), + modifier = Modifier.weight(1f, fill = false), + color = colors.text, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (tab.watching) { + Spacer(Modifier.width(6.dp)) + Box( + Modifier + .size(6.dp) + .background(colors.statusSuccess, RoundedCornerShape(50)), + ) + Spacer(Modifier.width(4.dp)) + Text( + text = stringResource(R.string.files_watching), + color = colors.statusSuccess, + fontSize = 9.5.sp, + ) + } + Spacer(Modifier.weight(1f)) + Box { + IconButton(onClick = { menuOpen = true }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = stringResource(R.string.files_row_actions), + tint = colors.textMuted, + modifier = Modifier.size(16.dp), + ) + } + OmpDropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { + ViewerMenuItem(R.string.files_mention) { + menuOpen = false + onMention() + } + ViewerMenuItem(R.string.files_copy_path) { + menuOpen = false + onCopyPath() + } + ViewerMenuItem(R.string.files_reveal) { + menuOpen = false + onReveal() + } + ViewerMenuItem(R.string.files_download) { + menuOpen = false + onDownload() + } + ViewerMenuItem(R.string.files_share) { + menuOpen = false + onShare() + } + } + } + } + Text( + text = buildString { + append(relativePath) + tab.line?.let { append(" · ").append(stringResource(R.string.files_line, it)) } + }, + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun ViewerModeBar( + modes: List, + current: FileOpenMode, + wrap: Boolean, + onMode: (FileOpenMode) -> Unit, + onToggleWrap: () -> Unit, +) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + modes.forEach { mode -> + val label = when (mode) { + FileOpenMode.Source -> stringResource(R.string.files_mode_source) + FileOpenMode.Preview -> stringResource(R.string.files_mode_preview) + FileOpenMode.Diff -> stringResource(R.string.files_mode_diff) + } + Text( + text = label, + modifier = Modifier + .background( + if (mode == current) colors.selected else colors.backgroundSubtle, + RoundedCornerShape(6.dp), + ) + .clickable { onMode(mode) } + .padding(horizontal = 8.dp, vertical = 4.dp), + color = if (mode == current) colors.text else colors.textMuted, + fontSize = 11.sp, + fontWeight = if (mode == current) FontWeight.Medium else FontWeight.Normal, + ) + } + Spacer(Modifier.weight(1f)) + if (current == FileOpenMode.Source) { + Text( + text = stringResource(if (wrap) R.string.files_wrap_off else R.string.files_wrap_on), + modifier = Modifier + .clickable(onClick = onToggleWrap) + .padding(horizontal = 6.dp, vertical = 4.dp), + color = colors.textDim, + fontSize = 10.5.sp, + ) + } + } +} + +@Composable +private fun TextBody( + tab: FileTab, + githubRepo: GithubRepoRef?, + onLink: (LinkTarget) -> Unit, + onRetryDiff: () -> Unit, +) { + val colors = LocalOmpColors.current + when (tab.mode) { + FileOpenMode.Diff -> { + val diffError = tab.diffError + if (diffError != null) { + // Before the empty state: "no diff" is not true when the fetch + // failed, and the retry is the only way out of a flaky answer. + ViewerMessage( + message = stringResource(R.string.files_diff_failed, diffError), + action = stringResource(R.string.files_retry) to onRetryDiff, + ) + } else if (tab.diffLoading) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator(strokeWidth = 2.dp, color = colors.accent, modifier = Modifier.size(18.dp)) + } + } else { + val diff = tab.diff + if (diff == null) { + ViewerMessage(message = stringResource(R.string.files_diff_empty), action = null) + } else { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(8.dp), + ) { + DiffView(path = tab.path, oldText = null, newText = null, unifiedPatch = diff) + } + } + } + } + + FileOpenMode.Preview -> { + val preview = textPreviewKind(tab.language) + val content = tab.content + when { + content == null -> ViewerMessage(message = stringResource(R.string.files_no_preview), action = null) + + preview == TextPreviewKind.Markdown -> Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(10.dp), + ) { + val baseDir = fileDirectory(tab.path) + CompositionLocalProvider(LocalFileLinkBaseDir provides baseDir) { + MarkdownText( + markdown = content, + githubRepo = githubRepo, + onLink = onLink, + ) + } + } + + preview == TextPreviewKind.Html -> HtmlDocument(html = content, modifier = Modifier.fillMaxSize()) + + else -> ViewerMessage(message = stringResource(R.string.files_no_preview), action = null) + } + } + + FileOpenMode.Source -> { + val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() + // The viewport's top in the window, so a reported line position + // can be translated into a scroll offset. + var viewportTop by remember(tab.path) { mutableFloatStateOf(0f) } + var revealedFor by remember(tab.path) { mutableStateOf(null) } + val revealKey = tab.line?.let { "${tab.path}:$it:${tab.wrap}" } + Box( + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .onGloballyPositioned { viewportTop = it.positionInRoot().y }, + ) { + Column(Modifier.padding(8.dp)) { + CodeBlock( + code = tab.content.orEmpty(), + language = tab.language, + wrap = tab.wrap, + modifier = Modifier.fillMaxWidth(), + revealLine = tab.line, + onRevealLine = { lineTop -> + if (revealKey != null && revealedFor != revealKey) { + revealedFor = revealKey + scope.launch { + val target = scrollState.value + lineTop - viewportTop + scrollState.scrollTo(target.toInt().coerceAtLeast(0)) + } + } + }, + ) + } + } + } + } +} + +@Composable +private fun BinaryNotice( + mime: String, + size: Long, + onDownload: () -> Unit, + onShare: () -> Unit, +) { + val colors = LocalOmpColors.current + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(22.dp), + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.files_binary_title), + color = colors.text, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource( + R.string.files_binary_body, + mime.ifBlank { stringResource(R.string.files_binary_unknown_type) }, + formatFileSize(size), + ), + color = colors.textMuted, + fontSize = 11.5.sp, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(10.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TextButton(onClick = onDownload) { Text(stringResource(R.string.files_download)) } + TextButton(onClick = onShare) { Text(stringResource(R.string.files_share)) } + } + } +} + +@Composable +private fun ViewerMessage(message: String, action: Pair Unit>?) { + val colors = LocalOmpColors.current + Column( + modifier = Modifier.fillMaxSize().padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = message, + color = colors.textMuted, + fontSize = 12.sp, + textAlign = TextAlign.Center, + ) + if (action != null) { + Spacer(Modifier.height(8.dp)) + TextButton(onClick = action.second) { + Text(action.first, color = colors.accent, fontSize = 12.sp) + } + } + } +} + +/** The server-rendered document (docx) and an HTML source preview. */ +@Composable +private fun HtmlDocument(html: String, modifier: Modifier = Modifier) { + // `update` runs on every recomposition; `loadDataWithBaseURL` would reset + // the scroll position each time, so only a changed document is loaded. + val loaded = remember { arrayOfNulls(1) } + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { configureHtmlPreview(this) } + }, + update = { view -> + if (loaded[0] != html) { + loaded[0] = html + loadHtmlPreview(view, html) + } + }, + ) +} + +/** + * PDF pages through the platform `PdfRenderer`: the file was downloaded into + * the app cache by `FilesViewModel`, so no byte ever leaves the device again. + * The bitmaps are rendered off the main thread, one page at a time. + */ +@Composable +private fun PdfPages(tab: FileTab) { + val colors = LocalOmpColors.current + val path = tab.pdfPath + if (path == null) { + ViewerMessage(message = stringResource(R.string.files_pdf_failed), action = null) + return + } + val file = remember(path) { File(path) } + var pageIndex by remember(path) { mutableIntStateOf(0) } + val measured by produceState(initialValue = null, path) { + value = withContext(Dispatchers.IO) { pdfPageCount(file) } + } + val pageCount = measured + if (pageCount == null) { + // Counting a large PDF takes a moment; claiming a failure here would + // flash "could not render" for every file that opens fine. + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator(strokeWidth = 2.dp, color = colors.accent, modifier = Modifier.size(20.dp)) + } + return + } + if (pageCount <= 0) { + ViewerMessage(message = stringResource(R.string.files_pdf_failed), action = null) + return + } + val index = pageIndex.coerceIn(0, pageCount - 1) + BoxWithConstraints(Modifier.fillMaxSize()) { + val widthPx = with(LocalDensity.current) { maxWidth.roundToPx() } + val page by produceState?>(null, path, index, widthPx) { + value = withContext(Dispatchers.IO) { renderPdfPage(file, index, widthPx) } + } + Column(Modifier.fillMaxSize()) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + onClick = { if (index > 0) pageIndex = index - 1 }, + enabled = index > 0, + modifier = Modifier.size(30.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = stringResource(R.string.files_pdf_previous), + tint = if (index > 0) colors.textMuted else colors.textDim, + modifier = Modifier.size(18.dp), + ) + } + Text( + text = stringResource(R.string.files_pdf_page, index + 1, pageCount), + modifier = Modifier.weight(1f), + color = colors.textMuted, + fontSize = 11.sp, + textAlign = TextAlign.Center, + ) + IconButton( + onClick = { if (index < pageCount - 1) pageIndex = index + 1 }, + enabled = index < pageCount - 1, + modifier = Modifier.size(30.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = stringResource(R.string.files_pdf_next), + tint = if (index < pageCount - 1) colors.textMuted else colors.textDim, + modifier = Modifier.size(18.dp), + ) + } + } + val current = page + Box( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .background(colors.backgroundSubtle), + contentAlignment = Alignment.TopCenter, + ) { + when { + current == null -> CircularProgressIndicator( + strokeWidth = 2.dp, + color = colors.accent, + modifier = Modifier.padding(top = 24.dp).size(20.dp), + ) + + current.isFailure -> Text( + text = stringResource(R.string.files_pdf_failed), + modifier = Modifier.padding(24.dp), + color = colors.statusError, + fontSize = 12.sp, + ) + + else -> Image( + bitmap = current.getOrThrow(), + contentDescription = stringResource(R.string.files_pdf_page, index + 1, pageCount), + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + } +} + +private fun pdfPageCount(file: File): Int = runCatching { + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { descriptor -> + PdfRenderer(descriptor).use { renderer -> renderer.pageCount } + } +}.getOrDefault(0) + +private fun renderPdfPage( + file: File, + index: Int, + widthPx: Int, +): Result = runCatching { + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { descriptor -> + PdfRenderer(descriptor).use { renderer -> + val target = index.coerceIn(0, renderer.pageCount - 1) + renderer.openPage(target).use { page -> + // Long pages stay readable: the width is the pane, the height follows the ratio. + val width = maxOf(1, widthPx) + val height = maxOf(1, (page.height.toFloat() * width / page.width).toInt()) + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + bitmap.eraseColor(android.graphics.Color.WHITE) + page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + bitmap.asImageBitmap() + } + } + } +} + +@Composable +private fun ViewerMenuItem(label: Int, onClick: () -> Unit) { + DropdownMenuItem( + text = { Text(stringResource(label), fontSize = 12.sp) }, + onClick = onClick, + ) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FilesViewModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FilesViewModel.kt new file mode 100644 index 000000000..02e810d56 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/FilesViewModel.kt @@ -0,0 +1,1217 @@ +package io.github.kahme247.ompweb.ui.files + +import io.github.kahme247.ompweb.chat.FileLinks +import io.github.kahme247.ompweb.domain.FileEntry +import io.github.kahme247.ompweb.domain.FileSearchRow +import io.github.kahme247.ompweb.domain.GitFileStatus +import io.github.kahme247.ompweb.domain.MAX_UPLOAD_BYTES_PER_FILE +import io.github.kahme247.ompweb.domain.MAX_UPLOAD_BYTES_TOTAL +import io.github.kahme247.ompweb.domain.UploadConflictPlan +import io.github.kahme247.ompweb.domain.UploadConflictStrategy +import io.github.kahme247.ompweb.domain.ViewerKind +import io.github.kahme247.ompweb.domain.atMentionsText +import io.github.kahme247.ompweb.domain.availableViewerModes +import io.github.kahme247.ompweb.domain.comparableFilePath +import io.github.kahme247.ompweb.domain.defaultViewerMode +import io.github.kahme247.ompweb.domain.effectiveViewerMode +import io.github.kahme247.ompweb.domain.fileSearchRows +import io.github.kahme247.ompweb.domain.isAbsoluteFilePath +import io.github.kahme247.ompweb.domain.isInsideDirectory +import io.github.kahme247.ompweb.domain.joinFilePath +import io.github.kahme247.ompweb.domain.mergeDirectoryListing +import io.github.kahme247.ompweb.domain.normalizeFilePath +import io.github.kahme247.ompweb.domain.relativeFilePath +import io.github.kahme247.ompweb.domain.revealAncestors +import io.github.kahme247.ompweb.domain.uploadConflictPlan +import io.github.kahme247.ompweb.domain.viewerKind +import io.github.kahme247.ompweb.events.EventChannel +import io.github.kahme247.ompweb.events.EventSubscriptionException +import io.github.kahme247.ompweb.events.SessionEventSource +import io.github.kahme247.ompweb.net.ApiError +import io.github.kahme247.ompweb.net.FileReadResult +import io.github.kahme247.ompweb.net.FileIndexPage +import io.github.kahme247.ompweb.net.FilesApi +import io.github.kahme247.ompweb.net.MAX_FILE_SEARCH_LIMIT +import io.github.kahme247.ompweb.net.GitApi +import io.github.kahme247.ompweb.net.UploadFile +import io.github.kahme247.ompweb.net.captureApi +import io.github.kahme247.ompweb.net.userMessage +import java.io.File +import java.security.MessageDigest +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull + +/** The tab strip's fixed entries; file tabs are additional. */ +enum class FilesPanelView { Explorer, Git, Worktrees, Notifications, File } + +/** One open file tab and everything the viewer needs for it. */ +data class FileTab( + val path: String, + val sourceSessionId: String? = null, + /** A line to scroll to when the tab opens (markdown link, host tool). */ + val line: Int? = null, + /** The mode the opener asked for; `null` means "pick the type's default". */ + val requestedMode: FileOpenMode? = null, + val mode: FileOpenMode = FileOpenMode.Source, + val wrap: Boolean = false, + val loading: Boolean = false, + val content: String? = null, + val language: String? = null, + val size: Long = 0, + val binaryMime: String? = null, + val error: String? = null, + val watching: Boolean = false, + val diff: String? = null, + val diffSupported: Boolean = false, + val diffLoading: Boolean = false, + val diffError: String? = null, + val docxHtml: String? = null, + val pdfPath: String? = null, + /** Bumped on a live image change so Coil refetches the same URL. */ + val imageVersion: Int = 0, +) { + val id: String get() = comparableFilePath(path) + .takeIf { it.isNotEmpty() } + ?: path + + val availableModes: List + get() = availableViewerModes(viewerKind(path), language, diffSupported) +} + +data class UploadFailureUi(val name: String, val message: String) + +/** + * The failure row's text for one file. A 413 is the server's size verdict, but + * its body is not guaranteed (a proxy or Next itself can answer before the + * upload route does), and "HTTP 413" would hide the actual cause. The row + * already names the file, so the message names the limits. + */ +internal fun uploadFailureMessage(error: Throwable): String { + val status = when (error) { + is ApiError.ServerResponse -> error.status + is ApiError.Http -> error.code + else -> null + } + return if (status == 413) { + "Too large for the server (25 MB per file, 100 MB per upload)." + } else { + error.userMessage() + } +} + +data class UploadSummaryUi( + val uploaded: List = emptyList(), + val skipped: List = emptyList(), + val failed: List = emptyList(), +) + +enum class UploadPhase { Idle, Checking, Uploading, Conflict } + +/** The explorer's upload banner/dialog state (web `FileExplorer` upload block). */ +data class FileUploadUiState( + val phase: UploadPhase = UploadPhase.Idle, + val percent: Int = 0, + val currentName: String? = null, + val index: Int = 0, + val total: Int = 0, + val conflict: UploadConflictPlan? = null, + val summary: UploadSummaryUi? = null, + val error: String? = null, +) { + val busy: Boolean get() = phase == UploadPhase.Checking || phase == UploadPhase.Uploading +} + +/** The Explorer tab's tree, git decorations, search and upload state. */ +data class ExplorerUiState( + val cwd: String? = null, + val children: Map> = emptyMap(), + val expanded: Set = emptySet(), + val loading: Set = emptySet(), + val error: String? = null, + val rootLoaded: Boolean = false, + val gitFiles: List = emptyList(), + val searchOpen: Boolean = false, + val searchQuery: String = "", + val searchResults: List = emptyList(), + val searchTruncated: Boolean = false, + val searchLoading: Boolean = false, + val searchFailed: Boolean = false, + /** Uploaded files (absolute) to highlight after an upload. */ + val highlight: Set = emptySet(), + /** A reveal request still waiting for its row to scroll into view. */ + val revealTarget: String? = null, + val upload: FileUploadUiState = FileUploadUiState(), +) + +data class FilesUiState( + val panelOpen: Boolean = false, + val view: FilesPanelView = FilesPanelView.Explorer, + val tabs: List = emptyList(), + val activeTabId: String? = null, + val explorer: ExplorerUiState = ExplorerUiState(), +) { + val activeTab: FileTab? get() = tabs.firstOrNull { it.id == activeTabId } +} + +/** + * The right panel's files: explorer tree, file tabs and their viewers, search, + * uploads and the live `file` channel. + * + * Process-scoped (like `NotificationHistoryViewModel` and Slice B's view + * models) so tabs survive rotation with the shell; [FileTabStore] additionally + * restores the open paths after a process death. `explorerCwd` is pushed in by + * the shell (`open session cwd → sidebar cwd → new-session cwd`). + */ +class FilesViewModel( + private val api: FilesApi, + private val git: GitApi, + private val events: SessionEventSource, + private val scope: CoroutineScope, + private val tabStore: FileTabStore, + private val cacheDir: File, + private val origin: () -> String? = { null }, + /** + * The origin, once the settings have loaded: tabs restored before that + * would be judged against "no installation" and restored under the wrong + * key. + */ + private val awaitOrigin: suspend () -> String? = { origin() }, +) { + + private val _state = MutableStateFlow(FilesUiState()) + val state: StateFlow = _state.asStateFlow() + + /** Bumped by every cwd change; late directory/git answers compare against it. */ + private var cwdGeneration = 0L + private var gitGeneration = 0L + private var searchGeneration = 0L + /** Per-tab patch tokens: two tabs' diffs must not cancel each other. */ + private val diffTokens = mutableMapOf() + private var uploadJob: Job? = null + private var uploadGeneration = 0L + + private val loadingDirectories = mutableSetOf() + private val loadTokens = mutableMapOf() + private val loadJobs = mutableMapOf() + private var watchJob: Job? = null + private var searchJob: Job? = null + private var pendingUpload: List = emptyList() + + init { + scope.launch { + val stored = runCatching { tabStore.load(awaitOrigin()) }.getOrDefault(emptyList()) + if (stored.isEmpty() || _state.value.tabs.isNotEmpty()) return@launch + val restored = stored.map { FileTab(path = it.path, sourceSessionId = it.sourceSessionId) } + _state.update { it.copy(tabs = restored, activeTabId = restored.firstOrNull()?.id) } + } + } + + // --- Panel & tabs ------------------------------------------------------- + + fun showPanel() { + if (_state.value.panelOpen) return + _state.update { it.copy(panelOpen = true) } + val active = _state.value.activeTab + if (_state.value.view == FilesPanelView.File && active != null) { + ensureTabLoaded(active) + startWatch(active) + } + } + + fun closePanel() { + stopWatch() + _state.update { it.copy(panelOpen = false) } + } + + /** Predictive/back: a file tab first returns to the explorer, then the panel closes. */ + fun back(): Boolean { + val current = _state.value + if (!current.panelOpen) return false + if (current.view == FilesPanelView.File) { + _state.update { it.copy(view = FilesPanelView.Explorer) } + stopWatch() + } else { + closePanel() + } + return true + } + + fun setView(view: FilesPanelView) { + val next = if (view == FilesPanelView.File && _state.value.activeTab == null) { + FilesPanelView.Explorer + } else { + view + } + _state.update { it.copy(view = next, panelOpen = true) } + if (next == FilesPanelView.File) { + _state.value.activeTab?.let { tab -> + ensureTabLoaded(tab) + startWatch(tab) + } + } else { + stopWatch() + } + } + + /** + * Opens (or reactivates) a file tab. [mode] overrides the type's default; + * a path outside [cwd] is still opened (the server decides), so a host + * tool's absolute path works after switching sessions. + */ + fun openTab( + path: String, + mode: FileOpenMode? = null, + line: Int? = null, + sourceSessionId: String? = null, + cwd: String? = _state.value.explorer.cwd, + ) { + val raw = path.trim() + if (raw.isEmpty()) return + val resolved = normalizeFilePath(raw).let { normalized -> + when { + normalized.isEmpty() -> return + isAbsoluteFilePath(normalized) -> normalized + cwd != null -> joinFilePath(cwd, normalized) + else -> normalized + } + } + val id = FileTab(path = resolved).id + val existing = _state.value.tabs.firstOrNull { it.id == id } + if (existing == null) { + val tab = FileTab( + path = resolved, + sourceSessionId = sourceSessionId, + line = line, + requestedMode = mode, + mode = mode ?: FileOpenMode.Source, + ) + _state.update { + it.copy( + tabs = it.tabs + tab, + activeTabId = tab.id, + view = FilesPanelView.File, + panelOpen = true, + ) + } + ensureTabLoaded(tab) + // An image needs no read, so nothing else subscribes to its watch + // channel and a changed file would never refresh. + if (viewerKind(tab.path) == ViewerKind.Image) startWatch(tab) + persistTabs() + return + } + val updated = existing.copy( + sourceSessionId = sourceSessionId ?: existing.sourceSessionId, + line = line ?: existing.line, + requestedMode = mode ?: existing.requestedMode, + mode = mode ?: existing.mode, + ) + _state.update { state -> + state.copy( + tabs = state.tabs.map { if (it.id == id) updated else it }, + activeTabId = id, + view = FilesPanelView.File, + panelOpen = true, + ) + } + ensureTabLoaded(updated) + startWatch(updated) + persistTabs() + } + + fun selectTab(id: String) { + val tab = _state.value.tabs.firstOrNull { it.id == id } ?: return + _state.update { it.copy(activeTabId = id, view = FilesPanelView.File) } + ensureTabLoaded(tab) + startWatch(tab) + } + + fun closeTab(id: String) { + val current = _state.value + val next = current.tabs.filterNot { it.id == id } + cancelLoad(id) + val active = when { + next.isEmpty() -> null + current.activeTabId == id -> next.last().id + else -> current.activeTabId + } + _state.update { + it.copy( + tabs = next, + activeTabId = active, + view = if (next.isEmpty() && it.view == FilesPanelView.File) { + FilesPanelView.Explorer + } else { + it.view + }, + ) + } + stopWatch() + next.firstOrNull { it.id == active }?.let { tab -> + if (_state.value.panelOpen && _state.value.view == FilesPanelView.File) { + ensureTabLoaded(tab) + startWatch(tab) + } + } + persistTabs() + } + + fun closeOtherTabs() { + val active = _state.value.activeTab ?: return + _state.value.tabs.filterNot { it.id == active.id }.forEach { cancelLoad(it.id) } + _state.update { it.copy(tabs = listOf(active), activeTabId = active.id) } + persistTabs() + } + + fun closeAllTabs() { + _state.value.tabs.forEach { cancelLoad(it.id) } + stopWatch() + _state.update { + it.copy( + tabs = emptyList(), + activeTabId = null, + view = if (it.view == FilesPanelView.File) FilesPanelView.Explorer else it.view, + ) + } + persistTabs() + } + + /** Leaving Diff asks for the bytes a diff-only tab never loaded. */ + fun setMode(mode: FileOpenMode) { + val tab = _state.value.activeTab ?: return + if (tab.availableModes.isNotEmpty() && mode !in tab.availableModes) return + updateTab(tab.id) { it.copy(mode = mode, requestedMode = mode) } + ensureTabLoaded(tab.copy(mode = mode, requestedMode = mode)) + } + + fun toggleWrap() { + val tab = _state.value.activeTab ?: return + updateTab(tab.id) { it.copy(wrap = !it.wrap) } + } + + fun retryTab() { + val tab = _state.value.activeTab ?: return + startLoad(tab.copy(error = null, content = null, binaryMime = null, docxHtml = null)) + } + + /** Raw bytes for Save/Share; throws the API error so the caller can toast it. */ + suspend fun fileBytes(path: String, sourceSessionId: String? = null): ByteArray = + api.download(FileLinks.apiPathSegments(path), sourceSessionId) + + /** A picker-side rejection (unreadable or oversized URI) for the upload banner. */ + fun reportUploadError(message: String) { + setUploadError(message) + } + + // --- Explorer ----------------------------------------------------------- + + fun setCwd(cwd: String?) { + val normalized = cwd?.takeIf { it.isNotBlank() } + val current = _state.value.explorer + if (normalized == current.cwd && (normalized == null || current.rootLoaded)) return + cwdGeneration++ + gitGeneration++ + searchGeneration++ + cancelUpload() + loadingDirectories.clear() + val keepSearch = current.searchOpen + val keepQuery = current.searchQuery + _state.update { + it.copy( + explorer = ExplorerUiState( + cwd = normalized, + searchOpen = keepSearch, + searchQuery = keepQuery, + ), + ) + } + // A diff belongs to the directory it was asked for; a moved cwd must + // refetch instead of showing the previous workspace's patch. + diffTokens.clear() + _state.update { state -> + state.copy( + tabs = state.tabs.map { it.copy(diff = null, diffSupported = false, diffLoading = false, diffError = null) }, + ) + } + if (normalized == null) return + loadDirectory(normalized, force = true) + loadGitStatus() + val active = _state.value.activeTab + if (active != null && active.mode == FileOpenMode.Diff) ensureDiff(active, force = true) + if (keepSearch && keepQuery.isNotBlank()) scheduleSearch(immediate = true) + } + + fun refreshExplorer() { + val cwd = _state.value.explorer.cwd ?: return + // A refresh invalidates every listing in flight; those jobs return on + // the generation check and would leave their row spinner forever. + loadingDirectories.clear() + _state.update { it.copy(explorer = it.explorer.copy(loading = emptySet())) } + cwdGeneration++ + gitGeneration++ + searchGeneration++ + val expanded = _state.value.explorer.expanded + loadDirectory(cwd, force = true) + expanded.forEach { loadDirectory(it, force = true) } + loadGitStatus() + val active = _state.value.activeTab + if (active != null && active.mode == FileOpenMode.Diff) ensureDiff(active, force = true) + if (_state.value.explorer.searchOpen && _state.value.explorer.searchQuery.isNotBlank()) { + scheduleSearch(immediate = true, refresh = true) + } + } + + fun toggleDirectory(path: String) { + val key = comparableFilePath(path) + val expanded = _state.value.explorer.expanded + val nowExpanded = if (key in expanded) expanded - key else expanded + key + _state.update { it.copy(explorer = it.explorer.copy(expanded = nowExpanded)) } + if (key in nowExpanded) ensureDirectoryLoaded(path) + } + + fun collapseAll() { + _state.update { it.copy(explorer = it.explorer.copy(expanded = emptySet())) } + } + + /** Reveals [path] in the tree: expands and loads every ancestor, then scrolls the row in. */ + fun reveal(path: String) { + val cwd = _state.value.explorer.cwd + val target = normalizeFilePath(path) + if (cwd == null || !isInsideDirectory(target, cwd)) { + _state.update { it.copy(view = FilesPanelView.Explorer, panelOpen = true) } + return + } + val ancestors = revealAncestors(cwd, target) + _state.update { + it.copy( + panelOpen = true, + view = FilesPanelView.Explorer, + explorer = it.explorer.copy( + expanded = it.explorer.expanded + ancestors.map(::comparableFilePath), + highlight = setOf(comparableFilePath(target)), + revealTarget = null, + ), + ) + } + scope.launch { + ancestors.forEach { ancestor -> loadDirectory(ancestor) } + _state.update { + it.copy(explorer = it.explorer.copy(revealTarget = comparableFilePath(target))) + } + delay(REVEAL_TIMEOUT_MILLIS) + _state.update { state -> + if (state.explorer.revealTarget == comparableFilePath(target)) { + state.copy(explorer = state.explorer.copy(revealTarget = null)) + } else { + state + } + } + } + } + + /** The explorer scrolled to [path] (or gave up); drop the one-shot scroll target. */ + fun acknowledgeReveal() { + if (_state.value.explorer.revealTarget == null) return + _state.update { it.copy(explorer = it.explorer.copy(revealTarget = null)) } + } + + // --- Search ------------------------------------------------------------- + + fun setSearchOpen(open: Boolean) { + _state.update { it.copy(explorer = it.explorer.copy(searchOpen = open)) } + if (!open) { + searchJob?.cancel() + searchGeneration++ + _state.update { + it.copy( + explorer = it.explorer.copy( + searchQuery = "", + searchResults = emptyList(), + searchTruncated = false, + searchLoading = false, + searchFailed = false, + ), + ) + } + } else if (_state.value.explorer.searchQuery.isNotBlank()) { + scheduleSearch(immediate = true) + } + } + + fun setSearchQuery(query: String) { + _state.update { it.copy(explorer = it.explorer.copy(searchQuery = query)) } + if (query.isBlank()) { + searchJob?.cancel() + searchGeneration++ + _state.update { + it.copy( + explorer = it.explorer.copy( + searchResults = emptyList(), + searchTruncated = false, + searchLoading = false, + searchFailed = false, + ), + ) + } + return + } + scheduleSearch() + } + + fun retrySearch() { + scheduleSearch(immediate = true) + } + + private fun scheduleSearch(immediate: Boolean = false, refresh: Boolean = false) { + val explorer = _state.value.explorer + val cwd = explorer.cwd ?: return + val query = explorer.searchQuery.trim() + if (query.isEmpty()) return + searchJob?.cancel() + val generation = ++searchGeneration + _state.update { it.copy(explorer = it.explorer.copy(searchLoading = true, searchFailed = false)) } + searchJob = scope.launch { + if (!immediate) delay(SEARCH_DEBOUNCE_MILLIS) + val result = captureApi { + api.index(cwd, query, limit = MAX_FILE_SEARCH_LIMIT, filesOnly = true, refresh = refresh) + } + if (generation != searchGeneration) return@launch + result.fold( + onSuccess = { page: FileIndexPage -> + _state.update { + it.copy( + explorer = it.explorer.copy( + searchLoading = false, + searchFailed = false, + searchResults = fileSearchRows(page.matches.map { it.path }), + searchTruncated = page.truncated, + ), + ) + } + }, + onFailure = { + _state.update { + it.copy( + explorer = it.explorer.copy( + searchLoading = false, + searchFailed = true, + searchResults = emptyList(), + searchTruncated = false, + ), + ) + } + }, + ) + } + } + + // --- Upload ------------------------------------------------------------- + + /** + * Pre-flights [files] and uploads them, or asks the conflict question + * first. [pickError] is the picker's own rejection for the same action (an + * unreadable or oversized URI): it rides along in the banner, so the + * upload progress cannot swallow it. + */ + fun upload(files: List, pickError: String? = null) { + if (_state.value.explorer.upload.busy) return + val cwd = _state.value.explorer.cwd ?: return + if (files.isEmpty()) { + if (pickError != null) setUploadError(pickError) + return + } + val tooLarge = files.firstOrNull { it.bytes.size > MAX_UPLOAD_BYTES_PER_FILE } + if (tooLarge != null) { + setUploadError("${tooLarge.name} is larger than 25 MB.", pickError) + return + } + if (files.sumOf { it.bytes.size.toLong() } > MAX_UPLOAD_BYTES_TOTAL) { + setUploadError("The selection is larger than 100 MB.", pickError) + return + } + pendingUpload = files + val generation = startUploadSession( + FileUploadUiState(phase = UploadPhase.Checking, total = files.size, error = pickError), + ) + uploadJob = scope.launch { + val check = captureApi { api.uploadCheck(cwd, files.map { file -> file.name }) } + if (!isCurrentUpload(cwd, generation)) return@launch + check.fold( + onSuccess = { result -> + val plan = uploadConflictPlan(result.conflicts, result.nonReplaceable) + if (plan != null) { + updateUploadState(cwd, generation) { explorer -> + explorer.copy( + upload = explorer.upload.copy( + phase = UploadPhase.Conflict, + conflict = plan, + total = files.size, + ), + ) + } + } else { + performUpload(cwd, generation, files, UploadConflictStrategy.Error) + } + }, + onFailure = { error -> + pendingUpload = emptyList() + updateUploadState(cwd, generation) { explorer -> + explorer.copy( + upload = explorer.upload.copy( + phase = UploadPhase.Idle, + error = uploadErrorText(explorer.upload.error, error.userMessage()), + ), + ) + } + }, + ) + } + } + + fun resolveUploadConflict(strategy: UploadConflictStrategy) { + val files = pendingUpload + val cwd = _state.value.explorer.cwd ?: return + if (files.isEmpty()) return + val generation = startUploadSession( + _state.value.explorer.upload.copy( + phase = UploadPhase.Uploading, + conflict = null, + total = files.size, + index = 0, + currentName = files.firstOrNull()?.name, + percent = 0, + ), + ) + uploadJob = scope.launch { performUpload(cwd, generation, files, strategy) } + } + + fun cancelUploadConflict() { + pendingUpload = emptyList() + _state.update { + it.copy(explorer = it.explorer.copy(upload = FileUploadUiState())) + } + } + + fun dismissUploadFeedback() { + _state.update { + it.copy(explorer = it.explorer.copy(upload = FileUploadUiState())) + } + } + + /** The `@mention` text for the files the last upload created, if any. */ + fun uploadedMentionText(): String? { + val cwd = _state.value.explorer.cwd ?: return null + val uploaded = _state.value.explorer.upload.summary?.uploaded.orEmpty() + if (uploaded.isEmpty()) return null + return atMentionsText(uploaded.map { name -> relativeFilePath(joinFilePath(cwd, name), cwd) }) + } + + /** + * Starts one upload run: a second selection or a cwd switch cancels the + * previous job, whose stale writes the generation then drops. + */ + private fun startUploadSession(initial: FileUploadUiState): Long { + uploadJob?.cancel() + uploadGeneration++ + _state.update { it.copy(explorer = it.explorer.copy(upload = initial)) } + return uploadGeneration + } + + private suspend fun performUpload( + cwd: String, + generation: Long, + files: List, + strategy: UploadConflictStrategy, + ) { + val uploaded = mutableListOf() + val skipped = mutableListOf() + val failed = mutableListOf() + for ((index, file) in files.withIndex()) { + if (!isCurrentUpload(cwd, generation)) return + updateUploadState(cwd, generation) { explorer -> + explorer.copy(upload = explorer.upload.copy(currentName = file.name, index = index, percent = 0)) + } + val outcome = try { + api.upload(cwd, file, strategy) { percent -> + updateUploadState(cwd, generation) { explorer -> + explorer.copy(upload = explorer.upload.copy(percent = percent)) + } + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + failed += UploadFailureUi(file.name, uploadFailureMessage(error)) + null + } + if (!isCurrentUpload(cwd, generation)) return + if (outcome != null) { + if (outcome.status == 409) { + pendingUpload = files + updateUploadState(cwd, generation) { explorer -> + explorer.copy( + upload = explorer.upload.copy( + phase = UploadPhase.Conflict, + conflict = uploadConflictPlan(outcome.conflicts, outcome.nonReplaceable), + total = files.size, + ), + ) + } + return + } + if (outcome.failed) { + failed += UploadFailureUi(file.name, "HTTP ${outcome.status}") + } else { + uploaded += outcome.uploaded + skipped += outcome.skipped + failed += outcome.errors.map { UploadFailureUi(it.name, it.message) } + } + } + } + pendingUpload = emptyList() + updateUploadState(cwd, generation) { explorer -> + explorer.copy( + upload = explorer.upload.copy( + phase = UploadPhase.Idle, + summary = UploadSummaryUi(uploaded, skipped, failed), + percent = 100, + ), + highlight = uploaded.mapTo(HashSet()) { name -> joinFilePath(cwd, name) }, + ) + } + reloadAfterUpload(cwd, generation, uploaded) + } + + /** Applies [transform] only while this run is still the one on screen. */ + private fun updateUploadState(cwd: String, generation: Long, transform: (ExplorerUiState) -> ExplorerUiState) { + _state.update { state -> + if (generation != uploadGeneration || state.explorer.cwd != cwd) return@update state + state.copy(explorer = transform(state.explorer)) + } + } + + private fun isCurrentUpload(cwd: String, generation: Long): Boolean = + generation == uploadGeneration && _state.value.explorer.cwd == cwd + + private fun reloadAfterUpload(cwd: String, generation: Long, uploaded: List) { + if (!isCurrentUpload(cwd, generation)) return + // Like a manual refresh: every listing already in flight is superseded. + loadingDirectories.clear() + _state.update { it.copy(explorer = it.explorer.copy(loading = emptySet())) } + cwdGeneration++ + gitGeneration++ + loadDirectory(cwd, force = true) + _state.value.explorer.expanded.forEach { loadDirectory(it, force = true) } + loadGitStatus() + if (uploaded.isNotEmpty()) { + searchGeneration++ + if (_state.value.explorer.searchOpen && _state.value.explorer.searchQuery.isNotBlank()) { + scheduleSearch(immediate = true, refresh = true) + } + } + } + + /** Abandons a running or queued upload (workspace switch, logout). */ + private fun cancelUpload() { + uploadJob?.cancel() + uploadJob = null + uploadGeneration++ + pendingUpload = emptyList() + } + + private fun setUploadError(message: String, pickError: String? = null) { + pendingUpload = emptyList() + _state.update { + it.copy( + explorer = it.explorer.copy( + upload = FileUploadUiState(phase = UploadPhase.Idle, error = uploadErrorText(pickError, message)), + ), + ) + } + } + + private fun uploadErrorText(first: String?, second: String?): String = + listOfNotNull(first, second).filter { it.isNotBlank() }.joinToString("\n") + + // --- Directory loading -------------------------------------------------- + + private fun loadDirectory(path: String, force: Boolean = false) { + val cwd = _state.value.explorer.cwd + val key = comparableFilePath(path) + if (!force && key in _state.value.explorer.children) return + if (key in loadingDirectories) return + loadingDirectories += key + _state.update { it.copy(explorer = it.explorer.copy(loading = it.explorer.loading + key)) } + val generation = cwdGeneration + scope.launch { + val result = captureApi { api.listDirectory(path) } + loadingDirectories -= key + if (generation != cwdGeneration || _state.value.explorer.cwd != cwd) return@launch + result.fold( + onSuccess = { entries -> + _state.update { + it.copy( + explorer = it.explorer.copy( + children = mergeDirectoryListing(it.explorer.children, path, entries), + loading = it.explorer.loading - key, + error = if (key == comparableFilePath(cwd.orEmpty())) null else it.explorer.error, + rootLoaded = it.explorer.rootLoaded || + key == comparableFilePath(cwd.orEmpty()), + ), + ) + } + }, + onFailure = { error -> + // A failed root must not count as loaded: the pane keys its + // "could not load" state (with the retry) on exactly that. + _state.update { + it.copy( + explorer = it.explorer.copy( + loading = it.explorer.loading - key, + error = if (key == comparableFilePath(cwd.orEmpty())) error.userMessage() else it.explorer.error, + ), + ) + } + }, + ) + } + } + + private fun ensureDirectoryLoaded(path: String) { + if (comparableFilePath(path) !in _state.value.explorer.children) loadDirectory(path) + } + + private fun loadGitStatus() { + val cwd = _state.value.explorer.cwd ?: return + val generation = ++gitGeneration + scope.launch { + val result = captureApi { git.status(cwd) } + if (generation != gitGeneration) return@launch + result.fold( + onSuccess = { response -> + _state.update { + it.copy( + explorer = it.explorer.copy( + gitFiles = if (response.isGitRepository) response.files else emptyList(), + ), + ) + } + }, + onFailure = { + // A decoration failure must never hide the tree. + _state.update { + it.copy(explorer = it.explorer.copy(gitFiles = emptyList())) + } + }, + ) + } + } + + // --- File loading ------------------------------------------------------- + + /** + * Fetches whatever [tab] still lacks. The tab is re-read from the state: + * callers hold a snapshot that a concurrent answer may already have + * superseded. In Diff mode the patch is the content, so no bytes are read + * and a read error cannot keep the diff from loading. + */ + private fun ensureTabLoaded(tab: FileTab) { + val current = _state.value.tabs.firstOrNull { it.id == tab.id } ?: tab + val kind = viewerKind(current.path) + if (current.mode != FileOpenMode.Diff && !current.loading && current.error == null) { + val needsLoad = when (kind) { + ViewerKind.Image -> false + ViewerKind.Pdf -> current.pdfPath == null + ViewerKind.Docx -> current.docxHtml == null + ViewerKind.Text -> current.content == null && current.binaryMime == null + } + if (needsLoad) startLoad(current) + } + val wantsDiff = kind == ViewerKind.Text && (current.mode == FileOpenMode.Diff || hasGitChange(current.path)) + if (wantsDiff && current.diff == null && !current.diffLoading) ensureDiff(current) + } + + private fun hasGitChange(path: String): Boolean { + val key = comparableFilePath(path) + return _state.value.explorer.gitFiles.any { comparableFilePath(it.filePath) == key } + } + + private fun startLoad(tab: FileTab) { + cancelLoad(tab.id) + val token = (loadTokens[tab.id] ?: 0L) + 1 + loadTokens[tab.id] = token + updateTab(tab.id) { + it.copy(loading = true, error = null, diffError = null, docxHtml = null, pdfPath = null) + } + val job = scope.launch { + when (viewerKind(tab.path)) { + ViewerKind.Image -> updateTab(tab.id) { it.copy(loading = false) } + ViewerKind.Pdf -> loadPdf(tab, token) + ViewerKind.Docx -> loadDocx(tab, token) + ViewerKind.Text -> loadText(tab, token) + } + } + loadJobs[tab.id] = job + if (_state.value.panelOpen) startWatch(tab) + } + + private suspend fun loadText(tab: FileTab, token: Long) { + val segments = FileLinks.apiPathSegments(tab.path) + if (segments.isEmpty()) { + failTab(tab.id, token, "Unable to open ${tab.path}.") + return + } + val result = captureApi { api.read(segments, tab.sourceSessionId) } + if (!isCurrent(tab.id, token)) return + result.fold( + onSuccess = { read -> + when (read) { + is FileReadResult.Text -> updateTab(tab.id) { current -> + val requested = current.requestedMode + val language = read.language + val nextMode = when (requested) { + null -> defaultViewerMode(current.path, language) + // The patch is fetched separately; its `supported` + // flag is not known yet, so a requested diff must + // survive this read (ensureDiff falls back if the + // file turns out to have no diff). + FileOpenMode.Diff -> FileOpenMode.Diff + else -> effectiveViewerMode( + requested, + availableViewerModes(ViewerKind.Text, language, current.diffSupported), + ) + } + current.copy( + loading = false, + content = read.content, + language = language, + size = read.size, + error = null, + binaryMime = null, + mode = nextMode, + ) + } + + is FileReadResult.Binary -> updateTab(tab.id) { current -> + current.copy( + loading = false, + content = null, + language = null, + size = read.bytes.size.toLong(), + binaryMime = read.mimeType, + mode = FileOpenMode.Source, + ) + } + } + }, + onFailure = { error -> failTab(tab.id, token, error.userMessage()) }, + ) + } + + private suspend fun loadPdf(tab: FileTab, token: Long) { + val segments = FileLinks.apiPathSegments(tab.path) + val meta = captureApi { api.meta(segments, tab.sourceSessionId) } + if (!isCurrent(tab.id, token)) return + val size = meta.getOrNull()?.size ?: 0 + updateTab(tab.id) { it.copy(size = size) } + val bytes = captureApi { api.download(segments, tab.sourceSessionId) } + if (!isCurrent(tab.id, token)) return + bytes.fold( + onSuccess = { data -> + val file = pdfCacheFile(tab.path) + runCatching { file.writeBytes(data) }.fold( + onSuccess = { + updateTab(tab.id) { + it.copy(loading = false, pdfPath = file.absolutePath, size = data.size.toLong()) + } + }, + onFailure = { failTab(tab.id, token, "Unable to cache the PDF.") }, + ) + }, + onFailure = { error -> failTab(tab.id, token, error.userMessage()) }, + ) + } + + private suspend fun loadDocx(tab: FileTab, token: Long) { + val segments = FileLinks.apiPathSegments(tab.path) + val result = captureApi { api.previewHtml(segments, tab.sourceSessionId) } + if (!isCurrent(tab.id, token)) return + result.fold( + onSuccess = { html -> + updateTab(tab.id) { it.copy(loading = false, docxHtml = html, content = null) } + }, + onFailure = { error -> failTab(tab.id, token, error.userMessage()) }, + ) + } + + private fun failTab(id: String, token: Long, message: String) { + if (!isCurrent(id, token)) return + updateTab(id) { it.copy(loading = false, error = message) } + } + + private fun isCurrent(id: String, token: Long): Boolean = + loadTokens[id] == token && _state.value.tabs.any { it.id == id } + + private fun cancelLoad(id: String) { + loadJobs.remove(id)?.cancel() + loadTokens[id] = (loadTokens[id] ?: 0L) + 1 + } + + private fun updateTab(id: String, transform: (FileTab) -> FileTab) { + _state.update { state -> + val tab = state.tabs.firstOrNull { it.id == id } ?: return@update state + state.copy(tabs = state.tabs.map { if (it.id == id) transform(tab) else it }) + } + } + + private fun pdfCacheFile(path: String): File { + val directory = File(cacheDir, "files").apply { mkdirs() } + val digest = MessageDigest.getInstance("SHA-1") + .digest(path.toByteArray(Charsets.UTF_8)) + .joinToString("") { "%02x".format(it) } + return File(directory, "$digest.pdf") + } + + // --- Diff --------------------------------------------------------------- + + /** Retries the patch of the active tab (the viewer's diff error state). */ + fun retryDiff() { + val tab = _state.value.activeTab ?: return + ensureDiff(tab, force = true) + } + + private fun ensureDiff(tab: FileTab, force: Boolean = false) { + val cwd = _state.value.explorer.cwd + if (cwd == null) { + updateTab(tab.id) { it.copy(diffLoading = false, diffSupported = false, diffError = "No workspace") } + return + } + if (!force && tab.diff != null) return + // Per tab: a slow patch of another tab must not discard this one. + val token = (diffTokens[tab.id] ?: 0L) + 1 + diffTokens[tab.id] = token + updateTab(tab.id) { it.copy(diffLoading = true, diffError = null) } + scope.launch { + val result = captureApi { git.diff(cwd, tab.path) } + if (diffTokens[tab.id] != token || _state.value.tabs.none { it.id == tab.id }) return@launch + result.fold( + onSuccess = { diff -> + updateTab(tab.id) { current -> + val supported = diff.supported && !diff.patch.isNullOrBlank() + current.copy( + diffLoading = false, + diff = if (supported) diff.patch else null, + diffSupported = supported, + diffError = null, + mode = effectiveViewerMode( + if (current.mode == FileOpenMode.Diff && !supported) FileOpenMode.Source else current.mode, + availableViewerModes(ViewerKind.Text, current.language, supported), + ), + ) + } + }, + onFailure = { error -> + // Stay in Diff: the body shows the error with a retry, so a + // flaky fetch does not silently flip the user to Source. + updateTab(tab.id) { current -> + current.copy( + diffLoading = false, + diff = null, + diffSupported = false, + diffError = error.userMessage(), + ) + } + }, + ) + } + } + + // --- Live file watch ---------------------------------------------------- + + private fun startWatch(tab: FileTab) { + watchJob?.cancel() + watchJob = scope.launch { + try { + events.subscribe(EventChannel.File(tab.path, tab.sourceSessionId)).collect { frame -> + when (frame.frameType()) { + "connected" -> updateTab(tab.id) { it.copy(watching = true) } + "change" -> { + updateTab(tab.id) { it.copy(watching = true) } + refreshFromWatch(tab.id) + } + "error" -> updateTab(tab.id) { it.copy(watching = false) } + } + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: EventSubscriptionException) { + updateTab(tab.id) { it.copy(watching = false) } + } catch (_: Exception) { + updateTab(tab.id) { it.copy(watching = false) } + } + } + } + + private fun stopWatch() { + watchJob?.cancel() + watchJob = null + _state.update { state -> + state.copy(tabs = state.tabs.map { it.copy(watching = false) }) + } + } + + private fun refreshFromWatch(id: String) { + val tab = _state.value.tabs.firstOrNull { it.id == id } ?: return + when (viewerKind(tab.path)) { + ViewerKind.Image -> updateTab(id) { it.copy(imageVersion = it.imageVersion + 1) } + ViewerKind.Pdf, ViewerKind.Docx, ViewerKind.Text -> { + // Keep the mode; a changed markdown file must not jump from + // Preview back to Source. A diff tab reloads only its patch. + if (tab.mode != FileOpenMode.Diff) startLoad(tab.copy(requestedMode = tab.mode, loading = false)) + } + } + if (tab.mode == FileOpenMode.Diff || tab.diff != null) ensureDiff(tab, force = true) + } + + // --- Lifecycle ---------------------------------------------------------- + + /** Logout / server switch: every path belonged to the previous installation. */ + fun reset() { + watchJob?.cancel() + searchJob?.cancel() + cancelUpload() + loadJobs.values.forEach { it.cancel() } + loadJobs.clear() + loadingDirectories.clear() + pendingUpload = emptyList() + cwdGeneration++ + gitGeneration++ + searchGeneration++ + diffTokens.clear() + _state.value = FilesUiState() + File(cacheDir, "files").deleteRecursively() + scope.launch { tabStore.clear() } + } + + private fun persistTabs() { + val stored = _state.value.tabs.map { StoredFileTab(path = it.path, sourceSessionId = it.sourceSessionId) } + val token = origin() + scope.launch { runCatching { tabStore.save(token, stored) } } + } + + private companion object { + const val SEARCH_DEBOUNCE_MILLIS = 150L + const val REVEAL_TIMEOUT_MILLIS = 1500L + } +} + +private fun JsonObject.frameType(): String? = + (this["type"] as? kotlinx.serialization.json.JsonPrimitive)?.contentOrNull diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/HtmlPreview.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/HtmlPreview.kt new file mode 100644 index 000000000..2f826646b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/files/HtmlPreview.kt @@ -0,0 +1,39 @@ +package io.github.kahme247.ompweb.ui.files + +import android.graphics.Color +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient + +internal const val HTML_PREVIEW_MIME_TYPE = "text/html" +internal const val HTML_PREVIEW_ENCODING = "utf-8" + +/** + * The HTML preview's sandbox (web `components/HtmlPreview.tsx` renders in a + * plain iframe). The document is user content: no script, no file or content + * access and no network at all — the docx/markdown HTML embeds its images as + * data URIs, and every navigation away is refused. + */ +internal fun configureHtmlPreview(webView: WebView) { + webView.settings.javaScriptEnabled = false + webView.settings.allowFileAccess = false + webView.settings.allowContentAccess = false + webView.settings.blockNetworkLoads = true + webView.settings.loadsImagesAutomatically = true + webView.setBackgroundColor(Color.TRANSPARENT) + webView.isVerticalScrollBarEnabled = true + webView.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest, + ): Boolean = true + } +} + +/** + * Loads [html] with no base URL: relative URLs cannot resolve and no origin + * cookies apply. The caller decides when a document actually changed. + */ +internal fun loadHtmlPreview(webView: WebView, html: String) { + webView.loadDataWithBaseURL(null, html, HTML_PREVIEW_MIME_TYPE, HTML_PREVIEW_ENCODING, null) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/git/GitChangesPane.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/git/GitChangesPane.kt new file mode 100644 index 000000000..971385f53 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/git/GitChangesPane.kt @@ -0,0 +1,450 @@ +package io.github.kahme247.ompweb.ui.git + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.GitCollapseReason +import io.github.kahme247.ompweb.domain.GitFileStatus +import io.github.kahme247.ompweb.domain.GitFileStatusKind +import io.github.kahme247.ompweb.domain.fileDirectory +import io.github.kahme247.ompweb.domain.fileName +import io.github.kahme247.ompweb.domain.gitChangeGroups +import io.github.kahme247.ompweb.domain.relativeFilePath +import io.github.kahme247.ompweb.ui.files.FileOpenMode +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily + +/** + * The Git tab (web `GitChangesPanel`): filter/clear, refresh, one row per + * changed file with its status label and one-letter badge, and the + * `.gitattributes` group. A tap opens Slice A's file viewer in Diff mode — + * this pane owns no diff renderer. + * + * Non-repo workspaces and a missing cwd render their own empty/unsupported + * state, so the shell may show the tab unconditionally. + */ +@Composable +fun GitChangesPane( + cwd: String?, + onOpenFile: (path: String, mode: FileOpenMode) -> Unit, + viewModel: GitChangesViewModel, + modifier: Modifier = Modifier, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + LaunchedEffect(cwd) { viewModel.activate(cwd) } + + Column(modifier.fillMaxSize()) { + GitFilterBar( + filter = state.filter, + onFilterChange = viewModel::setFilter, + onClear = viewModel::clearFilter, + onRefresh = viewModel::refresh, + ) + + val groups = remember(state.files, state.filter, state.cwd) { + gitChangeGroups(state.files, state.filter, state.cwd) + } + + val loadError = state.error + when { + state.cwd == null -> GitEmptyState( + title = stringResource(R.string.git_no_workspace), + hint = stringResource(R.string.git_no_workspace_hint), + ) + loadError != null -> GitErrorState( + message = stringResource(R.string.git_load_failed, loadError), + onRetry = viewModel::refresh, + ) + state.loading && !state.loaded -> Text( + text = stringResource(R.string.git_loading), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + color = LocalOmpColors.current.textDim, + fontSize = 11.sp, + ) + !state.isRepo -> GitEmptyState( + title = stringResource(R.string.git_not_a_repo), + hint = stringResource(R.string.git_not_a_repo_hint), + ) + state.files.isEmpty() -> GitEmptyState( + title = stringResource(R.string.git_no_changes), + hint = stringResource(R.string.git_no_changes_hint), + tint = LocalOmpColors.current.statusSuccess, + icon = Icons.Filled.Check, + ) + else -> Column(Modifier.fillMaxSize()) { + Text( + text = pluralStringResource( + R.plurals.git_files_changed, + state.files.size, + state.files.size, + ), + modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 4.dp), + color = LocalOmpColors.current.textDim, + fontSize = 10.sp, + ) + LazyColumn(Modifier.fillMaxSize()) { + if (groups.primary.isEmpty() && groups.collapsed.isEmpty()) { + item { GitNoMatches() } + } + items(groups.primary, key = { it.filePath }) { file -> + GitChangeRow( + file = file, + cwd = state.cwd, + reasonLabel = null, + selected = file.filePath == state.selectedPath, + onOpen = { + viewModel.select(file.filePath) + onOpenFile(file.filePath, FileOpenMode.Diff) + }, + ) + } + if (groups.collapsed.isNotEmpty()) { + item(key = "git-attributes") { + GitAttributeHeader( + count = groups.collapsed.size, + open = state.attributeGroupOpen, + onToggle = viewModel::toggleAttributeGroup, + ) + } + if (state.attributeGroupOpen) { + items(groups.collapsed, key = { "collapsed:${it.filePath}" }) { file -> + GitChangeRow( + file = file, + cwd = state.cwd, + reasonLabel = gitReasonLabel(file.collapseReason), + selected = file.filePath == state.selectedPath, + onOpen = { + viewModel.select(file.filePath) + onOpenFile(file.filePath, FileOpenMode.Diff) + }, + ) + } + } + } + } + } + } + } +} + +@Composable +private fun GitFilterBar( + filter: String, + onFilterChange: (String) -> Unit, + onClear: () -> Unit, + onRefresh: () -> Unit, +) { + val colors = LocalOmpColors.current + val focusManager = LocalFocusManager.current + Row( + modifier = Modifier.fillMaxWidth().padding(start = 8.dp, end = 8.dp, top = 6.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(13.dp), + ) + Box( + modifier = Modifier + .weight(1f) + .heightIn(min = 28.dp) + .clip(RoundedCornerShape(6.dp)) + .background(colors.background) + .border(1.dp, colors.border, RoundedCornerShape(6.dp)) + .padding(horizontal = 8.dp), + contentAlignment = Alignment.CenterStart, + ) { + if (filter.isEmpty()) { + Text( + text = stringResource(R.string.git_filter_files), + color = colors.textDim, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + BasicTextField( + value = filter, + onValueChange = onFilterChange, + singleLine = true, + textStyle = LocalTextStyle.current.copy(color = colors.text, fontSize = 12.sp), + cursorBrush = SolidColor(colors.accent), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }), + modifier = Modifier.fillMaxWidth(), + ) + } + if (filter.isNotEmpty()) { + IconButton(onClick = onClear) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.git_clear_filter), + tint = colors.textDim, + modifier = Modifier.size(14.dp), + ) + } + } + IconButton(onClick = onRefresh) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(R.string.git_refresh_changes), + tint = colors.textDim, + modifier = Modifier.size(15.dp), + ) + } + } +} + +@Composable +private fun GitChangeRow( + file: GitFileStatus, + cwd: String?, + reasonLabel: String?, + selected: Boolean, + onOpen: () -> Unit, +) { + val colors = LocalOmpColors.current + val relative = relativeFilePath(file.filePath, cwd) + val name = fileName(relative) + val directory = fileDirectory(relative) + val statusLabel = stringResource(gitStatusLabel(file.status)) + Row( + modifier = Modifier + .fillMaxWidth() + .background(if (selected) colors.selected else Color.Transparent) + .clickable( + role = Role.Button, + onClickLabel = stringResource(R.string.git_open_file), + onClick = onOpen, + ) + .padding(start = 8.dp, end = 8.dp, top = 7.dp, bottom = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = name, + modifier = Modifier.weight(1f, fill = false), + color = colors.text, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (directory.isNotEmpty()) { + Text( + text = directory, + modifier = Modifier.weight(1f), + color = colors.textDim, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (reasonLabel != null) { + Text( + text = reasonLabel, + color = colors.textDim, + fontSize = 10.sp, + maxLines = 1, + ) + } + Text( + text = statusLabel, + color = gitStatusColor(file.status), + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + ) + Text( + text = file.code, + color = gitStatusColor(file.status), + fontFamily = OmpMonoFontFamily, + fontSize = 11.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.width(14.dp), + ) + } +} + +@Composable +private fun GitAttributeHeader(count: Int, open: Boolean, onToggle: () -> Unit) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + role = Role.Button, + onClickLabel = stringResource(R.string.git_attribute_files_hint), + onClick = onToggle, + ) + .padding(start = 8.dp, end = 8.dp, top = 6.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(12.dp).rotate(if (open) 0f else -90f), + ) + Text( + text = pluralStringResource(R.plurals.git_attribute_files, count, count), + color = colors.textDim, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun GitNoMatches() { + Text( + text = stringResource(R.string.git_no_matching_files), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + color = LocalOmpColors.current.textDim, + fontSize = 11.sp, + ) +} + +@Composable +private fun GitEmptyState( + title: String, + hint: String, + tint: Color? = null, + icon: androidx.compose.ui.graphics.vector.ImageVector = Icons.Filled.Info, +) { + val colors = LocalOmpColors.current + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = tint ?: colors.textDim, + modifier = Modifier.size(24.dp), + ) + Spacer(Modifier.height(8.dp)) + Text( + text = title, + color = colors.text, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = hint, + color = colors.textDim, + fontSize = 11.sp, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun GitErrorState(message: String, onRetry: () -> Unit) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = message, + modifier = Modifier.weight(1f), + color = colors.statusError, + fontSize = 11.sp, + ) + TextButton(onClick = onRetry) { + Text(stringResource(R.string.git_retry), fontSize = 11.sp, fontWeight = FontWeight.SemiBold) + } + } +} + +/** The web's `GIT_STATUS_LABEL_KEYS`. */ +private fun gitStatusLabel(kind: GitFileStatusKind): Int = when (kind) { + GitFileStatusKind.MODIFIED -> R.string.git_status_modified + GitFileStatusKind.ADDED -> R.string.git_status_added + GitFileStatusKind.DELETED -> R.string.git_status_deleted + GitFileStatusKind.RENAMED -> R.string.git_status_renamed + GitFileStatusKind.UNTRACKED -> R.string.git_status_untracked + GitFileStatusKind.CONFLICT -> R.string.git_status_conflict +} + +/** The web's `GIT_STATUS_COLORS`, mapped onto the app palette. */ +@Composable +private fun gitStatusColor(kind: GitFileStatusKind): Color { + val colors = LocalOmpColors.current + return when (kind) { + GitFileStatusKind.MODIFIED -> colors.statusModified + GitFileStatusKind.ADDED, GitFileStatusKind.UNTRACKED -> colors.statusSuccess + GitFileStatusKind.DELETED, GitFileStatusKind.CONFLICT -> colors.statusError + GitFileStatusKind.RENAMED -> colors.statusRenamed + } +} + +/** The web's `COLLAPSE_REASON_LABEL_KEYS`. */ +@Composable +private fun gitReasonLabel(reason: GitCollapseReason?): String? = when (reason) { + null -> null + GitCollapseReason.GENERATED -> stringResource(R.string.git_reason_generated) + GitCollapseReason.VENDORED -> stringResource(R.string.git_reason_vendored) + GitCollapseReason.DOCUMENTATION -> stringResource(R.string.git_reason_documentation) + GitCollapseReason.NO_DIFF -> stringResource(R.string.git_reason_no_diff) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/git/GitChangesViewModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/git/GitChangesViewModel.kt new file mode 100644 index 000000000..5ac314d13 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/git/GitChangesViewModel.kt @@ -0,0 +1,150 @@ +package io.github.kahme247.ompweb.ui.git + +import io.github.kahme247.ompweb.domain.GitFileStatus +import io.github.kahme247.ompweb.domain.pickGitSelection +import io.github.kahme247.ompweb.net.GitApi +import io.github.kahme247.ompweb.net.captureApi +import io.github.kahme247.ompweb.net.userMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** What the Git tab renders (web `GitChangesPanel` state, minus the inline diff). */ +data class GitChangesState( + /** The directory the list belongs to; the pane opens files in Diff mode against it. */ + val cwd: String? = null, + val files: List = emptyList(), + val isRepo: Boolean = false, + val loading: Boolean = false, + /** A load for [cwd] has finished (successfully or not); before that the pane shows a spinner. */ + val loaded: Boolean = false, + /** Display-ready failure of the last load (web "Failed to load changes"). */ + val error: String? = null, + val filter: String = "", + val selectedPath: String? = null, + /** `.gitattributes` rows are folded behind a row; the web opens the group when only those changed. */ + val attributeGroupOpen: Boolean = false, +) + +/** + * The changed files of one workspace (`GET /api/git/status`), plus the web's + * list rules: filter/clear, refresh, keep-or-pick the previewed file, and the + * `.gitattributes` group. + * + * It owns no diff: selecting a row opens Slice A's viewer in Diff mode, which + * fetches `GET /api/git/diff` through [GitApi.diff]. + * + * [bind] is idempotent per cwd, so the pane may call it from a + * `LaunchedEffect(cwd)` on every recomposition boundary; a late answer for a + * cwd that is no longer bound is dropped. + */ +class GitChangesViewModel( + private val api: GitApi, + private val scope: CoroutineScope, +) { + + private val _state = MutableStateFlow(GitChangesState()) + val state: StateFlow = _state.asStateFlow() + + private var boundCwd: String? = null + private var requestGeneration = 0L + + /** Loads [cwd] once; a repeated call for the same cwd is a no-op. */ + fun bind(cwd: String?) { + if (cwd == boundCwd && _state.value.loaded) return + boundCwd = cwd + _state.value = GitChangesState(cwd = cwd, filter = _state.value.filter) + load(cwd) + } + + /** + * The tab was (re)entered — the panel builds only the visible tab, so this + * is the web's reload-on-mount (`GitChangesPanel`'s `useEffect([cwd, + * refreshKey])`): a new cwd binds, the same cwd reloads instead of showing + * the list from the last visit. + */ + fun activate(cwd: String?) { + if (cwd == boundCwd && _state.value.loaded) { + refresh() + } else { + bind(cwd) + } + } + + /** Reloads the bound cwd, keeping the filter and the previewed file while it still exists. */ + fun refresh() { + if (boundCwd == null) return + load(boundCwd) + } + + fun setFilter(value: String) { + _state.update { it.copy(filter = value) } + } + + fun clearFilter() { + _state.update { it.copy(filter = "") } + } + + fun select(path: String?) { + _state.update { it.copy(selectedPath = path) } + } + + fun toggleAttributeGroup() { + _state.update { it.copy(attributeGroupOpen = !it.attributeGroupOpen) } + } + + private fun load(cwd: String?) { + requestGeneration++ + val generation = requestGeneration + if (cwd.isNullOrBlank()) { + _state.update { it.copy(loading = false, loaded = true, isRepo = false, files = emptyList(), selectedPath = null) } + return + } + _state.update { it.copy(cwd = cwd, loading = true, error = null) } + scope.launch { + val result = captureApi { api.status(cwd) } + if (generation != requestGeneration) return@launch + result.fold( + onSuccess = { response -> + val files = if (response.isGitRepository) response.files else emptyList() + _state.update { current -> + if (current.cwd != cwd) { + current + } else { + current.copy( + loading = false, + loaded = true, + isRepo = response.isGitRepository, + files = files, + selectedPath = pickGitSelection(current.selectedPath, files), + // The web force-opens the group when nothing else is reviewable. + attributeGroupOpen = current.attributeGroupOpen || + (files.isNotEmpty() && files.all { it.collapseReason != null }), + error = null, + ) + } + } + }, + onFailure = { error -> + _state.update { current -> + if (current.cwd != cwd) { + current + } else { + current.copy( + loading = false, + loaded = true, + isRepo = false, + files = emptyList(), + selectedPath = null, + error = error.userMessage(), + ) + } + } + }, + ) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/hosttools/AndroidHostTools.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/hosttools/AndroidHostTools.kt new file mode 100644 index 000000000..ba6bb29e9 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/hosttools/AndroidHostTools.kt @@ -0,0 +1,81 @@ +package io.github.kahme247.ompweb.ui.hosttools + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.widget.Toast +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.net.toUri +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.ClipboardBridge +import io.github.kahme247.ompweb.domain.isSafeOpenUrl +import io.github.kahme247.ompweb.notify.AndroidNotifier +import io.github.kahme247.ompweb.ui.render.MAX_CLIPBOARD_CHARS + +/** + * The device clipboard behind `pi-web://clipboard`. Android 10+ only allows a + * foreground app to read the clipboard, which is exactly the situation here: + * the read happens while the user is looking at the chat. + */ +class AndroidClipboard(private val context: Context) : ClipboardBridge { + + override fun readText(): String? { + val manager = context.getSystemService(ClipboardManager::class.java) ?: return null + val clip = manager.primaryClip ?: return null + if (clip.itemCount == 0) return null + return clip.getItemAt(0).coerceToText(context)?.toString() + } + + override fun writeText(text: String) { + val manager = context.getSystemService(ClipboardManager::class.java) + ?: throw IllegalStateException("Clipboard service unavailable") + val clip = ClipData.newPlainText("ompweb", text.take(MAX_CLIPBOARD_CHARS)) + manager.setPrimaryClip(clip) + } +} + +/** + * Opens an http(s) or mailto URL, the only schemes [isSafeOpenUrl] lets + * through. Links open in a Custom Tab like every other external link in the + * app; `mailto:` goes to the mail app through `ACTION_SENDTO`. + */ +fun openExternalUrl(context: Context, url: String) { + if (!isSafeOpenUrl(url)) return + val uri = url.toUri() + try { + if (uri.scheme.equals("mailto", ignoreCase = true)) { + val intent = Intent(Intent.ACTION_SENDTO, uri) + if (context !is android.app.Activity) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } else { + val customTab = CustomTabsIntent.Builder().setShowTitle(true).build() + if (context !is android.app.Activity) { + customTab.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + customTab.launchUrl(context, uri) + } + } catch (_: Exception) { + Toast.makeText(context, R.string.chat_link_unavailable, Toast.LENGTH_SHORT).show() + } +} + +/** + * Executes `open_url` and `notify` for the host-tool controller. `notify` + * goes through the shared [AndroidNotifier] on the "Agent messages" channel + * and reports whether the system accepted it; otherwise the caller falls back + * to an in-app notice. + */ +class AndroidHostToolOpener(private val context: Context) : HostToolOpener { + + // Host-tool `notify` has no session and no answer action, so it carries no + // origin: its content intent only opens the app. + private val notifier = AndroidNotifier(context.applicationContext, origin = { null }) + + override fun openUrl(url: String) { + openExternalUrl(context, url) + } + + override fun notify(title: String, message: String): Boolean = + notifier.postAgentMessage(title, message) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/hosttools/HostToolPromptHost.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/hosttools/HostToolPromptHost.kt new file mode 100644 index 000000000..560edf3ff --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/hosttools/HostToolPromptHost.kt @@ -0,0 +1,69 @@ +package io.github.kahme247.ompweb.ui.hosttools + +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.HostToolKind +import io.github.kahme247.ompweb.domain.HostToolPrompt + +/** + * The consent dialog for a host-tool call (`open_url` / `open_file`) and the + * Android actions behind it. Web `AppShell` shows one prompt at a time from a + * queue; the controller keeps that queue, this host renders its head. + * + * The call itself is already answered when the prompt appears (web + * `requestOpenUrl` answers "Asked the user to confirm …"), so declining only + * dismisses the prompt — the agent never waits for it. + */ +@Composable +fun HostToolPromptHost( + controller: HostToolsController, + onOpenFile: (sessionId: String, path: String) -> Unit, +) { + val context = LocalContext.current + val latestOpenFile by rememberUpdatedState(onOpenFile) + DisposableEffect(controller, context) { + controller.attach( + opener = AndroidHostToolOpener(context), + onOpenFile = { sessionId, path -> latestOpenFile(sessionId, path) }, + ) + onDispose { controller.detach() } + } + val prompt by controller.pending.collectAsStateWithLifecycle() + val current = prompt ?: return + OmpAlertDialog( + onDismissRequest = controller::deny, + title = { Text(stringResource(titleRes(current))) }, + text = { Text(stringResource(bodyRes(current), current.target)) }, + confirmButton = { + TextButton(onClick = controller::approve) { + Text(stringResource(R.string.host_tool_open)) + } + }, + dismissButton = { + TextButton(onClick = controller::deny) { + Text(stringResource(R.string.host_tool_cancel)) + } + }, + ) +} + +private fun titleRes(prompt: HostToolPrompt): Int = when { + prompt.kind == HostToolKind.OpenFile -> R.string.host_tool_open_file_title + prompt.crossSession -> R.string.host_tool_open_url_cross_session_title + else -> R.string.host_tool_open_url_same_session_title +} + +private fun bodyRes(prompt: HostToolPrompt): Int = when { + prompt.kind == HostToolKind.OpenFile -> R.string.host_tool_open_file_body + prompt.crossSession -> R.string.host_tool_open_url_cross_session_body + else -> R.string.host_tool_open_url_same_session_body +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/hosttools/HostToolsController.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/hosttools/HostToolsController.kt new file mode 100644 index 000000000..e09526fdc --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/hosttools/HostToolsController.kt @@ -0,0 +1,311 @@ +package io.github.kahme247.ompweb.ui.hosttools + +import io.github.kahme247.ompweb.data.ChatSettings +import io.github.kahme247.ompweb.domain.HostToolDecision +import io.github.kahme247.ompweb.domain.HostToolKind +import io.github.kahme247.ompweb.domain.HostToolPrompt +import io.github.kahme247.ompweb.domain.NOTIFICATION_SHOWN_RESULT +import io.github.kahme247.ompweb.domain.NO_PATH_RESULT +import io.github.kahme247.ompweb.domain.UNSAFE_URL_RESULT +import io.github.kahme247.ompweb.domain.askedResult +import io.github.kahme247.ompweb.domain.hostToolResultCommand +import io.github.kahme247.ompweb.domain.isCrossSession +import io.github.kahme247.ompweb.domain.openedResult +import io.github.kahme247.ompweb.domain.openFileDecision +import io.github.kahme247.ompweb.domain.openUrlDecision +import io.github.kahme247.ompweb.domain.setHostToolsCommand +import io.github.kahme247.ompweb.domain.setHostUriSchemesCommand +import io.github.kahme247.ompweb.domain.unknownHostToolResult +import io.github.kahme247.ompweb.events.EventChannel +import io.github.kahme247.ompweb.events.EventMux +import io.github.kahme247.ompweb.net.ExtensionApi +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionStrings +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull + +/** What the shell can do with a consented host-tool action. */ +interface HostToolOpener { + fun openUrl(url: String) + + /** True when a system notification was actually posted. */ + fun notify(title: String, message: String): Boolean +} + +/** + * Executes the host tools this app registers with omp (`open_url`, `notify`, + * `open_file`) and answers every call with `host_tool_result`, so an agent + * turn can never hang on a tool nobody answers. + * + * Calls arrive on two paths: the viewed session's own stream (the chat view + * model forwards `host_tool_call` frames) and the app-wide `host-tools` + * channel for sessions no tab watches (`start`/`stop`). Consent rules mirror + * `AppShell.tsx`: a URL from the viewed session opens directly only when the + * `omp-web:open-url-automatically` preference is on; everything from another + * session asks first. + */ +class HostToolsController( + private val api: ExtensionApi, + private val events: EventMux, + private val settings: ChatSettings, + private val scope: CoroutineScope, + private val strings: ExtensionStrings = ExtensionStrings(), +) { + private val queue = ArrayDeque() + + private val _pending = MutableStateFlow(null) + val pending: StateFlow = _pending.asStateFlow() + + private val _notices = MutableSharedFlow( + extraBufferCapacity = 16, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + /** Snackbar text for `notify` when no system notification could be posted. */ + val notices: SharedFlow = _notices.asSharedFlow() + + private var viewedSessionId: String? = null + private var channelJob: Job? = null + private var opener: HostToolOpener? = null + private var openFile: ((sessionId: String, path: String) -> Unit)? = null + + /** Subscribes the app-wide `host-tools` channel; safe to call twice. */ + fun start() { + if (channelJob?.isActive == true) return + channelJob = scope.launch { + try { + events.subscribe(EventChannel.HostTools).collect { frame -> onChannelFrame(frame) } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // The mux reconnects and resubscribes; a refusal has no user action. + } + } + } + + fun stop() { + channelJob?.cancel() + channelJob = null + } + + /** The session the user is looking at; null while no chat is open. */ + fun setViewedSession(id: String?) { + viewedSessionId = id + } + + /** Drops the viewed session and every unanswered prompt (sign-out / wipe). */ + fun reset() { + viewedSessionId = null + queue.clear() + _pending.value = null + } + + /** + * `set_host_tools` + `set_host_uri_schemes` for [sessionId]. Registration is + * per wrapper and not persisted, so re-send it on every `connected` frame + * and after a prompt started the session (web `registerHostTools`). + */ + fun register(sessionId: String) { + scope.launch { + try { + api.command(sessionId, setHostToolsCommand()) + api.command(sessionId, setHostUriSchemesCommand()) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + // Older omp builds have no host bridge: the UI then stays passive. + } + } + } + + /** Attached by [HostToolPromptHost]; the Android actions live in that file. */ + fun attach(opener: HostToolOpener, onOpenFile: (sessionId: String, path: String) -> Unit) { + this.opener = opener + openFile = onOpenFile + } + + fun detach() { + opener = null + openFile = null + } + + /** + * Consume a frame of the viewed session's channel. Returns true for + * `host_tool_call`/`host_tool_cancel`, false for everything else. + */ + fun onFrame(sessionId: String, frame: JsonObject): Boolean = when (frame.type()) { + "host_tool_call" -> { + handleCall( + sessionId = sessionId, + id = frame.string("id").orEmpty(), + toolName = frame.string("toolName").orEmpty(), + args = frame["arguments"] as? JsonObject, + ) + true + } + + "host_tool_cancel" -> { + dropPrompt(frame.string("targetId").orEmpty()) + true + } + + else -> false + } + + /** Open the pending target (the consent prompt's confirm action). */ + fun approve() { + val prompt = takePrompt() ?: return + when (prompt.kind) { + HostToolKind.OpenUrl -> opener?.openUrl(prompt.target) + HostToolKind.OpenFile -> openFile?.invoke(prompt.sessionId, prompt.target) + } + } + + /** Dismiss the pending target; the call was already answered with "Asked …". */ + fun deny() { + takePrompt() + } + + private fun onChannelFrame(frame: JsonObject) { + when (frame.type()) { + "host_tool_call" -> handleCall( + sessionId = frame.string("sessionId").orEmpty(), + id = frame.string("id").orEmpty(), + toolName = frame.string("toolName").orEmpty(), + args = frame["arguments"] as? JsonObject, + ) + + "host_tool_cancel" -> dropPrompt(frame.string("targetId").orEmpty()) + } + } + + private fun handleCall(sessionId: String, id: String, toolName: String, args: JsonObject?) { + if (sessionId.isEmpty() || id.isEmpty() || toolName.isEmpty()) return + when (toolName) { + "open_url" -> handleOpenUrl(sessionId, id, args) + "notify" -> handleNotify(sessionId, id, args) + "open_file" -> handleOpenFile(sessionId, id, args) + else -> answer(sessionId, id, unknownHostToolResult(toolName), isError = true) + } + } + + private fun handleOpenUrl(sessionId: String, id: String, args: JsonObject?) { + val url = args?.string("url").orEmpty() + val autoOpen = settings.snapshot.value.openUrlAutomatically + var decision = openUrlDecision(url, sessionId, viewedSessionId, autoOpen) + if (decision == HostToolDecision.Execute && opener == null) decision = HostToolDecision.Ask + when (decision) { + HostToolDecision.Error -> + // Web parity: an empty URL is a plain refusal, a bad scheme is an error. + answer(sessionId, id, UNSAFE_URL_RESULT, isError = url.isNotEmpty()) + + HostToolDecision.Ask -> { + val crossSession = isCrossSession(sessionId, viewedSessionId) + // Web parity: the tool call is answered while the prompt is up, + // so an agent turn never blocks on the user's later click. + answer(sessionId, id, askedResult(url, crossSession), isError = false) + enqueue( + HostToolPrompt( + id = id, + sessionId = sessionId, + toolName = "open_url", + kind = HostToolKind.OpenUrl, + target = url, + crossSession = crossSession, + ), + ) + } + + HostToolDecision.Execute -> { + opener?.openUrl(url) + answer(sessionId, id, openedResult(url), isError = false) + } + } + } + + private fun handleNotify(sessionId: String, id: String, args: JsonObject?) { + val title = args?.string("title")?.takeIf { it.isNotBlank() } ?: "OMP" + val message = args?.string("message").orEmpty() + if (opener?.notify(title, message) != true) { + _notices.tryEmit(strings.notificationFallback(title, message)) + } + answer(sessionId, id, NOTIFICATION_SHOWN_RESULT, isError = false) + } + + private fun handleOpenFile(sessionId: String, id: String, args: JsonObject?) { + val path = args?.string("path").orEmpty() + var decision = openFileDecision(path, sessionId, viewedSessionId) + if (decision == HostToolDecision.Execute && openFile == null) decision = HostToolDecision.Ask + when (decision) { + HostToolDecision.Error -> + answer(sessionId, id, NO_PATH_RESULT, isError = true) + + HostToolDecision.Ask -> { + val crossSession = isCrossSession(sessionId, viewedSessionId) + answer(sessionId, id, askedResult(path, crossSession), isError = false) + enqueue( + HostToolPrompt( + id = id, + sessionId = sessionId, + toolName = "open_file", + kind = HostToolKind.OpenFile, + target = path, + crossSession = crossSession, + ), + ) + } + + HostToolDecision.Execute -> { + openFile?.invoke(sessionId, path) + answer(sessionId, id, openedResult(path), isError = false) + } + } + } + + private fun enqueue(prompt: HostToolPrompt) { + if (queue.any { it.id == prompt.id } || _pending.value?.id == prompt.id) return + queue.addLast(prompt) + _pending.value = queue.firstOrNull() + } + + private fun dropPrompt(targetId: String) { + if (targetId.isEmpty()) return + queue.removeAll { it.id == targetId } + if (_pending.value?.id == targetId) _pending.value = queue.firstOrNull() + } + + private fun takePrompt(): HostToolPrompt? { + if (queue.isEmpty()) return null + val prompt = queue.removeFirst() + _pending.value = queue.firstOrNull() + return prompt + } + + private fun answer(sessionId: String, id: String, text: String, isError: Boolean) { + scope.launch { + try { + api.command(sessionId, hostToolResultCommand(id, text, isError)) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + _notices.tryEmit(error.message ?: "Unable to answer the host tool call") + } + } + } + + private fun JsonObject.type(): String? = string("type") + + private fun JsonObject.string(key: String): String? = + (this[key] as? JsonPrimitive)?.contentOrNull +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationBellButton.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationBellButton.kt new file mode 100644 index 000000000..464a292d2 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationBellButton.kt @@ -0,0 +1,65 @@ +package io.github.kahme247.ompweb.ui.notifications + +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Notifications + +/** Web caps the count label at "99+" (`AppShell` mobile badge). */ +private const val BADGE_CAP = 99 + +/** + * Bell + unread badge for the session list (phone) and the top bar (tablet), + * mirroring web `NotificationBell.tsx` / `AppShell.tsx`. + * + * The first composition loads the count once; the pane refreshes on open and + * on resume, and the delivery service calls `onDelivered` for live entries. + */ +@Composable +fun NotificationBellButton( + vm: NotificationHistoryViewModel, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val state by vm.state.collectAsStateWithLifecycle() + val colors = LocalOmpColors.current + val unread = state.unreadCount + val label = if (unread > 0) { + pluralStringResource(R.plurals.notify_ui_bell_unread, unread, unread) + } else { + stringResource(R.string.notify_ui_bell) + } + + LaunchedEffect(vm) { vm.refresh() } + + IconButton(onClick = onClick, modifier = modifier.testTag("notification-bell")) { + BadgedBox( + badge = { + if (unread > 0) { + Badge(containerColor = colors.accent, contentColor = colors.onAccent) { + Text(if (unread > BADGE_CAP) "$BADGE_CAP+" else unread.toString()) + } + } + }, + ) { + Icon( + imageVector = Icons.Filled.Notifications, + contentDescription = label, + tint = colors.textMuted, + ) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationHistoryViewModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationHistoryViewModel.kt new file mode 100644 index 000000000..deece713e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationHistoryViewModel.kt @@ -0,0 +1,230 @@ +package io.github.kahme247.ompweb.ui.notifications + +import io.github.kahme247.ompweb.domain.NotificationHistoryEntry +import io.github.kahme247.ompweb.domain.NotificationHistorySync +import io.github.kahme247.ompweb.domain.SnapshotDecision +import io.github.kahme247.ompweb.domain.markNotificationHistoryRead +import io.github.kahme247.ompweb.domain.mergeDeliveredNotification +import io.github.kahme247.ompweb.domain.notificationHistoryEntryFromEvent +import io.github.kahme247.ompweb.domain.notificationReadIds +import io.github.kahme247.ompweb.domain.removeNotificationHistoryEntry +import io.github.kahme247.ompweb.domain.unreadNotificationCount +import io.github.kahme247.ompweb.net.NotificationHistoryApi +import io.github.kahme247.ompweb.net.captureApi +import io.github.kahme247.ompweb.net.userMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonObject + +/** What the Notifications tab and the bell render. */ +data class NotificationHistoryState( + /** Newest first, exactly the server's order (a delivered event goes to the front). */ + val entries: List = emptyList(), + val loading: Boolean = false, + /** The last load's failure, shown as a retry banner; optimistic changes stay visible. */ + val error: String? = null, +) { + val unreadCount: Int get() = unreadNotificationCount(entries) +} + +/** + * The persisted notification history (web `lib/notification-history.ts`): the + * server owns the list, this view model mirrors it with optimistic changes. + * + * Read/merge/rollback rules: + * - [onDelivered] adds the event straight to the list, no refetch; the server + * has already recorded it, and the next [refresh] adopts its timestamps. + * - [markRead], [markAllRead], [remove] and [clearAll] change the list first and + * send the request afterwards. A failed request is not retried locally: the + * list is reloaded, so the server's state is what stays on screen. + * - A load that raced a local change, or a change request still in flight, is + * not applied; it reloads once those requests settle + * ([NotificationHistorySync]). + * - The pane calls [onListOpened]/[onListClosed]; closing marks exactly the + * entries it showed, so a notification that arrives after the close stays + * unread. + */ +class NotificationHistoryViewModel( + private val api: NotificationHistoryApi, + private val scope: CoroutineScope, +) { + private val _state = MutableStateFlow(NotificationHistoryState()) + val state: StateFlow = _state.asStateFlow() + + private val sync = NotificationHistorySync() + private var loadsInFlight = 0 + + /** + * Bumped by [reset]: an answer of a load or change request that started + * before the origin changed must not land in the new installation's list. + */ + private var generation = 0 + + /** Ids the open list has shown; null while the list is closed. */ + private var shownIds: MutableSet? = null + + /** + * `GET /api/notifications/history`. The pane calls it on open and on every + * resume, which also covers notifications missed while the pane was gone. + * A delivery is merged locally by [onDelivered] instead of refetching, like + * the web store. + */ + fun refresh() { + val token = sync.beginLoad() + loadsInFlight++ + val startedIn = generation + _state.update { it.copy(loading = true) } + scope.launch { + val loaded = captureApi { api.load() } + // The list switched installations while this request ran. + if (startedIn != generation) return@launch + loadsInFlight-- + loaded.fold( + onSuccess = { entries -> + when (sync.decide(token)) { + SnapshotDecision.Apply -> _state.update { + it.copy(entries = entries, loading = loadsInFlight > 0, error = null) + } + + SnapshotDecision.Reload -> { + _state.update { it.copy(loading = loadsInFlight > 0) } + refresh() + } + + SnapshotDecision.ReloadWhenSettled -> { + _state.update { it.copy(loading = loadsInFlight > 0) } + } + + SnapshotDecision.Ignore -> { + _state.update { it.copy(loading = loadsInFlight > 0) } + } + } + recordShown() + }, + onFailure = { error -> + // Keep what is shown; a stale failure must not replace a newer load's answer. + _state.update { + it.copy( + loading = loadsInFlight > 0, + error = if (sync.isLatest(token)) error.userMessage() else it.error, + ) + } + }, + ) + } + } + + /** + * A notification delivered to this installation over the live channel. The + * server recorded it already; this only avoids waiting for the next load. + */ + fun onDelivered(event: JsonObject) { + val entry = notificationHistoryEntryFromEvent(event, System.currentTimeMillis()) ?: return + sync.localChange() + _state.update { it.copy(entries = mergeDeliveredNotification(it.entries, entry)) } + recordShown() + } + + /** Mark exactly [ids] read (optimistic, then `PATCH`). */ + fun markRead(ids: List) { + val unread = _state.value.entries.filter { !it.read }.map { it.id }.toSet() + val target = ids.filter { it in unread }.distinct() + if (target.isEmpty()) return + change( + next = { entries -> markNotificationHistoryRead(entries, target) }, + request = { api.markRead(target) }, + ) + } + + /** + * The toolbar's "Mark all as read": every unread entry the list currently + * shows (the server caps the list at 100, so the PATCH stays within its limit). + */ + fun markAllRead() { + val target = notificationReadIds(_state.value.entries) + if (target.isEmpty()) return + change( + next = { entries -> markNotificationHistoryRead(entries, target) }, + request = { api.markRead(target) }, + ) + } + + /** Swipe/row dismiss: remove first, delete on the server afterwards. */ + fun remove(id: String) { + change( + next = { entries -> removeNotificationHistoryEntry(entries, id) }, + request = { api.remove(id) }, + ) + } + + /** "Clear all": empty the list first, clear the server afterwards. */ + fun clearAll() { + change( + next = { emptyList() }, + request = { api.clear() }, + ) + } + + /** + * The list became visible. It records what it shows until [onListClosed], + * so leaving it marks only those entries read (web `AppShell` layout effect). + */ + fun onListOpened() { + shownIds = mutableSetOf() + recordShown() + } + + /** The list was closed: mark what it showed, leave later arrivals unread. */ + fun onListClosed() { + val seen = shownIds ?: return + shownIds = null + if (seen.isEmpty()) return + val unread = _state.value.entries.filter { !it.read && it.id in seen }.map { it.id } + markRead(unread) + } + + /** Dismiss the retry banner without touching the list. */ + fun dismissError() { + _state.update { it.copy(error = null) } + } + + /** + * The user left this installation (sign-out, server switch, 401): drop the + * whole list — it belongs to the old origin — and make every in-flight + * answer of that origin irrelevant. + */ + fun reset() { + generation++ + loadsInFlight = 0 + shownIds = null + sync.reset() + _state.value = NotificationHistoryState() + } + + private fun change( + next: (List) -> List, + request: suspend () -> Unit, + ) { + val startedIn = generation + sync.localChange() + _state.update { it.copy(entries = next(it.entries)) } + recordShown() + sync.beginRequest() + scope.launch { + val result = captureApi { request() } + if (startedIn != generation) return@launch + if (result.isFailure) sync.requestFailed() + // A failed change request means the shown state is not the server's; reload once idle. + if (sync.endRequest()) refresh() + } + } + + private fun recordShown() { + val shown = shownIds ?: return + shown += _state.value.entries.map { it.id } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationListPane.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationListPane.kt new file mode 100644 index 000000000..9774e179e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationListPane.kt @@ -0,0 +1,375 @@ +package io.github.kahme247.ompweb.ui.notifications + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Warning +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.NOTIFICATION_DETAIL_MAX +import io.github.kahme247.ompweb.domain.NOTIFICATION_TITLE_MAX +import io.github.kahme247.ompweb.domain.NotificationHistoryEntry +import io.github.kahme247.ompweb.domain.NotificationHistoryEvent +import io.github.kahme247.ompweb.domain.NotificationHistoryType +import io.github.kahme247.ompweb.domain.clipNotificationText +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import java.text.DateFormat +import java.util.Date +import kotlin.math.abs + +/** Web dismisses a swipe past 40 px; a touch row uses a slightly larger 48 dp target. */ +private val SWIPE_DISMISS = 48.dp + +/** + * The persisted notification history (web `componentNotificationList.tsx`): + * newest first, unread dot + bold title, read rows dimmed, tap/Enter opens the + * session and marks that row read, a sideways swipe or the X dismisses it. + * + * Shown as the phone's Notifications tab / the tablet's right-panel tab; the + * caller decides the container. Opening the pane refreshes on resume; closing + * it marks exactly the entries it showed (see [NotificationHistoryViewModel]). + */ +@Composable +fun NotificationListPane( + vm: NotificationHistoryViewModel, + onOpenSession: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val state by vm.state.collectAsStateWithLifecycle() + val colors = LocalOmpColors.current + var clearConfirm by remember { mutableStateOf(false) } + + // The list marks read on close, but only the entries it showed. + DisposableEffect(vm) { + vm.onListOpened() + onDispose { vm.onListClosed() } + } + // On open and whenever the pane comes back to the foreground. + LifecycleResumeEffect(vm) { + vm.refresh() + onPauseOrDispose { } + } + + Surface(modifier = modifier.fillMaxSize(), color = colors.panel) { + Column(Modifier.fillMaxSize()) { + Row( + modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.notify_ui_title), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = colors.text, + modifier = Modifier + .weight(1f) + .semantics { heading() }, + ) + IconButton(onClick = vm::markAllRead, enabled = state.unreadCount > 0) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = stringResource(R.string.notify_ui_mark_all_read), + tint = if (state.unreadCount > 0) colors.textMuted else colors.textDim, + ) + } + IconButton(onClick = { clearConfirm = true }, enabled = state.entries.isNotEmpty()) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = stringResource(R.string.notify_ui_clear), + tint = if (state.entries.isNotEmpty()) colors.textMuted else colors.textDim, + ) + } + } + if (state.loading) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = colors.accent) + } else { + Spacer(Modifier.height(2.dp)) + } + state.error?.let { message -> + Row( + modifier = Modifier + .fillMaxWidth() + .semantics { liveRegion = LiveRegionMode.Polite } + .padding(horizontal = 12.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = message, + color = colors.statusError, + fontSize = 12.sp, + modifier = Modifier.weight(1f), + ) + TextButton(onClick = vm::refresh) { + Text(stringResource(R.string.notify_ui_retry), color = colors.accent) + } + IconButton(onClick = vm::dismissError) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.notify_ui_dismiss), + tint = colors.textMuted, + modifier = Modifier.size(18.dp), + ) + } + } + } + if (state.entries.isEmpty()) { + if (!state.loading) EmptyNotifications() + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = rememberLazyListState(), + ) { + items(state.entries, key = { it.id }) { entry -> + NotificationRow( + entry = entry, + onOpen = { + vm.markRead(listOf(entry.id)) + entry.event.sessionId.takeIf { it.isNotBlank() }?.let(onOpenSession) + }, + onDismiss = { vm.remove(entry.id) }, + ) + } + } + } + } + } + + if (clearConfirm) { + OmpAlertDialog( + onDismissRequest = { clearConfirm = false }, + title = { Text(stringResource(R.string.notify_ui_clear_title)) }, + text = { Text(stringResource(R.string.notify_ui_clear_body)) }, + confirmButton = { + TextButton( + onClick = { + clearConfirm = false + vm.clearAll() + }, + ) { + Text(stringResource(R.string.notify_ui_clear_confirm), color = colors.statusError) + } + }, + dismissButton = { + TextButton(onClick = { clearConfirm = false }) { + Text(stringResource(R.string.notify_ui_cancel)) + } + }, + ) + } +} + +@Composable +private fun EmptyNotifications() { + val colors = LocalOmpColors.current + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Filled.Notifications, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(26.dp), + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.notify_ui_empty), + color = colors.text, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + ) + } +} + +@Composable +private fun NotificationRow( + entry: NotificationHistoryEntry, + onOpen: () -> Unit, + onDismiss: () -> Unit, +) { + val colors = LocalOmpColors.current + val density = LocalDensity.current + val threshold = with(density) { SWIPE_DISMISS.toPx() } + var offsetTarget by remember(entry.id) { mutableFloatStateOf(0f) } + var dragging by remember(entry.id) { mutableStateOf(false) } + val offset by animateFloatAsState( + targetValue = offsetTarget, + animationSpec = if (dragging) snap() else spring(stiffness = 900f), + label = "notification-swipe", + ) + val time = remember(entry.at) { + DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(entry.at)) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .graphicsLayer { translationX = offset; alpha = 1f - minOf(abs(offset) / 800f, 0.5f) } + .pointerInput(entry.id) { + detectHorizontalDragGestures( + onDragStart = { + dragging = true + offsetTarget = 0f + }, + onHorizontalDrag = { change, dragAmount -> + change.consume() + offsetTarget += dragAmount + }, + onDragEnd = { + dragging = false + if (abs(offsetTarget) >= threshold) onDismiss() else offsetTarget = 0f + }, + onDragCancel = { + dragging = false + offsetTarget = 0f + }, + ) + } + .background(if (entry.read) colors.panel else colors.backgroundSubtle) + .clickable(role = Role.Button, onClick = onOpen) + .padding(start = 8.dp, end = 2.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.Top, + ) { + // Unread marker in a fixed leading slot, so read and unread rows align. + Box(Modifier.width(14.dp).padding(top = 5.dp), contentAlignment = Alignment.Center) { + if (!entry.read) { + Box(Modifier.size(8.dp).background(colors.accent, CircleShape)) + } + } + Icon( + imageVector = if (entry.event.type == NotificationHistoryType.ERROR) { + Icons.Filled.Warning + } else { + Icons.Filled.Notifications + }, + contentDescription = null, + tint = if (entry.event.type == NotificationHistoryType.ERROR) colors.statusError else colors.textMuted, + modifier = Modifier.padding(top = 1.dp).size(15.dp), + ) + Spacer(Modifier.width(8.dp)) + Column(Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = notificationTitle(entry.event), + color = if (entry.read) colors.textMuted else colors.text, + fontSize = 13.sp, + fontWeight = if (entry.read) FontWeight.Normal else FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Spacer(Modifier.width(8.dp)) + Text(text = time, color = colors.textDim, fontSize = 11.sp) + } + Text( + text = notificationBody(entry.event), + color = if (entry.read) colors.textDim else colors.textMuted, + fontSize = 12.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 2.dp), + ) + } + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.notify_ui_dismiss), + tint = colors.textDim, + modifier = Modifier.size(14.dp), + ) + } + } +} + +@Composable +private fun notificationTitle(event: NotificationHistoryEvent): String { + val name = event.sessionName?.let { clipNotificationText(it, NOTIFICATION_TITLE_MAX) } + return name?.takeIf { it.isNotEmpty() } ?: stringResource(R.string.notify_ui_untitled_session) +} + +@Composable +private fun notificationBody(event: NotificationHistoryEvent): String { + // Same wording as the web's `renderNotification` (server/notification shade + // strings live in Slice A; the list has its own resource file by ownership). + val detail = event.detail?.let { clipNotificationText(it, NOTIFICATION_DETAIL_MAX) }.orEmpty() + return when (event.type) { + NotificationHistoryType.COMPLETED -> stringResource(R.string.notify_ui_body_completed) + NotificationHistoryType.INPUT -> + if (detail.isNotEmpty()) stringResource(R.string.notify_ui_body_input, detail) else stringResource(R.string.notify_ui_body_input_none) + + NotificationHistoryType.ERROR -> + if (detail.isNotEmpty()) stringResource(R.string.notify_ui_body_error, detail) else stringResource(R.string.notify_ui_body_error_none) + + NotificationHistoryType.MODEL_SWITCH -> { + val from = event.from + val to = event.to + when { + from != null && to != null && detail.isNotEmpty() -> + stringResource(R.string.notify_ui_body_model_switch, from, to, detail) + + from != null && to != null -> + stringResource(R.string.notify_ui_body_model_switch_none, from, to) + + else -> detail + } + } + + NotificationHistoryType.TEST -> stringResource(R.string.notify_ui_body_test) + else -> detail.ifEmpty { stringResource(R.string.notify_ui_body_other) } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationNoticeHost.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationNoticeHost.kt new file mode 100644 index 000000000..7e0aa3e33 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationNoticeHost.kt @@ -0,0 +1,163 @@ +package io.github.kahme247.ompweb.ui.notifications + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.InAppNotice +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import kotlinx.coroutines.flow.StateFlow + +/** Test tag of one notice card; the tests address the whole card and its actions. */ +const val NOTIFICATION_NOTICE_TAG = "notification-notice" +const val NOTIFICATION_NOTICE_OPEN_TAG = "notification-notice-open" +const val NOTIFICATION_NOTICE_DISMISS_TAG = "notification-notice-dismiss" + +/** + * The in-app delivery surface (web `useNotifications.showToast`): cards over + * the shell for `whenActive:"toast"` events and for every system post Android + * refused. A card opens its session and marks the history entry read; the X + * and the dismiss frame remove it. + * + * Rendered once by the shell on top of the list and the chat, so a notice + * works on any view — like the web toast, which is not bound to the chat. + */ +@Composable +fun NotificationNoticeHost( + notices: StateFlow>, + history: NotificationHistoryViewModel, + onDismiss: (InAppNotice) -> Unit, + onOpenSession: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val cards by notices.collectAsStateWithLifecycle() + if (cards.isEmpty()) return + Box(modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + cards.forEach { notice -> + NotificationNoticeCard( + notice = notice, + onOpen = { + // Following a notification is reading it (web toast). + history.markRead(listOf(notice.id)) + onDismiss(notice) + if (notice.sessionId.isNotBlank()) onOpenSession(notice.sessionId) + }, + onDismiss = { onDismiss(notice) }, + ) + } + } + } +} + +@Composable +private fun NotificationNoticeCard( + notice: InAppNotice, + onOpen: () -> Unit, + onDismiss: () -> Unit, +) { + val colors = LocalOmpColors.current + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .border( + 1.dp, + if (notice.error) colors.statusError.copy(alpha = 0.7f) else colors.border, + RoundedCornerShape(10.dp), + ) + .testTag(NOTIFICATION_NOTICE_TAG) + .semantics { liveRegion = LiveRegionMode.Polite } + .clickable(role = Role.Button, onClick = onOpen), + color = colors.panel, + shadowElevation = 6.dp, + ) { + Row( + modifier = Modifier.padding(start = 10.dp, end = 2.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = if (notice.error) Icons.Filled.Warning else Icons.Filled.Notifications, + contentDescription = null, + tint = if (notice.error) colors.statusError else colors.accent, + modifier = Modifier.size(16.dp), + ) + Spacer(Modifier.width(8.dp)) + Column(Modifier.weight(1f)) { + Text( + text = notice.title, + color = colors.text, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = notice.body, + color = colors.textMuted, + fontSize = 12.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 1.dp), + ) + TextButton( + onClick = onOpen, + modifier = Modifier.testTag(NOTIFICATION_NOTICE_OPEN_TAG), + ) { + Text(stringResource(R.string.notify_ui_open), color = colors.accent, fontSize = 12.sp) + } + } + IconButton( + onClick = onDismiss, + modifier = Modifier.testTag(NOTIFICATION_NOTICE_DISMISS_TAG), + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.notify_ui_dismiss), + tint = colors.textDim, + modifier = Modifier.size(14.dp), + ) + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationSettings.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationSettings.kt new file mode 100644 index 000000000..cd2188f5d --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/notifications/NotificationSettings.kt @@ -0,0 +1,411 @@ +package io.github.kahme247.ompweb.ui.notifications + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.settings.SettingsCard +import io.github.kahme247.ompweb.ui.settings.SettingsMessage +import io.github.kahme247.ompweb.ui.settings.SettingsRow +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors + +/** + * The settings surface Slice A implements (prefs, permission, service state). + * Every field is driven by A; this screen only renders it and calls the + * actions, which keeps the permission request inside the user's tap. + */ +enum class NotificationTypeToggle { + COMPLETED, + INPUT, + ERROR, + MODEL_SWITCH, +} + +/** Web `prefs.whenActive`: in-app toast vs. system notification while active elsewhere. */ +enum class NotificationWhenActive { TOAST, SYSTEM } + +/** `POST_NOTIFICATIONS` (API 33+) runtime permission state. */ +enum class NotificationPermissionState { NOT_ASKED, GRANTED, DENIED, UNSUPPORTED } + +/** Battery-optimisation exemption; doze without it delays the foreground service. */ +enum class NotificationBatteryState { EXEMPT, NOT_EXEMPT } + +/** The foreground service's connection, shown as a status row. */ +enum class NotificationConnectionState { DISABLED, CONNECTING, CONNECTED, OFFLINE } + +/** One-shot result line (test sent, save failed, server's 502 answer). */ +data class NotificationSettingsMessage( + val text: String, + val error: Boolean = false, +) + +/** Web defaults: enabled off, completed/input/error on, model switch off, when-active toast. */ +val DEFAULT_NOTIFICATION_TYPES: Map = mapOf( + NotificationTypeToggle.COMPLETED to true, + NotificationTypeToggle.INPUT to true, + NotificationTypeToggle.ERROR to true, + NotificationTypeToggle.MODEL_SWITCH to false, +) + +data class NotificationSettingsUiState( + val enabled: Boolean = false, + val types: Map = DEFAULT_NOTIFICATION_TYPES, + val whenActive: NotificationWhenActive = NotificationWhenActive.TOAST, + /** The user's vibration choice: on by default, independent of the type list. */ + val vibrate: Boolean = true, + val permission: NotificationPermissionState = NotificationPermissionState.NOT_ASKED, + val battery: NotificationBatteryState = NotificationBatteryState.EXEMPT, + val connection: NotificationConnectionState = NotificationConnectionState.DISABLED, + /** e.g. "2 agents running" / the service's last failure; null uses the plain state label. */ + val connectionDetail: String? = null, + /** A prefs save or permission request is running; switches stay disabled. */ + val busy: Boolean = false, + val message: NotificationSettingsMessage? = null, +) { + fun typeEnabled(type: NotificationTypeToggle): Boolean = types[type] ?: false +} + +/** + * What the screen can ask for. Slice A owns prefs and permissions: + * `setEnabled(true)` must request `POST_NOTIFICATIONS` inside that tap + * (invariant 14), `sendTest` posts `/api/notifications/test` and falls back to + * a local notification through A's renderer when this install is not subscribed. + */ +data class NotificationSettingsActions( + val setEnabled: (Boolean) -> Unit, + val setType: (NotificationTypeToggle, Boolean) -> Unit, + val setWhenActive: (NotificationWhenActive) -> Unit, + val setVibrate: (Boolean) -> Unit, + val sendTest: () -> Unit, + val requestPermission: () -> Unit, + val openNotificationSettings: () -> Unit, + val requestBatteryExemption: () -> Unit, + val dismissMessage: () -> Unit, +) + +/** + * Notifications settings (web `NotificationSettingsPanel`): enable, per-type + * toggles, when-active choice, test notification, permission/battery fix rows + * and the service's connection state. + * + * Content only — the shell provides the surrounding screen and back affordance. + */ +@Composable +fun NotificationSettingsScreen( + state: NotificationSettingsUiState, + actions: NotificationSettingsActions, + modifier: Modifier = Modifier, + highlight: String? = null, +) { + val colors = LocalOmpColors.current + val controlsEnabled = state.enabled && !state.busy + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text( + text = stringResource(R.string.notify_ui_settings_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = colors.text, + ) + Text( + text = stringResource(R.string.notify_ui_settings_subtitle), + color = colors.textMuted, + fontSize = 12.sp, + ) + + SettingsCard { + SettingsRow( + title = stringResource(R.string.notify_ui_enable), + description = stringResource(R.string.notify_ui_enable_desc), + highlighted = highlight == "notifications-enable", + trailing = { + Switch( + checked = state.enabled, + onCheckedChange = actions.setEnabled, + enabled = !state.busy, + ) + }, + ) + if (state.enabled && state.permission != NotificationPermissionState.GRANTED) { + PermissionNote(state.permission) + } + PermissionRow(state.permission, actions) + } + + SettingsCard { + SettingsRow( + title = stringResource(R.string.notify_ui_connection), + trailing = { + Text( + text = connectionLabel(state), + color = when (state.connection) { + NotificationConnectionState.CONNECTED -> colors.statusSuccess + NotificationConnectionState.OFFLINE -> colors.statusError + NotificationConnectionState.CONNECTING -> colors.statusWarning + NotificationConnectionState.DISABLED -> colors.textDim + }, + fontSize = 12.sp, + ) + }, + ) + Spacer(Modifier.height(6.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + Text( + text = stringResource(R.string.notify_ui_battery), + color = colors.text, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + ) + Text( + text = batteryLabel(state.battery), + color = if (state.battery == NotificationBatteryState.NOT_EXEMPT) colors.statusWarning else colors.textMuted, + fontSize = 11.5.sp, + ) + } + if (state.battery != NotificationBatteryState.EXEMPT) { + Button(onClick = actions.requestBatteryExemption) { + Text(stringResource(R.string.notify_ui_battery_fix)) + } + } + } + } + + Text( + text = stringResource(R.string.notify_ui_types_title), + color = colors.textMuted, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(top = 4.dp), + ) + SettingsCard { + NotificationTypeToggle.entries.forEach { type -> + SettingsRow( + title = typeLabel(type), + description = typeDescription(type), + enabled = controlsEnabled, + highlighted = highlight == typePreferenceId(type), + trailing = { + Switch( + checked = state.typeEnabled(type), + onCheckedChange = { actions.setType(type, it) }, + enabled = controlsEnabled, + ) + }, + ) + } + } + + Text( + text = stringResource(R.string.notify_ui_feedback_title), + color = colors.textMuted, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(top = 4.dp), + ) + SettingsCard { + SettingsRow( + title = stringResource(R.string.notify_ui_vibrate), + description = stringResource(R.string.notify_ui_vibrate_desc), + enabled = controlsEnabled, + highlighted = highlight == "notifications-vibrate", + trailing = { + Switch( + checked = state.vibrate, + onCheckedChange = actions.setVibrate, + enabled = controlsEnabled, + modifier = Modifier.testTag("notifications-vibrate-switch"), + ) + }, + ) + } + + Text( + text = stringResource(R.string.notify_ui_when_active), + color = colors.text, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + ) + SettingsCard(highlighted = highlight == "notifications-when-active") { + WhenActiveOption( + label = stringResource(R.string.notify_ui_when_active_toast), + selected = state.whenActive == NotificationWhenActive.TOAST, + enabled = controlsEnabled, + onSelect = { actions.setWhenActive(NotificationWhenActive.TOAST) }, + ) + WhenActiveOption( + label = stringResource(R.string.notify_ui_when_active_system), + selected = state.whenActive == NotificationWhenActive.SYSTEM, + enabled = controlsEnabled, + onSelect = { actions.setWhenActive(NotificationWhenActive.SYSTEM) }, + ) + } + + Button( + onClick = actions.sendTest, + enabled = state.enabled && !state.busy, + ) { + Text(stringResource(R.string.notify_ui_send_test)) + } + + state.message?.let { message -> + Row(verticalAlignment = Alignment.CenterVertically) { + SettingsMessage( + text = message.text, + error = message.error, + modifier = Modifier.weight(1f), + ) + TextButton(onClick = actions.dismissMessage) { + Text(stringResource(R.string.notify_ui_dismiss_message), color = colors.textMuted) + } + } + } + } +} + +@Composable +private fun PermissionRow( + permission: NotificationPermissionState, + actions: NotificationSettingsActions, +) { + val colors = LocalOmpColors.current + Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + Text( + text = stringResource(R.string.notify_ui_permission), + color = colors.text, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + ) + Text( + text = when (permission) { + NotificationPermissionState.NOT_ASKED -> stringResource(R.string.notify_ui_permission_not_asked) + NotificationPermissionState.GRANTED -> stringResource(R.string.notify_ui_permission_granted) + NotificationPermissionState.DENIED -> stringResource(R.string.notify_ui_permission_denied) + NotificationPermissionState.UNSUPPORTED -> stringResource(R.string.notify_ui_permission_unsupported) + }, + color = if (permission == NotificationPermissionState.DENIED) colors.statusWarning else colors.textMuted, + fontSize = 11.5.sp, + ) + } + when (permission) { + NotificationPermissionState.NOT_ASKED -> + Button(onClick = actions.requestPermission) { + Text(stringResource(R.string.notify_ui_permission_allow)) + } + + NotificationPermissionState.DENIED -> + Button(onClick = actions.openNotificationSettings) { + Text(stringResource(R.string.notify_ui_permission_open_settings)) + } + + NotificationPermissionState.GRANTED, NotificationPermissionState.UNSUPPORTED -> Unit + } + } +} + +/** Web `notifications.statusPermission`: explains why nothing is delivered yet. */ +@Composable +private fun PermissionNote(permission: NotificationPermissionState) { + if (permission == NotificationPermissionState.GRANTED) return + val colors = LocalOmpColors.current + Text( + text = if (permission == NotificationPermissionState.DENIED) { + stringResource(R.string.notify_ui_status_permission_denied) + } else { + stringResource(R.string.notify_ui_status_permission) + }, + color = colors.statusWarning, + fontSize = 11.5.sp, + ) +} + +@Composable +private fun connectionLabel(state: NotificationSettingsUiState): String = when (state.connection) { + NotificationConnectionState.DISABLED -> stringResource(R.string.notify_ui_connection_off) + NotificationConnectionState.CONNECTING -> stringResource(R.string.notify_ui_connection_connecting) + NotificationConnectionState.CONNECTED -> state.connectionDetail?.takeIf { it.isNotBlank() } + ?: stringResource(R.string.notify_ui_connection_connected) + + NotificationConnectionState.OFFLINE -> state.connectionDetail?.takeIf { it.isNotBlank() } + ?: stringResource(R.string.notify_ui_connection_offline) +} + +@Composable +private fun batteryLabel(state: NotificationBatteryState): String = when (state) { + NotificationBatteryState.EXEMPT -> stringResource(R.string.notify_ui_battery_exempt) + NotificationBatteryState.NOT_EXEMPT -> stringResource(R.string.notify_ui_battery_restricted) +} + +@Composable +private fun typeLabel(type: NotificationTypeToggle): String = when (type) { + NotificationTypeToggle.COMPLETED -> stringResource(R.string.notify_ui_type_completed) + NotificationTypeToggle.INPUT -> stringResource(R.string.notify_ui_type_input) + NotificationTypeToggle.ERROR -> stringResource(R.string.notify_ui_type_error) + NotificationTypeToggle.MODEL_SWITCH -> stringResource(R.string.notify_ui_type_model_switch) +} + +/** The settings-search row id the web attaches to each notification type. */ +private fun typePreferenceId(type: NotificationTypeToggle): String = when (type) { + NotificationTypeToggle.COMPLETED -> "notifications-completed" + NotificationTypeToggle.INPUT -> "notifications-input" + NotificationTypeToggle.ERROR -> "notifications-error" + NotificationTypeToggle.MODEL_SWITCH -> "notifications-modelSwitch" +} + +@Composable +private fun typeDescription(type: NotificationTypeToggle): String = when (type) { + NotificationTypeToggle.COMPLETED -> stringResource(R.string.notify_ui_type_completed_desc) + NotificationTypeToggle.INPUT -> stringResource(R.string.notify_ui_type_input_desc) + NotificationTypeToggle.ERROR -> stringResource(R.string.notify_ui_type_error_desc) + NotificationTypeToggle.MODEL_SWITCH -> stringResource(R.string.notify_ui_type_model_switch_desc) +} + +@Composable +private fun WhenActiveOption( + label: String, + selected: Boolean, + enabled: Boolean, + onSelect: () -> Unit, +) { + val colors = LocalOmpColors.current + Row( + modifier = Modifier + .fillMaxWidth() + .selectable(selected = selected, enabled = enabled, role = Role.RadioButton, onClick = onSelect), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton(selected = selected, onClick = null, enabled = enabled) + Spacer(Modifier.width(8.dp)) + Text(text = label, color = if (enabled) colors.text else colors.textDim, fontSize = 12.5.sp) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/onboarding/PasswordScreen.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/onboarding/PasswordScreen.kt new file mode 100644 index 000000000..e3b430542 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/onboarding/PasswordScreen.kt @@ -0,0 +1,192 @@ +package io.github.kahme247.ompweb.ui.onboarding + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.net.ServerAddress +import io.github.kahme247.ompweb.ui.PasswordError +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors + +@Composable +fun PasswordScreen( + origin: ServerAddress?, + error: PasswordError?, + signingIn: Boolean, + onSubmit: (String) -> Unit, + onChangeServer: () -> Unit, +) { + // Never rememberSaveable: that would write the typed password into the + // saved-instance-state bundle the system keeps for a killed process. + var password by remember { mutableStateOf("") } + val focusManager = LocalFocusManager.current + val colors = LocalOmpColors.current + + fun submit() { + if (password.isEmpty() || signingIn) return + focusManager.clearFocus() + onSubmit(password) + } + + Scaffold { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .imePadding(), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column(modifier = Modifier.widthIn(max = 480.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(8.dp), + ) + Spacer(Modifier.width(12.dp)) + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + ) + } + + Spacer(Modifier.height(32.dp)) + Text( + text = stringResource(R.string.password_heading), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.semantics { heading() }, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = origin?.let { stringResource(R.string.password_intro, it.origin) } + ?: stringResource(R.string.password_intro_unknown), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(Modifier.height(24.dp)) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + modifier = Modifier.fillMaxWidth(), + label = { Text(stringResource(R.string.password_label)) }, + singleLine = true, + isError = error != null, + supportingText = error?.let { reason -> + { + Text( + text = stringResource(reason.messageRes()), + modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite }, + ) + } + }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { submit() }), + ) + + Spacer(Modifier.height(16.dp)) + Button( + onClick = { submit() }, + modifier = Modifier.fillMaxWidth(), + enabled = password.isNotEmpty() && !signingIn, + ) { + Text(stringResource(R.string.password_button)) + } + + if (signingIn) { + Spacer(Modifier.height(20.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth(), + ) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + Spacer(Modifier.width(12.dp)) + Text( + text = stringResource(R.string.password_signing_in), + style = MaterialTheme.typography.bodyMedium, + ) + } + } + + Spacer(Modifier.height(8.dp)) + TextButton(onClick = onChangeServer) { + Text( + text = stringResource(R.string.password_change_server), + color = colors.accent, + ) + } + } + } + } + } +} + +private fun PasswordError.messageRes(): Int = when (this) { + PasswordError.WrongPassword -> R.string.password_error_wrong + PasswordError.TooLarge -> R.string.password_error_too_large + PasswordError.Misconfigured -> R.string.password_error_misconfigured + PasswordError.Forbidden -> R.string.password_error_forbidden + PasswordError.Network -> R.string.password_error_network + PasswordError.Server -> R.string.password_error_server +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/onboarding/ServerTooOldScreen.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/onboarding/ServerTooOldScreen.kt new file mode 100644 index 000000000..89c19c090 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/onboarding/ServerTooOldScreen.kt @@ -0,0 +1,122 @@ +package io.github.kahme247.ompweb.ui.onboarding + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.net.ServerAddress +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily + +/** + * The server answered but does not offer `/api/app-info` (or an `api` version + * this app speaks). The screen names the exact update command instead of a + * generic "connection failed", because nothing the user types will fix it. + */ +@Composable +fun ServerTooOldScreen( + address: ServerAddress, + serverVersion: String?, + onRetry: () -> Unit, + onChangeServer: () -> Unit, +) { + val colors = LocalOmpColors.current + Scaffold { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 28.dp), + ) { + Column(modifier = Modifier.widthIn(max = 560.dp)) { + Text( + text = stringResource(R.string.too_old_heading), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + color = colors.statusWarning, + ) + Spacer(Modifier.height(12.dp)) + Text( + text = stringResource(R.string.too_old_intro, address.origin), + style = MaterialTheme.typography.bodyLarge, + ) + if (serverVersion != null) { + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.too_old_version, serverVersion), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Spacer(Modifier.height(24.dp)) + Text( + text = stringResource(R.string.too_old_command_label), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.height(8.dp)) + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(10.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurface, + ) { + Text( + text = stringResource(R.string.too_old_command), + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodyMedium, + fontFamily = OmpMonoFontFamily, + ) + } + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.too_old_restart), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(Modifier.height(28.dp)) + Button(onClick = onRetry, modifier = Modifier.fillMaxWidth()) { + Text(stringResource(R.string.too_old_retry)) + } + Spacer(Modifier.height(8.dp)) + OutlinedButton(onClick = onChangeServer, modifier = Modifier.fillMaxWidth()) { + Text(stringResource(R.string.password_change_server)) + } + Spacer(Modifier.height(24.dp)) + Text( + text = stringResource(R.string.too_old_help), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Start, + modifier = Modifier + .fillMaxWidth() + .background(colors.backgroundSubtle, RoundedCornerShape(10.dp)) + .padding(12.dp), + ) + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/ComposerPanels.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/ComposerPanels.kt new file mode 100644 index 000000000..8aef0064e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/ComposerPanels.kt @@ -0,0 +1,65 @@ +package io.github.kahme247.ompweb.ui.panels + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp +import io.github.kahme247.ompweb.domain.SubagentInfo +import io.github.kahme247.ompweb.domain.TodoPhase + +/** + * The session panels pinned above the chat input (web `ComposerPanels`): + * the active side question first (rendered by Slice B and passed in as + * [leading]), then the live todo plan, then the subagent roster. Each panel is + * independently collapsible and unmounts when its input is empty. + * + * On phones the whole stack is capped and scrolls, so two expanded panels + * cannot push the composer off-screen; each body keeps the web's own + * `min(30vh, 240px)` cap. + */ +@Composable +fun ComposerPanels( + todoPhases: List, + subagents: List, + todoCollapsed: Boolean, + subagentsCollapsed: Boolean, + onTodoCollapsedChange: (Boolean) -> Unit, + onSubagentsCollapsedChange: (Boolean) -> Unit, + onSelectSubagent: (SubagentInfo) -> Unit, + modifier: Modifier = Modifier, + leading: (@Composable () -> Unit)? = null, +) { + val configuration = LocalConfiguration.current + val compact = configuration.screenWidthDp < 600 + val stackModifier = if (compact) { + Modifier + .fillMaxWidth() + .heightIn(max = (configuration.screenHeightDp * 0.45f).dp) + .verticalScroll(rememberScrollState()) + } else { + Modifier.fillMaxWidth() + } + Column( + modifier = modifier.then(stackModifier), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + leading?.invoke() + TodoPanel( + phases = todoPhases, + collapsed = todoCollapsed, + onCollapsedChange = onTodoCollapsedChange, + ) + SubagentsPanel( + subagents = subagents, + collapsed = subagentsCollapsed, + onCollapsedChange = onSubagentsCollapsedChange, + onSelectSubagent = onSelectSubagent, + ) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/PanelChrome.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/PanelChrome.kt new file mode 100644 index 000000000..73801817e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/PanelChrome.kt @@ -0,0 +1,203 @@ +package io.github.kahme247.ompweb.ui.panels + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpCardRadius + +/** The composer-attached panels share one card frame (web `bg-bg-subtle` + border). */ +@Composable +internal fun PanelCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { + val colors = LocalOmpColors.current + Surface( + modifier = modifier.fillMaxWidth(), + color = colors.backgroundSubtle, + contentColor = colors.text, + shape = RoundedCornerShape(OmpCardRadius), + border = BorderStroke(1.dp, colors.border), + ) { + Column(content = content) + } +} + +/** Collapsible header row: title, live summary, chevron (web panel header). */ +@Composable +internal fun PanelHeader( + title: String, + collapsed: Boolean, + onToggle: () -> Unit, + summary: String, + modifier: Modifier = Modifier, + icon: (@Composable () -> Unit)? = null, +) { + val colors = LocalOmpColors.current + val rotation by animateFloatAsState(if (collapsed) -90f else 0f, label = "panel-chevron") + Row( + modifier = modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .semantics { heading() } + .clickable(role = Role.Button, onClick = onToggle) + .padding(start = 12.dp, end = 10.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + icon?.invoke() + Text( + text = title, + color = colors.text, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + maxLines = 1, + ) + Text( + text = summary, + modifier = Modifier.weight(1f), + color = colors.textMuted, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.End, + ) + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource( + if (collapsed) R.string.panels_expand_panel else R.string.panels_collapse_panel, + ), + tint = colors.textDim, + modifier = Modifier.size(16.dp).rotate(rotation), + ) + } +} + +/** The body cap both attached panels use (web `max-height: min(30vh, 240px)`). */ +@Composable +internal fun panelBodyMaxHeight(): Dp { + val screenHeight = LocalConfiguration.current.screenHeightDp.dp + return (screenHeight * 0.30f).coerceAtMost(240.dp) +} + +/** Height-capped, independently scrolling panel body. */ +@Composable +internal fun PanelBody( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Column( + modifier = modifier + .fillMaxWidth() + .heightIn(max = panelBodyMaxHeight()) + .verticalScroll(rememberScrollState()) + .padding(start = 12.dp, end = 12.dp, top = 2.dp, bottom = 10.dp), + content = content, + ) +} + +/** A 1 dp separator line matching the panel borders. */ +@Composable +internal fun PanelDivider() { + val colors = LocalOmpColors.current + Box(Modifier.fillMaxWidth().height(1.dp).background(colors.border)) +} + +/** Small status glyphs; only `material-icons-core` ships, so circles are drawn. */ +internal enum class GlyphKind { PENDING, IN_PROGRESS, COMPLETED, BLOCKED, ABANDONED, RUNNING, FAILED, ABORTED } + +@Composable +internal fun StatusGlyph(kind: GlyphKind, tint: Color? = null) { + val colors = LocalOmpColors.current + val color = tint ?: when (kind) { + GlyphKind.COMPLETED, GlyphKind.IN_PROGRESS, GlyphKind.RUNNING -> colors.accent + GlyphKind.BLOCKED -> colors.textMuted + GlyphKind.FAILED -> colors.statusError + GlyphKind.ABANDONED, GlyphKind.PENDING, GlyphKind.ABORTED -> colors.textDim + } + val background = colors.background + val strokePx = 1.6.dp + Canvas(Modifier.size(14.dp)) { + val stroke = strokePx.toPx() + val radius = size.minDimension / 2f - stroke / 2f + val center = Offset(size.width / 2f, size.height / 2f) + fun line(x1: Float, y1: Float, x2: Float, y2: Float, lineColor: Color = color) { + drawLine(lineColor, Offset(x1, y1), Offset(x2, y2), strokeWidth = stroke) + } + when (kind) { + GlyphKind.COMPLETED -> { + drawCircle(color, radius = radius, center = center) + line(center.x - radius * 0.45f, center.y, center.x - radius * 0.1f, center.y + radius * 0.35f, background) + line(center.x - radius * 0.1f, center.y + radius * 0.35f, center.x + radius * 0.5f, center.y - radius * 0.4f, background) + } + GlyphKind.IN_PROGRESS -> { + drawCircle(color, radius = radius, center = center, style = Stroke(stroke)) + drawCircle(color, radius = radius * 0.42f, center = center) + } + GlyphKind.RUNNING -> drawCircle(color, radius = radius * 0.75f, center = center) + GlyphKind.FAILED -> { + drawCircle(color, radius = radius, center = center) + line(center.x - radius * 0.4f, center.y - radius * 0.4f, center.x + radius * 0.4f, center.y + radius * 0.4f, background) + line(center.x + radius * 0.4f, center.y - radius * 0.4f, center.x - radius * 0.4f, center.y + radius * 0.4f, background) + } + GlyphKind.BLOCKED -> { + drawCircle(color, radius = radius, center = center, style = Stroke(stroke)) + line(center.x, center.y - radius * 0.55f, center.x, center.y + radius * 0.1f) + drawCircle(color, radius = stroke * 0.55f, center = Offset(center.x, center.y + radius * 0.55f)) + } + GlyphKind.ABANDONED -> { + drawCircle(color, radius = radius, center = center, style = Stroke(stroke)) + line(center.x - radius * 0.6f, center.y + radius * 0.6f, center.x + radius * 0.6f, center.y - radius * 0.6f) + } + GlyphKind.ABORTED -> { + drawCircle(color, radius = radius, center = center, style = Stroke(stroke)) + line(center.x - radius * 0.5f, center.y - radius * 0.5f, center.x + radius * 0.5f, center.y + radius * 0.5f) + } + GlyphKind.PENDING -> drawCircle(color, radius = radius, center = center, style = Stroke(stroke)) + } + } +} + +/** A fixed 14 dp slot so glyph rows align with the surrounding text. */ +@Composable +internal fun GlyphSlot(kind: GlyphKind) { + Box(Modifier.size(14.dp), contentAlignment = Alignment.Center) { StatusGlyph(kind) } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/SubagentTranscriptDialog.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/SubagentTranscriptDialog.kt new file mode 100644 index 000000000..e0a6d43dc --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/SubagentTranscriptDialog.kt @@ -0,0 +1,567 @@ +package io.github.kahme247.ompweb.ui.panels + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.ui.theme.OmpDialog +import androidx.compose.ui.window.DialogProperties +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.chat.Speaker +import io.github.kahme247.ompweb.chat.SpeakerRole +import io.github.kahme247.ompweb.domain.SubagentActivityEvent +import io.github.kahme247.ompweb.domain.SubagentActivityKind +import io.github.kahme247.ompweb.domain.SubagentTranscriptMessage +import io.github.kahme247.ompweb.ui.chat.SpeakerHeaderRow +import io.github.kahme247.ompweb.ui.chat.SubagentDialogState +import io.github.kahme247.ompweb.ui.chat.SubagentTranscriptState +import io.github.kahme247.ompweb.ui.render.GithubRepoRef +import io.github.kahme247.ompweb.ui.render.LinkTarget +import io.github.kahme247.ompweb.ui.render.MarkdownText +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +/** + * The subagent transcript dialog (web `SubagentTranscriptDialog`): live header + * telemetry, the recent activity tail while the child still runs, the task and + * the settled completion (JSON-aware), and a paged raw transcript with an + * RPC-first / disk-fallback pager driven by the view model. + */ +@Composable +fun SubagentTranscriptDialog( + state: SubagentDialogState, + githubRepo: GithubRepoRef?, + onLink: (LinkTarget) -> Unit, + onToggleTranscript: () -> Unit, + onLoadMore: () -> Unit, + onDismiss: () -> Unit, +) { + val colors = LocalOmpColors.current + val configuration = LocalConfiguration.current + val maxWidth = (configuration.screenWidthDp * 0.94f).coerceAtMost(920f).dp + val transcriptMax = (configuration.screenHeightDp * 0.50f).dp + OmpDialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 6.dp, vertical = 12.dp), + color = colors.background, + contentColor = colors.text, + shape = RoundedCornerShape(14.dp), + tonalElevation = 4.dp, + ) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 14.dp, vertical = 10.dp), + ) { + val transcriptTitle = stringResource(R.string.panels_transcript_title) + Row( + modifier = Modifier.semantics { contentDescription = transcriptTitle }, + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Column(Modifier.weight(1f)) { + Text( + text = state.agent ?: state.id, + color = colors.accent, + fontSize = 14.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + state.description?.takeIf { it.isNotBlank() }?.let { description -> + Text( + text = description, + color = colors.textMuted, + fontSize = 12.5.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + SubagentModelRow(state.resolvedModel) + Text( + text = state.sessionFile ?: state.id, + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + StateHistoryMeta(state) + state.outcomeError?.takeIf { it.isNotBlank() }?.let { outcome -> + Text( + text = outcome, + color = colors.statusError, + fontSize = 11.sp, + modifier = Modifier.padding(top = 2.dp), + ) + } + } + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.panels_transcript_close), + tint = colors.textMuted, + modifier = Modifier.size(18.dp), + ) + } + } + + state.activity.takeLast(4) + .takeIf { state.live && state.completion == null && it.isNotEmpty() } + ?.let { events -> RecentActivity(events) } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = false) + .verticalScroll(rememberScrollState()) + .padding(top = 8.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + state.error?.let { error -> + Text(text = error, color = colors.statusError, fontSize = 12.sp) + } ?: run { + state.task?.takeIf { it.isNotBlank() }?.let { task -> + LabeledBlock( + label = stringResource(R.string.panels_transcript_task), + container = colors.backgroundSubtle, + ) { + MarkdownText( + markdown = task, + githubRepo = githubRepo, + onLink = onLink, + ) + } + } + LabeledBlock( + label = stringResource(R.string.panels_transcript_result), + container = colors.panel, + trailing = if (state.truncated) { + { + Text( + text = stringResource(R.string.panels_transcript_truncated), + color = colors.textDim, + fontSize = 10.5.sp, + ) + } + } else { + null + }, + ) { + CompletionBody( + completion = state.completion, + githubRepo = githubRepo, + onLink = onLink, + ) + } + if (state.loading) { + Text( + text = stringResource(R.string.panels_transcript_loading), + color = colors.textDim, + fontSize = 11.sp, + ) + } + Text( + text = stringResource( + if (state.transcript.open) { + R.string.panels_transcript_hide + } else { + R.string.panels_transcript_show + }, + ), + color = colors.accent, + fontSize = 12.sp, + modifier = Modifier + .clickable(role = Role.Button, onClick = onToggleTranscript) + .padding(vertical = 2.dp), + ) + if (state.transcript.open) { + TranscriptPanel( + transcript = state.transcript, + maxHeight = transcriptMax, + agentName = state.agent ?: state.id, + onLoadMore = onLoadMore, + ) + } + } + } + } + } + } + } +} + +@Composable +private fun SubagentModelRow(resolvedModel: String?) { + val colors = LocalOmpColors.current + val model = io.github.kahme247.ompweb.domain.fullModel(resolvedModel) ?: return + val effort = io.github.kahme247.ompweb.domain.modelEffort(resolvedModel) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = stringResource(R.string.panels_transcript_model), + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + ) + Text( + text = model, + color = colors.textMuted, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), + ) + if (effort != null) { + Text( + text = stringResource(R.string.panels_transcript_effort), + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + ) + Text( + text = effort, + color = colors.textMuted, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + ) + } + } +} + +@Composable +private fun StateHistoryMeta(state: SubagentDialogState) { + val tokens = io.github.kahme247.ompweb.domain.formatTokens(state.tokens) + val meta = listOfNotNull( + tokens?.let { stringResource(R.string.panels_tokens_unit, it) }, + io.github.kahme247.ompweb.domain.formatCost(state.cost), + io.github.kahme247.ompweb.domain.formatDuration(state.durationMs), + ).joinToString(" · ") + if (meta.isEmpty()) return + val colors = LocalOmpColors.current + Text( + text = meta, + color = colors.textDim, + fontSize = 10.5.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun RecentActivity(events: List) { + val colors = LocalOmpColors.current + Surface( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + color = colors.panel, + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, colors.border), + ) { + Column(Modifier.padding(horizontal = 10.dp, vertical = 6.dp)) { + events.forEach { event -> + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + text = when (event.kind) { + SubagentActivityKind.TOOL -> "·" + SubagentActivityKind.NOTICE -> "!" + SubagentActivityKind.TEXT -> "»" + }, + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + ) + Text( + text = event.label, + color = if (event.kind == SubagentActivityKind.TOOL) colors.accent else colors.textMuted, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} + +@Composable +private fun LabeledBlock( + label: String, + container: androidx.compose.ui.graphics.Color, + trailing: (@Composable () -> Unit)? = null, + content: @Composable () -> Unit, +) { + val colors = LocalOmpColors.current + Surface( + modifier = Modifier.fillMaxWidth(), + color = container, + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, colors.border), + ) { + Column(Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + text = label.uppercase(), + color = colors.textDim, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + letterSpacing = 0.4.sp, + fontFamily = OmpMonoFontFamily, + ) + trailing?.invoke() + } + Box(Modifier.padding(top = 6.dp)) { content() } + } + } +} + +/** The settled `.md` output: a single JSON string unwraps to its text. */ +@Composable +private fun CompletionBody( + completion: String?, + githubRepo: GithubRepoRef?, + onLink: (LinkTarget) -> Unit, +) { + val colors = LocalOmpColors.current + val parsed = rememberParsedCompletion(completion) + val singleText = parsed + ?.takeIf { it.size == 1 } + ?.values + ?.firstOrNull() + ?.let { it as? JsonPrimitive } + ?.takeIf { it.isString } + ?.content + when { + singleText != null -> Text( + text = singleText, + color = colors.textMuted, + fontSize = 12.sp, + lineHeight = 19.sp, + ) + parsed != null -> JsonValue(parsed) + completion != null -> MarkdownText( + markdown = completion, + githubRepo = githubRepo, + onLink = onLink, + ) + else -> Text( + text = stringResource(R.string.panels_transcript_no_completion), + color = colors.textDim, + fontSize = 12.sp, + ) + } +} + +@Composable +private fun rememberParsedCompletion(completion: String?): JsonObject? { + if (completion.isNullOrBlank()) return null + return androidx.compose.runtime.remember(completion) { + runCatching { + kotlinx.serialization.json.Json.parseToJsonElement(completion) as? JsonObject + }.getOrNull() + } +} + +/** Recursive structured-completion renderer (web `JsonValue`). */ +@Composable +private fun JsonValue(value: JsonElement) { + val colors = LocalOmpColors.current + when (value) { + is JsonObject -> Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + value.keys.forEach { key -> + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = key, + color = colors.textDim, + fontSize = 10.5.sp, + fontFamily = OmpMonoFontFamily, + modifier = Modifier.widthIn(max = 130.dp), + ) + Box(Modifier.weight(1f)) { JsonValue(value.getValue(key)) } + } + } + } + is JsonArray -> Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { + value.forEach { item -> + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Text(text = "•", color = colors.textDim, fontSize = 12.sp) + Box(Modifier.weight(1f)) { JsonValue(item) } + } + } + } + is JsonPrimitive -> Text( + text = if (value is JsonNull) "null" else value.content, + color = colors.textMuted, + fontSize = 12.sp, + lineHeight = 19.sp, + ) + else -> Text(text = value.toString(), color = colors.textMuted, fontSize = 12.sp) + } +} + +@Composable +private fun TranscriptPanel( + transcript: SubagentTranscriptState, + maxHeight: androidx.compose.ui.unit.Dp, + agentName: String?, + onLoadMore: () -> Unit, +) { + val colors = LocalOmpColors.current + Surface( + modifier = Modifier.fillMaxWidth(), + color = colors.panel, + shape = RoundedCornerShape(10.dp), + border = BorderStroke(1.dp, colors.border), + ) { + Column(Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) { + if (transcript.error != null && transcript.messages.isNotEmpty()) { + Text( + text = transcript.error, + color = colors.statusError, + fontSize = 12.sp, + modifier = Modifier.padding(bottom = 6.dp), + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = maxHeight) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (transcript.messages.isEmpty() && !transcript.loading) { + Text( + text = transcript.error ?: stringResource(R.string.panels_transcript_no_messages), + color = if (transcript.error != null) colors.statusError else colors.textDim, + fontSize = 12.sp, + ) + } else { + // The role column is always reserved (web + // `SubagentTranscriptDialog` renders its label cell on + // every row), so a tool result no longer shifts the text + // column left. A run also survives non-speaker rows: the + // header repeats only when the speaker really changes. + var previous: Speaker? = null + transcript.messages.forEach { message -> + val speaker = transcriptSpeaker(message.role, agentName) + if (speaker != null && speaker != previous) { + SpeakerHeaderRow( + speaker = speaker, + timestamp = null, + ) + } + if (speaker != null) previous = speaker + TranscriptRow(message = message) + } + } + if (transcript.loading) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + CircularProgressIndicator( + modifier = Modifier.size(12.dp), + strokeWidth = 1.5.dp, + color = colors.accent, + ) + Text( + text = stringResource(R.string.panels_transcript_loading), + color = colors.textDim, + fontSize = 11.sp, + ) + } + } + if (!transcript.exhausted && !transcript.loading) { + Text( + text = stringResource(R.string.panels_transcript_load_more), + color = colors.accent, + fontSize = 12.sp, + modifier = Modifier.clickable(role = Role.Button, onClick = onLoadMore), + ) + } + } + } + } +} + +/** Who speaks in a subagent transcript: the agent's own output is named. */ +private fun transcriptSpeaker(role: String, agentName: String?): Speaker? = when (role) { + "user" -> Speaker(SpeakerRole.You) + "assistant" -> Speaker(SpeakerRole.Subagent, agentName) + else -> null +} + +@Composable +private fun TranscriptRow(message: SubagentTranscriptMessage) { + val colors = LocalOmpColors.current + val (label, labelColor) = when (message.role) { + "user" -> "U" to colors.accent + "assistant" -> "A" to colors.textMuted + else -> "R" to colors.textDim + } + val toolResult = message.role == "toolResult" + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + // The label cell is always laid out, even next to a speaker header, so + // every row starts its text at the same x (web parity). + Text( + text = label, + color = labelColor, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + modifier = Modifier.width(10.dp).padding(top = 2.dp), + ) + Text( + text = message.text, + color = if (toolResult) colors.textMuted else colors.text, + fontSize = if (toolResult || message.role == "assistant") 11.5.sp else 12.5.sp, + lineHeight = 18.sp, + fontFamily = if (toolResult) FontFamily.Monospace else null, + modifier = Modifier.weight(1f), + ) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/SubagentsPanel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/SubagentsPanel.kt new file mode 100644 index 000000000..99712fee0 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/SubagentsPanel.kt @@ -0,0 +1,305 @@ +package io.github.kahme247.ompweb.ui.panels + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.SubagentAgentSource +import io.github.kahme247.ompweb.domain.SubagentInfo +import io.github.kahme247.ompweb.domain.SubagentSource +import io.github.kahme247.ompweb.domain.SubagentStatus +import io.github.kahme247.ompweb.domain.countNestedSubagents +import io.github.kahme247.ompweb.domain.formatCost +import io.github.kahme247.ompweb.domain.formatTokens +import io.github.kahme247.ompweb.domain.shortModel +import io.github.kahme247.ompweb.ui.chat.ElapsedText +import io.github.kahme247.ompweb.ui.chat.LivePulseDot +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpCardRadius +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily + +/** + * The subagent roster attached above the composer (web `SubagentsPanel`): + * running children as live chips, finished ones behind a per-mount + * "Completed (N)" toggle (never persisted — finished runs pile up). + */ +@Composable +fun SubagentsPanel( + subagents: List, + collapsed: Boolean, + onCollapsedChange: (Boolean) -> Unit, + onSelectSubagent: (SubagentInfo) -> Unit, + modifier: Modifier = Modifier, +) { + if (subagents.isEmpty()) return + val colors = LocalOmpColors.current + var completedExpanded by remember { mutableStateOf(false) } + val running = subagents.filter { it.running } + val completed = subagents.filterNot { it.running } + val summary = stringResource(R.string.panels_subagents_summary, running.size, subagents.size) + + PanelCard(modifier = modifier) { + PanelHeader( + title = stringResource(R.string.panels_subagents_title), + collapsed = collapsed, + onToggle = { onCollapsedChange(!collapsed) }, + summary = "${running.size}/${subagents.size}", + modifier = Modifier.semantics { contentDescription = summary }, + ) + if (!collapsed) { + PanelDivider() + PanelBody { + if (running.isNotEmpty()) { + SubagentChips(running, onSelectSubagent) + } + if (completed.isNotEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(role = Role.Button) { completedExpanded = !completedExpanded } + .padding(top = if (running.isNotEmpty()) 6.dp else 0.dp, start = 2.dp, end = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = androidx.compose.material.icons.Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier + .size(12.dp) + .rotate( + androidx.compose.animation.core.animateFloatAsState( + if (completedExpanded) 0f else -90f, + label = "completed-chevron", + ).value, + ), + ) + Text( + text = stringResource(R.string.panels_subagents_completed, completed.size), + color = colors.textMuted, + fontSize = 11.sp, + ) + } + if (completedExpanded) { + SubagentChips(completed, onSelectSubagent) + } + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun SubagentChips( + subagents: List, + onSelectSubagent: (SubagentInfo) -> Unit, +) { + FlowRow( + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + subagents.forEach { subagent -> + SubagentChip(subagent, onClick = { onSelectSubagent(subagent) }) + } + } +} + +@Composable +private fun SubagentChip(subagent: SubagentInfo, onClick: () -> Unit) { + val colors = LocalOmpColors.current + val live = subagent.source != SubagentSource.HISTORY + val running = subagent.running + val stateLabel = stringResource( + when (subagent.status) { + SubagentStatus.STARTED -> R.string.panels_subagent_state_started + SubagentStatus.COMPLETED -> R.string.panels_subagent_state_completed + SubagentStatus.FAILED -> R.string.panels_subagent_state_failed + SubagentStatus.ABORTED -> R.string.panels_subagent_state_aborted + }, + ) + val headline = subagent.headline.orEmpty() + val label = buildString { + append(subagent.agent) + append(" · ") + append(stateLabel) + if (headline.isNotEmpty()) { + append(" · ") + append(headline) + } + } + // The last progress frame reports how long this run already lasted; the + // chip keeps that number moving instead of freezing between frames. + val progressDuration = subagent.progress?.durationMs + val startedAt = remember(subagent.lastUpdate, progressDuration) { + val lastUpdate = subagent.lastUpdate + if (lastUpdate != null && progressDuration != null) lastUpdate - progressDuration else null + } + Surface( + modifier = Modifier + .widthIn(max = 320.dp) + .clickable(role = Role.Button, onClick = onClick) + .semantics { contentDescription = label }, + color = colors.background, + contentColor = if (live && running) colors.text else colors.textDim, + shape = RoundedCornerShape(OmpCardRadius), + border = BorderStroke( + 1.dp, + if (running) colors.accent.copy(alpha = 0.55f) else colors.border, + ), + ) { + Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (running) { + LivePulseDot(size = 9.dp) + } else { + StatusGlyph( + kind = when (subagent.status) { + SubagentStatus.STARTED -> GlyphKind.RUNNING + SubagentStatus.COMPLETED -> GlyphKind.COMPLETED + SubagentStatus.FAILED -> GlyphKind.FAILED + SubagentStatus.ABORTED -> GlyphKind.ABORTED + }, + tint = when (subagent.status) { + SubagentStatus.FAILED -> colors.statusError + else -> colors.textDim + }, + ) + } + Text( + text = subagent.agent, + color = if (running) colors.accent else colors.textMuted, + fontSize = 10.5.sp, + fontFamily = OmpMonoFontFamily, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + ) + Text( + text = headline.ifEmpty { stateLabel }, + modifier = Modifier.weight(1f, fill = false), + color = if (live && running) colors.text else colors.textDim, + fontSize = 11.5.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + ElapsedText( + startedAt = startedAt, + running = running, + settledMs = progressDuration?.takeIf { it > 0 }, + color = if (running) colors.accent else colors.textDim, + fontSize = 10.sp, + ) + if (subagent.detached == true) { + val detachedLabel = stringResource(R.string.panels_subagent_detached) + Text( + text = "⤴", + color = colors.textDim, + fontSize = 10.sp, + fontFamily = OmpMonoFontFamily, + modifier = Modifier.semantics { + contentDescription = detachedLabel + }, + ) + } + } + SubagentActivityLine(subagent) + } + } +} + +/** + * Icon-first telemetry under the chip label (web `SubagentActivityLine`): + * retry state while backing off, work intent while running, and the compact + * token/cost/context/model/duration metrics. + */ +@Composable +private fun SubagentActivityLine(subagent: SubagentInfo) { + val colors = LocalOmpColors.current + val progress = subagent.progress + val retryState = progress?.retryState + val retryFailure = progress?.retryFailure + val retryActive = retryState != null || retryFailure != null + val metrics = ArrayList(7) + if (retryActive) { + val attempt = retryState?.attempt ?: retryFailure?.attempt ?: 0 + val max = retryState?.maxAttempts ?: 0 + metrics += if (max > 0) { + stringResource(R.string.panels_subagent_retrying, attempt, max) + } else { + stringResource(R.string.panels_subagent_retry_attempt, attempt) + } + } else if (subagent.status == SubagentStatus.STARTED) { + val activity = when { + progress?.currentTool != null && progress.lastIntent != null -> + "${progress.currentTool} — ${progress.lastIntent}" + progress?.currentTool != null -> progress.currentTool + else -> progress?.lastIntent + } + if (!activity.isNullOrBlank()) metrics += activity + } + // Web `SubagentActivityLine`: the origin chip is shown unless the agent is + // bundled. Text-only here, so the wire label stands in for its icon. + subagent.agentSource + ?.takeIf { it != SubagentAgentSource.BUNDLED } + ?.let { metrics += it.wireName } + val nested = countNestedSubagents(progress) + if (nested > 0) metrics += stringResource(R.string.panels_subagent_nested, nested) + formatTokens(progress?.tokens)?.let { tokens -> + metrics += stringResource(R.string.panels_tokens_unit, tokens) + } + formatCost(progress?.cost)?.let { metrics += it } + val contextTokens = formatTokens(progress?.contextTokens) + if (contextTokens != null) { + metrics += stringResource( + R.string.panels_context_gauge, + contextTokens, + formatTokens(progress?.contextWindow) ?: "?", + ) + } + shortModel(progress?.resolvedModel)?.let { metrics += it } + if (metrics.isEmpty()) return + Text( + text = metrics.joinToString(" · "), + color = if (retryActive) colors.accent else colors.textDim, + fontSize = 10.5.sp, + fontFamily = OmpMonoFontFamily, + lineHeight = 15.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 20.dp, top = 1.dp), + ) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/TodoPanel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/TodoPanel.kt new file mode 100644 index 000000000..3535f8bf7 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/TodoPanel.kt @@ -0,0 +1,153 @@ +package io.github.kahme247.ompweb.ui.panels + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.List +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.TodoItem +import io.github.kahme247.ompweb.domain.TodoPhase +import io.github.kahme247.ompweb.domain.TodoStatus +import io.github.kahme247.ompweb.domain.todoPreview +import io.github.kahme247.ompweb.domain.todoProgress +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors + +/** + * The live todo plan attached above the composer (web `components/TodoList.tsx`): + * header with `done/total`, a capped preview of four phases and five tasks, and + * a "Show all tasks" footer. Collapse state is owned (and persisted) by the + * caller; the show-all expansion is per-mount, like the web component. + */ +@Composable +fun TodoPanel( + phases: List, + collapsed: Boolean, + onCollapsedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + if (phases.isEmpty()) return + val colors = LocalOmpColors.current + var expanded by remember { mutableStateOf(false) } + val progress = todoProgress(phases) + val preview = todoPreview(phases, expanded) + val planScrollLabel = stringResource(R.string.panels_todo_plan_scroll) + + PanelCard(modifier = modifier) { + PanelHeader( + title = stringResource(R.string.panels_todo_title), + collapsed = collapsed, + onToggle = { onCollapsedChange(!collapsed) }, + summary = stringResource(R.string.panels_todo_progress, progress.done, progress.total), + icon = { + Icon( + imageVector = Icons.Filled.List, + contentDescription = null, + tint = colors.textMuted, + modifier = Modifier.size(15.dp), + ) + }, + ) + if (!collapsed) { + PanelDivider() + PanelBody(modifier = Modifier.semantics { contentDescription = planScrollLabel }) { + preview.phases.forEachIndexed { phaseIndex, phase -> + if (phaseIndex > 0) Spacer(Modifier.height(8.dp)) + Text( + text = phase.name, + color = colors.textMuted, + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + ) + Column(Modifier.padding(top = 4.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) { + phase.tasks.forEach { task -> TodoRow(task) } + } + } + } + if (preview.truncated || expanded) { + PanelDivider() + Text( + text = stringResource( + if (expanded) R.string.panels_todo_show_less else R.string.panels_todo_show_all, + ), + color = colors.accent, + fontSize = 12.sp, + modifier = Modifier + .fillMaxWidth() + .clickable(role = Role.Button) { expanded = !expanded } + .padding(horizontal = 12.dp, vertical = 8.dp), + ) + } + } + } +} + +@Composable +private fun TodoRow(task: TodoItem) { + val colors = LocalOmpColors.current + val statusLabel = stringResource( + when (task.status) { + TodoStatus.PENDING -> R.string.panels_todo_status_pending + TodoStatus.IN_PROGRESS -> R.string.panels_todo_status_in_progress + TodoStatus.COMPLETED -> R.string.panels_todo_status_completed + TodoStatus.BLOCKED -> R.string.panels_todo_status_blocked + TodoStatus.ABANDONED -> R.string.panels_todo_status_abandoned + TodoStatus.UNKNOWN -> R.string.panels_todo_status_pending + }, + ) + val finished = task.status == TodoStatus.COMPLETED || task.status == TodoStatus.ABANDONED + Row( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "$statusLabel: ${task.content}" } + .padding(vertical = 3.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + GlyphSlot( + when (task.status) { + TodoStatus.COMPLETED -> GlyphKind.COMPLETED + TodoStatus.IN_PROGRESS -> GlyphKind.IN_PROGRESS + TodoStatus.BLOCKED -> GlyphKind.BLOCKED + TodoStatus.ABANDONED -> GlyphKind.ABANDONED + TodoStatus.PENDING, TodoStatus.UNKNOWN -> GlyphKind.PENDING + }, + ) + Column(Modifier.weight(1f)) { + Text( + text = task.content, + color = if (finished) colors.textDim else colors.text, + fontSize = 13.sp, + textDecoration = if (finished) TextDecoration.LineThrough else null, + ) + task.blocker?.takeIf { it.isNotBlank() }?.let { blocker -> + Text( + text = stringResource(R.string.panels_todo_blocker, blocker), + color = colors.textMuted, + fontSize = 11.sp, + ) + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/btw/BtwPanel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/btw/BtwPanel.kt new file mode 100644 index 000000000..84f2b979f --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/btw/BtwPanel.kt @@ -0,0 +1,548 @@ +package io.github.kahme247.ompweb.ui.panels.btw + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.ui.theme.OmpDialog +import androidx.compose.ui.window.DialogProperties +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.BtwRecord +import io.github.kahme247.ompweb.domain.BtwStatus +import io.github.kahme247.ompweb.domain.BtwTurn +import io.github.kahme247.ompweb.domain.btwTurns +import io.github.kahme247.ompweb.domain.latestBtwAnswer +import io.github.kahme247.ompweb.domain.latestBtwTurn +import io.github.kahme247.ompweb.ui.render.LinkTarget +import io.github.kahme247.ompweb.ui.render.MarkdownText +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import java.text.DateFormat +import java.util.Date +import kotlinx.coroutines.launch + +/** The toast text for a side-question failure (web `toastBtwError`). */ +@Composable +fun BtwFailure.message(): String = when (kind) { + BtwFailureKind.UNSUPPORTED -> stringResource(R.string.btw_unsupported) + BtwFailureKind.FAILED -> detail?.let { stringResource(R.string.btw_failed_detail, it) } + ?: stringResource(R.string.btw_failed) +} + +/** + * What the composer says about a refused [BtwViewModel.ask]; null = say + * nothing. Transport failures ([BtwRefusal.UNSUPPORTED], [BtwRefusal.FAILED]) + * are already emitted on [BtwViewModel.errors] — the web's `toastBtwError` — + * so the composer only has to keep the draft for them, never repeat the text. + */ +@Composable +fun BtwRefusal.message(): String? = when (this) { + BtwRefusal.NO_SESSION -> stringResource(R.string.btw_no_session) + BtwRefusal.UNSUPPORTED, BtwRefusal.FAILED, BtwRefusal.EMPTY_QUESTION, BtwRefusal.BUSY -> null +} + +/** The streaming topic above the composer (web `components/BtwPanel.tsx`). */ +@Composable +fun BtwPanel( + vm: BtwViewModel, + onCopy: (String) -> Unit, + modifier: Modifier = Modifier, + onLink: (LinkTarget) -> Unit = {}, +) { + val state by vm.state.collectAsState() + val record = state.active ?: return + val colors = LocalOmpColors.current + val latest = latestBtwTurn(record) + val turns = btwTurns(record) + val running = latest.status == BtwStatus.RUNNING + val copyable = latestBtwAnswer(record) + val scope = rememberCoroutineScope() + var draft by remember(record.id) { mutableStateOf("") } + var sending by remember(record.id) { mutableStateOf(false) } + val scroll = rememberScrollState() + // Follow the streaming tail until the user scrolls up (web `followTailRef`). + var followTail by remember(record.id) { mutableStateOf(true) } + LaunchedEffect(scroll.isScrollInProgress) { + if (!scroll.isScrollInProgress) followTail = scroll.maxValue - scroll.value < 24 + } + LaunchedEffect(latest.answer, turns.size, state.expanded) { + if (state.expanded && followTail && !scroll.isScrollInProgress) { + scroll.scrollTo(scroll.maxValue) + } + } + + fun submit() { + val question = draft.trim() + if (question.isEmpty() || running || sending) return + sending = true + followTail = true + scope.launch { + if (vm.ask(question, record.id) == BtwAskResult.Started) draft = "" + sending = false + } + } + + val toggleLabel = stringResource(if (state.expanded) R.string.btw_collapse_panel else R.string.btw_expand_panel) + Surface( + modifier = modifier.fillMaxWidth(), + color = colors.backgroundSubtle, + shape = RoundedCornerShape(12.dp), + border = BorderStroke(1.dp, colors.border), + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth().heightIn(min = 40.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier + .weight(1f) + .clickable(onClickLabel = toggleLabel, onClick = vm::toggle) + .padding(start = 10.dp, end = 4.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(R.string.btw_panel_title), + color = colors.text, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + ) + Text( + text = record.question, + color = colors.textMuted, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + BtwStatusLabel(latest.status) + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = toggleLabel, + tint = colors.textDim, + modifier = Modifier.size(14.dp).rotate(if (state.expanded) 0f else -90f), + ) + } + IconButton(onClick = vm::close) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.btw_close), + tint = colors.textDim, + modifier = Modifier.size(14.dp), + ) + } + } + if (state.expanded) { + // `min(36vh, 320px)` on the web: the panel must never push the composer + // off-screen. The screen height, not the space left in the + // composer column, is the reference. + val turnsMaxHeight = minOf( + 320.dp, + LocalConfiguration.current.screenHeightDp.dp * 0.36f, + ) + Column(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 8.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = turnsMaxHeight) + .verticalScroll(scroll), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + turns.forEachIndexed { index, turn -> + BtwTurnView( + turn = turn, + live = index == turns.lastIndex, + onLink = onLink, + ) + } + } + Row( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (running) { + BtwAction(stringResource(R.string.btw_cancel), onClick = vm::cancel) + } else { + FollowUpField( + value = draft, + onValueChange = { draft = it }, + onSubmit = ::submit, + enabled = !sending, + modifier = Modifier.weight(1f), + ) + BtwAction( + text = stringResource(R.string.btw_ask), + onClick = ::submit, + enabled = !sending && draft.isNotBlank(), + ) + } + if (copyable.isNotEmpty()) { + BtwAction(stringResource(R.string.btw_copy), onClick = { onCopy(copyable) }) + } + } + } + } + } + } +} + +/** `/btw` alone: every side question of the session, newest first (web `BtwHistoryDialog`). */ +@Composable +fun BtwHistoryDialog( + vm: BtwViewModel, + onCopy: (String) -> Unit, + onLink: (LinkTarget) -> Unit = {}, +) { + val state by vm.state.collectAsState() + if (!state.historyOpen) return + val colors = LocalOmpColors.current + var expandedId by remember { mutableStateOf(null) } + OmpDialog( + onDismissRequest = vm::closeHistory, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + BoxWithConstraints( + modifier = Modifier.fillMaxSize().padding(16.dp), + contentAlignment = Alignment.Center, + ) { + val dialogWidth = minOf(maxWidth * 0.92f, 640.dp) + val dialogMaxHeight = maxHeight * 0.85f + Surface( + modifier = Modifier.width(dialogWidth).heightIn(max = dialogMaxHeight), + color = colors.background, + shape = RoundedCornerShape(14.dp), + tonalElevation = 4.dp, + ) { + Column(Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(colors.panel) + .padding(start = 14.dp, end = 4.dp, top = 10.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.btw_history_title), + color = colors.text, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f), + ) + IconButton(onClick = vm::closeHistory) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.common_close), + tint = colors.textMuted, + ) + } + } + if (state.records.isEmpty()) { + Text( + text = stringResource(R.string.btw_history_empty), + color = colors.textMuted, + fontSize = 13.sp, + modifier = Modifier.padding(16.dp), + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxWidth().weight(1f, fill = false), + contentPadding = PaddingValues(10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(state.records, key = { it.id }) { record -> + BtwHistoryRow( + record = record, + expanded = expandedId == record.id, + onToggle = { + expandedId = if (expandedId == record.id) null else record.id + }, + onFollowUp = { vm.openRecord(record.id) }, + onCopy = onCopy, + onLink = onLink, + ) + } + } + } + } + } + } + } +} + +/** One exchange. `live` marks the turn whose answer streams. */ +@Composable +private fun BtwTurnView(turn: BtwTurn, live: Boolean, onLink: (LinkTarget) -> Unit) { + val colors = LocalOmpColors.current + val running = turn.status == BtwStatus.RUNNING + Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = turn.question, + color = colors.textMuted, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + ) + val liveModifier = if (live && running) { + Modifier.semantics { liveRegion = LiveRegionMode.Polite } + } else { + Modifier + } + Box(Modifier.fillMaxWidth().then(liveModifier)) { + when { + turn.answer.isNotEmpty() -> MarkdownText(markdown = turn.answer, onLink = onLink) + running -> Text( + text = stringResource(R.string.btw_waiting), + color = colors.textDim, + fontSize = 12.sp, + ) + } + } + turn.error?.let { error -> + Text(text = error, color = colors.statusError, fontSize = 12.sp) + } + } +} + +@Composable +private fun BtwStatusLabel(status: BtwStatus) { + val colors = LocalOmpColors.current + val tint = if (status == BtwStatus.ERROR) colors.statusError else colors.textMuted + val icon = when (status) { + BtwStatus.RUNNING -> Icons.Filled.Refresh + BtwStatus.COMPLETE -> Icons.Filled.Check + BtwStatus.ERROR -> Icons.Filled.Warning + BtwStatus.CANCELLED -> Icons.Filled.Close + BtwStatus.INTERRUPTED -> Icons.Filled.Info + } + val label = when (status) { + BtwStatus.RUNNING -> stringResource(R.string.btw_status_running) + BtwStatus.COMPLETE -> stringResource(R.string.btw_status_complete) + BtwStatus.CANCELLED -> stringResource(R.string.btw_status_cancelled) + BtwStatus.ERROR -> stringResource(R.string.btw_status_error) + BtwStatus.INTERRUPTED -> stringResource(R.string.btw_status_interrupted) + } + val spin = if (status == BtwStatus.RUNNING) { + val transition = rememberInfiniteTransition(label = "btw-status") + val angle by transition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable(tween(durationMillis = 1200, easing = LinearEasing)), + label = "btw-status-angle", + ) + angle + } else { + 0f + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = tint, + modifier = Modifier.size(12.dp).rotate(spin), + ) + Text(text = label, color = tint, fontSize = 11.sp, maxLines = 1) + } +} + +@Composable +private fun BtwAction(text: String, onClick: () -> Unit, enabled: Boolean = true) { + val colors = LocalOmpColors.current + TextButton( + onClick = onClick, + enabled = enabled, + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp), + colors = ButtonDefaults.textButtonColors( + contentColor = colors.textMuted, + disabledContentColor = colors.textDim, + ), + ) { + Text(text = text, fontSize = 12.sp, maxLines = 1) + } +} + +/** The panel's follow-up input: Enter (IME send) submits, composition stays intact. */ +@Composable +private fun FollowUpField( + value: String, + onValueChange: (String) -> Unit, + onSubmit: () -> Unit, + enabled: Boolean, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + val label = stringResource(R.string.btw_follow_up_label) + val placeholder = stringResource(R.string.btw_follow_up_placeholder) + Box( + modifier = modifier + .heightIn(min = 34.dp) + .border(1.dp, colors.border, RoundedCornerShape(8.dp)) + .padding(horizontal = 8.dp, vertical = 7.dp), + contentAlignment = Alignment.CenterStart, + ) { + if (value.isEmpty()) { + Text( + text = placeholder, + color = colors.textDim, + fontSize = 13.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + BasicTextField( + value = value, + onValueChange = onValueChange, + enabled = enabled, + singleLine = true, + textStyle = TextStyle(color = colors.text, fontSize = 13.sp), + cursorBrush = SolidColor(colors.accent), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), + keyboardActions = KeyboardActions(onSend = { onSubmit() }), + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = label }, + ) + } +} + +@Composable +private fun BtwHistoryRow( + record: BtwRecord, + expanded: Boolean, + onToggle: () -> Unit, + onFollowUp: () -> Unit, + onCopy: (String) -> Unit, + onLink: (LinkTarget) -> Unit, +) { + val colors = LocalOmpColors.current + val latest = latestBtwTurn(record) + val copyable = latestBtwAnswer(record) + val stamp = remember(record.id, latest.updatedAt) { + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + .format(Date(latest.updatedAt)) + } + Surface( + modifier = Modifier.fillMaxWidth(), + color = colors.backgroundSubtle, + shape = RoundedCornerShape(10.dp), + border = BorderStroke(1.dp, colors.border), + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onToggle) + .padding(horizontal = 10.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = colors.textDim, + modifier = Modifier.size(13.dp).rotate(if (expanded) 0f else -90f), + ) + Text( + text = record.question, + color = colors.text, + fontSize = 13.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + BtwStatusLabel(latest.status) + Text(text = stamp, color = colors.textDim, fontSize = 11.sp, maxLines = 1) + } + if (expanded) { + HorizontalDivider(color = colors.border) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + btwTurns(record).forEach { turn -> + BtwTurnView(turn = turn, live = false, onLink = onLink) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (latest.status != BtwStatus.RUNNING) { + BtwAction(stringResource(R.string.btw_follow_up), onClick = onFollowUp) + } + if (copyable.isNotEmpty()) { + BtwAction(stringResource(R.string.btw_copy), onClick = { onCopy(copyable) }) + } + } + } + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/btw/BtwViewModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/btw/BtwViewModel.kt new file mode 100644 index 000000000..3445cc7ec --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/btw/BtwViewModel.kt @@ -0,0 +1,388 @@ +package io.github.kahme247.ompweb.ui.panels.btw + +import io.github.kahme247.ompweb.chat.FlushScheduler +import io.github.kahme247.ompweb.domain.BtwRecord +import io.github.kahme247.ompweb.domain.BtwStatus +import io.github.kahme247.ompweb.domain.applyBtwEvent +import io.github.kahme247.ompweb.domain.btwType +import io.github.kahme247.ompweb.domain.latestBtwTurn +import io.github.kahme247.ompweb.domain.mergeBtwHistory +import io.github.kahme247.ompweb.domain.parseBtwRecord +import io.github.kahme247.ompweb.domain.upsertBtwRecord +import io.github.kahme247.ompweb.net.ApiError +import io.github.kahme247.ompweb.net.BtwApi +import io.github.kahme247.ompweb.net.userMessage +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +/** Side-question state the composer panel and the history dialog render. */ +data class BtwState( + val records: List = emptyList(), + /** The topic in the composer panel; null hides the panel. */ + val activeId: String? = null, + /** Panel body visible (header stays). A new topic always starts expanded. */ + val expanded: Boolean = true, + val historyOpen: Boolean = false, +) { + val active: BtwRecord? get() = records.firstOrNull { it.id == activeId } + val visible: Boolean get() = active != null +} + +/** Why an [BtwViewModel.ask] call did not start a side question. */ +enum class BtwRefusal { + /** No session is bound (the composer had no chat to ask from). */ + NO_SESSION, + + /** The question was blank. */ + EMPTY_QUESTION, + + /** Another side question is still starting; this one was not sent. */ + BUSY, + + /** The omp behind this server is older than `/btw`. */ + UNSUPPORTED, + + /** The request failed; the question was not asked. */ + FAILED, +} + +sealed interface BtwAskResult { + /** omp accepted the question; the answer streams through [BtwViewModel.onFrame]. */ + data object Started : BtwAskResult + + data class Refused(val reason: BtwRefusal) : BtwAskResult +} + +enum class BtwFailureKind { UNSUPPORTED, FAILED } + +/** A failure worth a toast (web `toastBtwError`). */ +data class BtwFailure(val kind: BtwFailureKind, val detail: String? = null) + +/** omp's `btw` failure when a Cancel (or session change) beat its first checkpoint. */ +private const val CANCELLED_BEFORE_START = "cancelled before it started" + +/** Deltas arrive at token rate; apply them at display rate (web `scheduleAtDisplayRate`). */ +private const val DISPLAY_FLUSH_MS = 50L + +/** An omp without the btw commands is not re-asked on every SSE connect (web `UNSUPPORTED_RETRY_MS`). */ +const val BTW_UNSUPPORTED_RETRY_MS: Long = 60_000 + +/** + * The pause after an omp answered "Unknown command" to a btw RPC (web's + * module-global `unsupportedUntil`). Every [BtwViewModel] shares [shared], so + * one old omp is not re-asked by every chat; a test injects its own gate to + * stay independent of execution order. + */ +class BtwSupportGate { + @Volatile + private var unsupportedUntil = 0L + + fun isPaused(nowMs: Long): Boolean = nowMs < unsupportedUntil + + fun pause(nowMs: Long) { + unsupportedUntil = nowMs + BTW_UNSUPPORTED_RETRY_MS + } + + /** The commands answered again: a restarted omp is asked as usual. */ + fun resume() { + unsupportedUntil = 0L + } + + companion object { + val shared = BtwSupportGate() + } +} + +/** The default display-rate scheduler: a 50 ms timer on [scope] (hidden-window safe). */ +fun displayRateScheduler(scope: CoroutineScope): FlushScheduler = { flush -> + val job: Job = scope.launch { + delay(DISPLAY_FLUSH_MS) + flush() + } + val cancel: () -> Unit = { job.cancel() } + cancel +} + +/** + * Side questions (omp `/btw`) for one chat: the session's BTW history fed by + * `btw_record`/`btw_delta` frames, the record shown in the composer panel, and + * the history dialog. Port of `hooks/useBtw.ts` on top of `domain/Btw.kt`; + * never touches the main transcript. + * + * Everything here is main-thread state: [onFrame] is called from the chat's + * frame pump, exactly like the web hook. + */ +class BtwViewModel( + private val api: BtwApi, + private val scope: CoroutineScope, + private val displayScheduler: FlushScheduler = displayRateScheduler(scope), + /** Shared by every chat: one old omp must not be re-asked from each of them. */ + private val support: BtwSupportGate = BtwSupportGate.shared, +) { + private val _state = MutableStateFlow(BtwState()) + val state: StateFlow = _state.asStateFlow() + + /** Toasts only (`toastBtwError`): the "requires a newer omp" hint and hard failures. */ + private val _errors = MutableSharedFlow(extraBufferCapacity = 8) + val errors: SharedFlow = _errors.asSharedFlow() + + private var sessionId: String? = null + + // Frames queued until the display flush; the list is what this client + // already knows, which decides auto-opening and which running records a + // history snapshot may interrupt. + private val pendingFrames = ArrayList() + private var cancelScheduledFlush: (() -> Unit)? = null + + private var historyJob: Job? = null + private var askInFlight = false + + /** The chat now shows [sessionId]; the previous chat's side questions are dropped. */ + fun bind(sessionId: String?) { + if (this.sessionId == sessionId) return + this.sessionId = sessionId + historyJob?.cancel() + historyJob = null + cancelScheduledFlush?.invoke() + cancelScheduledFlush = null + pendingFrames.clear() + _state.value = BtwState() + } + + /** + * One session SSE frame. True when the frame was a side-question frame and + * must not reach the transcript (the chat routes `btw_*` here *before* + * `catchUp.observe`, so it can never reset the live view). + */ + fun onFrame(frame: JsonObject): Boolean { + when (frame.btwType()) { + "btw_record", "btw_delta" -> Unit + else -> return false + } + pendingFrames += frame + if (cancelScheduledFlush == null) cancelScheduledFlush = displayScheduler { flushFrames() } + return true + } + + /** A fresh (re)subscription: re-read omp's history, silently. */ + fun onStreamOpen() { + refreshHistory(explicit = false) + } + + /** Half-open SSE recovery: re-read the history while an answer is still running. */ + fun onReconcile() { + if (_state.value.records.any { latestBtwTurn(it).status == BtwStatus.RUNNING }) { + refreshHistory(explicit = false) + } + } + + /** + * Ask a side question, optionally as a follow-up of [followUpOf]. Returns + * once omp accepted it (the answer streams) or refused it; a failure is + * also pushed to [errors] so the caller only has to keep the draft. + */ + suspend fun ask(question: String, followUpOf: String? = null): BtwAskResult { + val text = question.trim() + if (text.isEmpty()) return BtwAskResult.Refused(BtwRefusal.EMPTY_QUESTION) + val sid = sessionId ?: return BtwAskResult.Refused(BtwRefusal.NO_SESSION) + // A second ask while one is starting is ignored (web `btwAskPendingRef`). + if (askInFlight) return BtwAskResult.Refused(BtwRefusal.BUSY) + askInFlight = true + try { + val reply = try { + api.send( + sid, + buildJsonObject { + put("type", "btw") + put("question", text) + if (followUpOf != null) put("recordId", followUpOf) + }, + ) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Throwable) { + // The user's own Cancel: its `btw_record` already shows it, no message. + if (errorMessage(error).contains(CANCELLED_BEFORE_START, ignoreCase = true)) { + return BtwAskResult.Started + } + report(error) + return BtwAskResult.Refused( + if (isUnknownCommand(error)) BtwRefusal.UNSUPPORTED else BtwRefusal.FAILED, + ) + } + val record = parseBtwRecord((reply as? JsonObject)?.get("record")) + if (sessionId != sid || record == null) return BtwAskResult.Started + // The start state: never newer than frames, so upsert keeps streamed text. + _state.update { + it.copy( + records = upsertBtwRecord(it.records, record), + activeId = record.id, + expanded = true, + ) + } + return BtwAskResult.Started + } finally { + askInFlight = false + } + } + + /** Cancel the topic in the panel. */ + fun cancel() { + val sid = sessionId ?: return + val recordId = _state.value.activeId ?: return + scope.launch { + try { + val reply = api.send( + sid, + buildJsonObject { + put("type", "btw_cancel") + put("recordId", recordId) + }, + ) + // Nothing was running: the shown state is stale (missed frame, lost child). + val cancelled = (reply as? JsonObject)?.get("cancelled") + ?.let { (it as? JsonPrimitive)?.booleanOrNull } + if (cancelled == false) readHistory(sid, explicit = false) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Throwable) { + report(error) + } + } + } + + /** Hide the panel; the record stays in the history. */ + fun close() { + _state.update { it.copy(activeId = null) } + } + + /** Collapse or expand the panel body. */ + fun toggle() { + if (_state.value.activeId == null) return + _state.update { it.copy(expanded = !it.expanded) } + } + + /** `/btw` alone: the history dialog. Unlike background reads, this starts omp. */ + fun openHistory() { + _state.update { it.copy(historyOpen = true) } + val sid = sessionId ?: return + scope.launch { + try { + api.send(sid, buildJsonObject { put("type", "get_state") }) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Throwable) { + report(error) + return@launch + } + readHistory(sid, explicit = true) + } + } + + fun closeHistory() { + _state.update { it.copy(historyOpen = false) } + } + + /** History dialog "Follow up": show that topic in the panel. */ + fun openRecord(recordId: String) { + if (_state.value.records.none { it.id == recordId }) return + _state.update { it.copy(activeId = recordId, expanded = true, historyOpen = false) } + } + + /** Drain queued frames now (before a history snapshot is merged, or on demand). */ + internal fun flushFrames() { + cancelScheduledFlush?.invoke() + cancelScheduledFlush = null + if (pendingFrames.isEmpty()) return + val frames = pendingFrames.toList() + pendingFrames.clear() + val known = _state.value.records.mapTo(HashSet()) { it.id } + _state.update { state -> + state.copy(records = frames.fold(state.records, ::applyBtwEvent)) + } + // A side question started elsewhere (another tab) opens the panel; a + // topic this client already knows never reopens a panel the user closed. + val started = frames.asReversed() + .mapNotNull { parseBtwRecord(it["record"]) } + .firstOrNull { latestBtwTurn(it).status == BtwStatus.RUNNING && it.id !in known } + if (started != null) { + _state.update { it.copy(activeId = started.id, expanded = true) } + } + } + + private fun refreshHistory(explicit: Boolean) { + val sid = sessionId ?: return + if (!explicit && support.isPaused(System.currentTimeMillis())) return + historyJob?.cancel() + historyJob = scope.launch { readHistory(sid, explicit) } + } + + /** + * Merge omp's history. Background reads (SSE open, reconcile, a no-op + * cancel) stay silent: the route never starts omp for them, and an omp + * without the commands is not asked again for a while. + */ + private suspend fun readHistory(sid: String, explicit: Boolean) { + if (!explicit && support.isPaused(System.currentTimeMillis())) return + val knownBefore = _state.value.records.mapTo(LinkedHashSet()) { it.id } + try { + val reply = api.send(sid, buildJsonObject { put("type", "get_btw_history") }) + support.resume() + if (sessionId != sid) return + val snapshot = ((reply as? JsonObject)?.get("records") as? JsonArray) + ?.mapNotNull { parseBtwRecord(it) } + .orEmpty() + // Frames already received go first: the staleness rules then keep + // whichever of them and the snapshot is newer. Applied after it, + // deltas the snapshot already holds would be appended twice. + flushFrames() + if (sessionId != sid) return + val known = _state.value.records.mapTo(HashSet()) { it.id } + _state.update { it.copy(records = mergeBtwHistory(it.records, snapshot, knownBefore)) } + val running = snapshot.firstOrNull { + latestBtwTurn(it).status == BtwStatus.RUNNING && it.id !in known + } + if (running != null && _state.value.activeId == null) { + _state.update { it.copy(activeId = running.id, expanded = true) } + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Throwable) { + if (isUnknownCommand(error)) support.pause(System.currentTimeMillis()) + if (explicit) report(error) + } + } + + private fun report(error: Throwable) { + _errors.tryEmit( + if (isUnknownCommand(error)) { + BtwFailure(BtwFailureKind.UNSUPPORTED) + } else { + BtwFailure(BtwFailureKind.FAILED, errorMessage(error)) + }, + ) + } +} + +private fun errorMessage(error: Throwable): String = error.userMessage() + +/** An omp without the btw commands answers "Unknown command". */ +private fun isUnknownCommand(error: Throwable): Boolean = + error.message?.contains("Unknown command", ignoreCase = true) == true || + (error as? ApiError.ServerResponse)?.code == "unknown_command" diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/AnsiText.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/AnsiText.kt new file mode 100644 index 000000000..31a79cbdc --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/AnsiText.kt @@ -0,0 +1,242 @@ +package io.github.kahme247.ompweb.ui.panels.extension + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily + +/** + * The ANSI slice of `lib/ansi.ts` the extension panels need: SGR styles for + * status/widget/terminal lines, escape stripping and the TUI frame cleanup + * (`normalizeCustomPanelLines`). Only styles Compose can paint are mapped; + * everything else is dropped exactly like the web's `stripAnsi`. + */ +private val ANSI_ESCAPE = + Regex("\u001B(?:\\][^\u0007\u001B]*(?:\u0007|\u001B\\\\)|\\[[0-?]*[ -/]*[@-~]|[@-Z\\\\-_])") +private val ANSI_ESCAPE_AT_START = + Regex("^\u001B(?:\\][^\u0007\u001B]*(?:\u0007|\u001B\\\\)|\\[[0-?]*[ -/]*[@-~]|[@-Z\\\\-_])") +private val ANSI_SGR = Regex("\u001B\\[([0-9;]*)m") +private val TUI_CURSOR_MARKER = Regex("\u001B_pi:c\u0007") + +private val ANSI_8_COLORS = listOf( + "#1f2937", "#dc2626", "#16a34a", "#d97706", "#2563eb", "#9333ea", "#0891b2", "#6b7280", +) +private val ANSI_BRIGHT_COLORS = listOf( + "#9ca3af", "#ef4444", "#22c55e", "#f59e0b", "#3b82f6", "#a855f7", "#06b6d4", "#e5e7eb", +) + +/** One styled run of a line; [dim] is the SGR 2 "faint" attribute. */ +data class AnsiSegment(val text: String, val style: SpanStyle, val dim: Boolean = false) + +fun stripAnsi(text: String): String = text.replace(TUI_CURSOR_MARKER, "").replace(ANSI_ESCAPE, "") + +private fun colorOf(hex: String): Color = Color(0xFF000000L or hex.removePrefix("#").toLong(16)) + +private fun ansi256Color(index: Int): Color? = when { + index in 0..7 -> colorOf(ANSI_8_COLORS[index]) + index in 8..15 -> colorOf(ANSI_BRIGHT_COLORS[index - 8]) + index in 16..231 -> { + val n = index - 16 + val scale = { value: Int -> if (value == 0) 0 else 55 + value * 40 } + Color(scale(n / 36), scale((n % 36) / 6), scale(n % 6)) + } + + index in 232..255 -> { + val gray = 8 + (index - 232) * 10 + Color(gray, gray, gray) + } + + else -> null +} + +private class AnsiStyleState(var style: SpanStyle = SpanStyle(), var dim: Boolean = false) + +private fun AnsiStyleState.applyCodes(codes: List) { + var i = 0 + while (i < codes.size) { + when (val code = codes[i]) { + 0 -> { + style = SpanStyle() + dim = false + } + + 1 -> style = style.copy(fontWeight = FontWeight.Bold) + 2 -> dim = true + 3 -> style = style.copy(fontStyle = FontStyle.Italic) + 4 -> style = style.copy(textDecoration = TextDecoration.Underline) + 22 -> { + style = style.copy(fontWeight = null) + dim = false + } + + 23 -> style = style.copy(fontStyle = null) + 24 -> style = style.copy(textDecoration = TextDecoration.None) + 39 -> style = style.copy(color = Color.Unspecified) + 49 -> style = style.copy(background = Color.Unspecified) + in 30..37 -> style = style.copy(color = colorOf(ANSI_8_COLORS[code - 30])) + in 90..97 -> style = style.copy(color = colorOf(ANSI_BRIGHT_COLORS[code - 90])) + in 40..47 -> style = style.copy(background = colorOf(ANSI_8_COLORS[code - 40])) + in 100..107 -> style = style.copy(background = colorOf(ANSI_BRIGHT_COLORS[code - 100])) + 38, 48 -> { + if (codes.getOrNull(i + 1) == 2) { + val color = Color( + codes.getOrNull(i + 2) ?: 0, + codes.getOrNull(i + 3) ?: 0, + codes.getOrNull(i + 4) ?: 0, + ) + style = if (code == 38) style.copy(color = color) else style.copy(background = color) + i += 4 + } else if (codes.getOrNull(i + 1) == 5) { + ansi256Color(codes.getOrNull(i + 2) ?: -1)?.let { color -> + style = if (code == 38) style.copy(color = color) else style.copy(background = color) + } + i += 2 + } + } + } + i++ + } +} + +/** Split one line into styled runs (web `parseAnsiLine`). */ +fun ansiSegments(line: String): List { + val segments = mutableListOf() + val state = AnsiStyleState() + var lastIndex = 0 + for (match in ANSI_SGR.findAll(line)) { + val text = stripAnsi(line.substring(lastIndex, match.range.first)) + if (text.isNotEmpty()) segments += AnsiSegment(text, state.style, state.dim) + val codes = match.groupValues[1] + .takeIf { it.isNotEmpty() } + ?.split(';') + ?.map { it.toIntOrNull() ?: 0 } + ?: listOf(0) + state.applyCodes(codes) + lastIndex = match.range.last + 1 + } + val tail = stripAnsi(line.substring(lastIndex)) + if (tail.isNotEmpty()) segments += AnsiSegment(tail, state.style, state.dim) + return segments +} + +/** [lines] as one annotated block, joined by newlines (web `renderAnsiLine` per line). */ +fun ansiText(lines: List, baseColor: Color): AnnotatedString = buildAnnotatedString { + lines.forEachIndexed { index, line -> + ansiSegments(line).forEach { segment -> + val style = if (segment.dim) { + val base = segment.style.color.takeIf { it != Color.Unspecified } ?: baseColor + segment.style.copy(color = base.copy(alpha = 0.65f)) + } else { + segment.style + } + pushStyle(style) + append(segment.text) + pop() + } + if (index < lines.lastIndex) append('\n') + } +} + +@Composable +fun AnsiLines( + lines: List, + modifier: Modifier = Modifier, + fontSize: TextUnit = 12.sp, + lineHeight: TextUnit = 16.sp, + color: Color = LocalOmpColors.current.text, +) { + val text = remember(lines, color) { ansiText(lines, color) } + Text( + text = text, + modifier = modifier, + color = color, + fontFamily = OmpMonoFontFamily, + fontSize = fontSize, + lineHeight = lineHeight, + ) +} + +// --------------------------------------------------------------------------- +// Terminal frame cleanup (web `normalizeCustomPanelLines`) +// --------------------------------------------------------------------------- + +private data class VisibleChar(val start: Int, val end: Int, val char: String) + +private val HORIZONTAL_FRAME_LINE = Regex("^[┌├└╭╰][─┬┴┼]+[┐┤┘╮╯]$") + +private fun visibleChars(text: String): List { + val positions = mutableListOf() + var i = 0 + while (i < text.length) { + if (text[i] == '\u001B') { + val match = ANSI_ESCAPE_AT_START.find(text.substring(i)) + if (match != null) { + i += match.value.length + continue + } + } + val char = String(Character.toChars(text.codePointAt(i))) + positions += VisibleChar(i, i + char.length, char) + i += char.length + } + return positions +} + +private fun removeVisibleCharAt(text: String, index: Int): String { + val position = visibleChars(text).getOrNull(index) ?: return text + return text.substring(0, position.start) + text.substring(position.end) +} + +private fun lastNonSpaceVisibleIndex(text: String): Int { + val positions = visibleChars(text) + for (i in positions.indices.reversed()) { + if (positions[i].char.isNotBlank()) return i + } + return -1 +} + +private fun trimEndVisibleSpaces(text: String): String { + var next = text + while (true) { + val positions = visibleChars(next) + val last = positions.lastOrNull() ?: return next + if (last.char.isNotBlank()) return next + next = next.substring(0, last.start) + next.substring(last.end) + } +} + +/** + * Strip the TUI box an extension draws around its custom panel so only the + * content lines remain. Falls back to the raw lines when nothing is left. + */ +fun normalizeCustomPanelLines(lines: List): List { + val normalized = mutableListOf() + for (raw in lines) { + val withoutCursor = raw.replace(TUI_CURSOR_MARKER, "") + if (HORIZONTAL_FRAME_LINE.matches(stripAnsi(withoutCursor).trimEnd())) continue + var line = withoutCursor + val first = visibleChars(line).firstOrNull()?.char + if (first == "│" || first == "┃") { + line = removeVisibleCharAt(line, 0) + if (visibleChars(line).firstOrNull()?.char == " ") line = removeVisibleCharAt(line, 0) + } + val rightIndex = lastNonSpaceVisibleIndex(line) + val right = visibleChars(line).getOrNull(rightIndex)?.char + if (right == "│" || right == "┃") line = removeVisibleCharAt(line, rightIndex) + normalized += trimEndVisibleSpaces(line) + } + while (normalized.isNotEmpty() && stripAnsi(normalized.first()).isBlank()) normalized.removeAt(0) + while (normalized.isNotEmpty() && stripAnsi(normalized.last()).isBlank()) normalized.removeAt(normalized.lastIndex) + return normalized.ifEmpty { lines } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionDialogHost.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionDialogHost.kt new file mode 100644 index 000000000..91d2b073b --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionDialogHost.kt @@ -0,0 +1,514 @@ +package io.github.kahme247.ompweb.ui.panels.extension + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import io.github.kahme247.ompweb.ui.theme.OmpAlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isCtrlPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.AskForm +import io.github.kahme247.ompweb.domain.AskQuestion +import io.github.kahme247.ompweb.domain.ExtensionDialogState +import io.github.kahme247.ompweb.domain.ExtensionResponse +import io.github.kahme247.ompweb.domain.initialAskForm +import io.github.kahme247.ompweb.ui.hosttools.AndroidClipboard +import io.github.kahme247.ompweb.ui.hosttools.openExternalUrl +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors + +/** + * The extension UI of the open session: the blocking dialog and, attached to + * it, the clipboard and Custom-Tab seams the controller needs. + * + * The panel is always composer-attached (`attached: true` in the web), which is + * the only placement the app has: `select` picks a row and confirms with Next, + * `confirm`/`cancel`/system back answer `{cancelled:true}` and Enter submits + * input. The web's overlay rendering (immediate select responses) has no + * Android counterpart and is deliberately not reproduced. + */ +@Composable +fun ExtensionDialogHost(controller: ExtensionUiController) { + val context = LocalContext.current + DisposableEffect(controller, context) { + controller.attachPlatform( + opener = { url -> openExternalUrl(context, url) }, + clipboard = AndroidClipboard(context), + ) + onDispose { controller.detachPlatform() } + } + val dialog by controller.dialog.collectAsStateWithLifecycle() + val clipboardPrompt by controller.clipboardPrompt.collectAsStateWithLifecycle() + if (clipboardPrompt != null) { + ClipboardConsentDialog( + onAllow = controller::approveClipboardRead, + onDeny = controller::denyClipboardRead, + ) + } + val request = dialog ?: return + BackHandler { controller.respond(ExtensionResponse.Cancelled(request.id)) } + ExtensionDialogPanel( + request = request, + onRespond = controller::respond, + ) +} + +/** + * `pi-web://clipboard` reads are default-deny: omp asked for the clipboard and + * the answer waits for this dialog. A denial sticks for the chat. + */ +@Composable +private fun ClipboardConsentDialog(onAllow: () -> Unit, onDeny: () -> Unit) { + OmpAlertDialog( + onDismissRequest = onDeny, + title = { Text(stringResource(R.string.extension_clipboard_title)) }, + text = { Text(stringResource(R.string.extension_clipboard_body)) }, + confirmButton = { + TextButton(onClick = onAllow) { + Text(stringResource(R.string.extension_clipboard_allow)) + } + }, + dismissButton = { + TextButton(onClick = onDeny) { + Text(stringResource(R.string.extension_clipboard_deny)) + } + }, + ) +} + +@Composable +private fun ExtensionDialogPanel( + request: ExtensionDialogState, + onRespond: (ExtensionResponse) -> Unit, +) { + val colors = LocalOmpColors.current + val title = when (request) { + is ExtensionDialogState.Ask -> stringResource(R.string.extension_ask_title) + is ExtensionDialogState.Select -> request.title + is ExtensionDialogState.Confirm -> request.title + is ExtensionDialogState.Input -> request.title + is ExtensionDialogState.Editor -> request.title + } + + var value by remember(request.id) { + mutableStateOf((request as? ExtensionDialogState.Editor)?.prefill.orEmpty()) + } + var selectedOption by remember(request.id) { mutableStateOf(null) } + var form by remember(request.id) { mutableStateOf(initialAskForm((request as? ExtensionDialogState.Ask)?.questions.orEmpty())) } + val questions = (request as? ExtensionDialogState.Ask)?.questions.orEmpty() + val otherFocus = remember(request.id) { List(questions.size) { FocusRequester() } } + val inputHint = (request as? ExtensionDialogState.Input)?.placeholder + val inputPlaceholder: (@Composable () -> Unit)? = if (inputHint != null) { + { Text(inputHint) } + } else { + null + } + + val canSubmit = when (request) { + is ExtensionDialogState.Select -> selectedOption != null + is ExtensionDialogState.Ask -> form.canSubmit(questions) + else -> true + } + + val submit: () -> Unit = { + when (request) { + is ExtensionDialogState.Select -> selectedOption?.let { + onRespond(ExtensionResponse.Value(request.id, it)) + } + + is ExtensionDialogState.Confirm -> + onRespond(ExtensionResponse.Confirmed(request.id, true)) + + is ExtensionDialogState.Input -> + onRespond(ExtensionResponse.Value(request.id, value)) + + is ExtensionDialogState.Editor -> + onRespond(ExtensionResponse.Value(request.id, value)) + + is ExtensionDialogState.Ask -> if (form.canSubmit(questions)) { + onRespond(ExtensionResponse.Answers(request.id, form.answers(questions))) + } + } + } + val cancel = { onRespond(ExtensionResponse.Cancelled(request.id)) } + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 4.dp), + shape = RoundedCornerShape(10.dp), + color = colors.panel, + contentColor = colors.text, + border = BorderStroke(1.dp, colors.border), + ) { + Column { + Column(Modifier.padding(start = 14.dp, end = 14.dp, top = 12.dp, bottom = 8.dp)) { + Text( + title, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.semantics { heading() }, + ) + Spacer(Modifier.width(4.dp)) + Text( + stringResource(R.string.extension_request), + fontFamily = FontFamily.Monospace, + color = colors.textDim, + style = MaterialTheme.typography.labelSmall, + ) + } + Column( + Modifier + // The body is the only flexible part: on a short window + // (split screen, 200 % font) it shrinks and scrolls instead + // of squeezing the action row below it to a few pixels. + .weight(1f, fill = false) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 14.dp, vertical = 6.dp), + ) { + when (request) { + is ExtensionDialogState.Confirm -> Text(request.message) + is ExtensionDialogState.Select -> ExtensionSelect(request, selectedOption) { + selectedOption = it + } + + is ExtensionDialogState.Input -> OutlinedTextField( + value = value, + onValueChange = { value = it }, + placeholder = inputPlaceholder, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { submit() }), + modifier = Modifier + .fillMaxWidth() + .onPreviewKeyEvent { event -> + val enter = event.key == Key.Enter && event.type == KeyEventType.KeyDown + if (enter) submit() + enter + }, + ) + + is ExtensionDialogState.Editor -> OutlinedTextField( + value = value, + onValueChange = { value = it }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Default), + textStyle = if (request.promptStyle) { + MaterialTheme.typography.bodyMedium + } else { + MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace) + }, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 88.dp, max = 220.dp) + .onPreviewKeyEvent { event -> + val submitShortcut = event.type == KeyEventType.KeyDown && + event.isCtrlPressed && event.key == Key.Enter + if (submitShortcut) submit() + submitShortcut + }, + ) + + is ExtensionDialogState.Ask -> ExtensionAskFields( + questions = questions, + form = form, + otherFocus = otherFocus, + onFormChange = { form = it }, + onSubmit = submit, + ) + } + } + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton(onClick = cancel) { Text(stringResource(R.string.extension_cancel)) } + Spacer(Modifier.width(8.dp)) + when (request) { + is ExtensionDialogState.Confirm -> Button(onClick = submit) { + Text(stringResource(R.string.extension_confirm)) + } + + is ExtensionDialogState.Select -> Button(onClick = submit, enabled = canSubmit) { + Text(stringResource(R.string.extension_next)) + } + + is ExtensionDialogState.Ask -> Button(onClick = submit, enabled = canSubmit) { + Text(stringResource(R.string.extension_submit)) + } + + is ExtensionDialogState.Input, is ExtensionDialogState.Editor -> Button(onClick = submit) { + Text(stringResource(R.string.extension_submit)) + } + } + } + } + } +} + +@Composable +private fun ExtensionSelect( + request: ExtensionDialogState.Select, + selected: String?, + onSelect: (String) -> Unit, +) { + val colors = LocalOmpColors.current + Column(Modifier.selectableGroup(), verticalArrangement = Arrangement.spacedBy(8.dp)) { + request.options.forEach { option -> + val isSelected = selected == option + Text( + text = option, + modifier = Modifier + .fillMaxWidth() + .border( + 1.dp, + if (isSelected) colors.accent else colors.border, + RoundedCornerShape(6.dp), + ) + .background( + if (isSelected) colors.selected else colors.panel, + RoundedCornerShape(6.dp), + ) + .selectable(selected = isSelected, role = Role.RadioButton) { onSelect(option) } + .padding(horizontal = 10.dp, vertical = 9.dp), + ) + } + } +} + +@Composable +private fun ExtensionAskFields( + questions: List, + form: AskForm, + otherFocus: List, + onFormChange: (AskForm) -> Unit, + onSubmit: () -> Unit, +) { + val colors = LocalOmpColors.current + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + questions.forEachIndexed { index, question -> + val draft = form.draftAt(index) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + question.header?.let { header -> + Text( + header, + modifier = Modifier + .border(1.dp, colors.border, RoundedCornerShape(999.dp)) + .padding(horizontal = 7.dp, vertical = 1.dp), + color = colors.textMuted, + style = MaterialTheme.typography.labelSmall, + ) + Spacer(Modifier.width(6.dp)) + } + Text(question.question, style = MaterialTheme.typography.bodyMedium) + } + Spacer(Modifier.width(8.dp)) + Column( + Modifier.selectableGroup(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + question.options.forEachIndexed { optionIndex, option -> + val checked = option.label in draft.selected + AskOptionRow( + checked = checked, + multi = question.multi, + label = option.label, + description = option.description, + preview = option.preview, + recommended = optionIndex == question.recommended, + onCheckedChange = { + onFormChange(form.toggleOption(questions, index, option.label)) + }, + ) + } + AskOtherRow( + question = question, + draftOther = draft.other, + chosen = draft.otherChosen, + focusRequester = otherFocus.getOrNull(index), + onChoose = { chosen -> + onFormChange(form.chooseOther(questions, index, chosen)) + if (chosen) otherFocus.getOrNull(index)?.requestFocus() + }, + onFocusOther = { + onFormChange(form.focusOther(questions, index)) + }, + onTextChange = { text -> + onFormChange(form.setOther(questions, index, text)) + }, + onSubmit = onSubmit, + ) + } + } + } + } +} + +@Composable +private fun AskOptionRow( + checked: Boolean, + multi: Boolean, + label: String, + description: String?, + preview: String?, + recommended: Boolean, + onCheckedChange: () -> Unit, +) { + val colors = LocalOmpColors.current + Row( + Modifier + .fillMaxWidth() + .border(1.dp, if (checked) colors.accent else colors.border, RoundedCornerShape(7.dp)) + .background( + if (checked) colors.selected else colors.panel, + RoundedCornerShape(7.dp), + ) + .then( + if (multi) { + Modifier.toggleable(value = checked, role = Role.Checkbox) { onCheckedChange() } + } else { + Modifier.selectable(selected = checked, role = Role.RadioButton) { onCheckedChange() } + }, + ) + .padding(horizontal = 8.dp, vertical = 8.dp), + verticalAlignment = Alignment.Top, + ) { + if (multi) { + Checkbox(checked = checked, onCheckedChange = { onCheckedChange() }) + } else { + RadioButton(selected = checked, onClick = { onCheckedChange() }) + } + Spacer(Modifier.width(6.dp)) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(label, style = MaterialTheme.typography.bodyMedium) + if (recommended) { + Spacer(Modifier.width(6.dp)) + Text( + stringResource(R.string.extension_ask_recommended), + color = colors.accent, + style = MaterialTheme.typography.labelSmall, + ) + } + } + description?.let { + Text(it, color = colors.textMuted, style = MaterialTheme.typography.bodySmall) + } + preview?.let { + Text( + it, + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp) + .background(colors.toolBackground, RoundedCornerShape(6.dp)) + .padding(horizontal = 8.dp, vertical = 6.dp), + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.bodySmall, + ) + } + } + } +} + +@Composable +private fun AskOtherRow( + question: AskQuestion, + draftOther: String, + chosen: Boolean, + focusRequester: FocusRequester?, + onChoose: (Boolean) -> Unit, + onFocusOther: () -> Unit, + onTextChange: (String) -> Unit, + onSubmit: () -> Unit, +) { + val colors = LocalOmpColors.current + Row( + Modifier + .fillMaxWidth() + .border(1.dp, if (chosen) colors.accent else colors.border, RoundedCornerShape(7.dp)) + .background( + if (chosen) colors.selected else colors.panel, + RoundedCornerShape(7.dp), + ) + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.Top, + ) { + if (question.multi) { + Checkbox(checked = chosen, onCheckedChange = onChoose) + } else { + RadioButton(selected = chosen, onClick = { onChoose(true) }) + } + Spacer(Modifier.width(6.dp)) + OutlinedTextField( + value = draftOther, + onValueChange = onTextChange, + placeholder = { Text(stringResource(R.string.extension_ask_other)) }, + maxLines = 4, + modifier = Modifier + .weight(1f) + .then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier) + .onFocusChanged { focus -> + // Tapping back into kept Other text answers with it again (web `onFocus`). + if (focus.isFocused) onFocusOther() + } + .onPreviewKeyEvent { event -> + if (event.type == KeyEventType.KeyDown && event.isCtrlPressed && event.key == Key.Enter) { + onSubmit() + true + } else { + false + } + }, + ) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionStatusBar.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionStatusBar.kt new file mode 100644 index 000000000..fceabd5b8 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionStatusBar.kt @@ -0,0 +1,168 @@ +package io.github.kahme247.ompweb.ui.panels.extension + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.ExtensionStatusItem +import io.github.kahme247.ompweb.domain.ExtensionWidgetItem +import io.github.kahme247.ompweb.domain.ExtensionWidgetPlacement +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors + +/** + * The extension status bar (`setStatus` frames) and the above-editor widgets + * (`setWidget`, placement `aboveEditor`), exactly where the web renders them: + * in the chat column above the panel stack. + */ +@Composable +fun ExtensionStatusBar(controller: ExtensionUiController) { + val statuses by controller.statusItems.collectAsStateWithLifecycle() + val widgets by controller.widgets.collectAsStateWithLifecycle() + ExtensionStatusItems(statuses) + ExtensionWidgetList(widgets.filter { it.placement == ExtensionWidgetPlacement.AboveEditor }) +} + +/** + * The deferred `open_url`: an extension URL never opens by itself, the user + * taps Open here (S4 on the phase-7 review). Dismissing drops the request. + */ +@Composable +fun ExtensionOpenUrlNotice(controller: ExtensionUiController) { + val prompt by controller.openUrlPrompt.collectAsStateWithLifecycle() + val current = prompt ?: return + val colors = LocalOmpColors.current + val text = if (current.instructions.isNullOrBlank()) { + stringResource(R.string.extension_open_url_offer, current.url) + } else { + stringResource(R.string.extension_open_url_instructions, current.instructions, current.url) + } + Surface( + color = colors.accent.copy(alpha = 0.08f), + contentColor = colors.text, + shape = RoundedCornerShape(6.dp), + border = BorderStroke(1.dp, colors.accent.copy(alpha = 0.32f)), + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.padding(start = 9.dp, end = 2.dp, top = 2.dp, bottom = 2.dp), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + ) { + Text( + text = text, + fontSize = 11.5.sp, + modifier = Modifier.weight(1f), + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + TextButton(onClick = controller::openUrlNow) { + Text(stringResource(R.string.extension_open_url_open), fontSize = 11.sp, color = colors.accent) + } + TextButton(onClick = controller::dismissOpenUrl) { + Text(stringResource(R.string.extension_open_url_dismiss), fontSize = 11.sp, color = colors.textMuted) + } + } + } +} + +/** + * The widgets of one placement. The composer area renders the `belowEditor` + * widgets through + * `ExtensionWidgets(controller, ExtensionWidgetPlacement.BelowEditor)`; both + * placements share this list. + */ +@Composable +fun ExtensionWidgets(controller: ExtensionUiController, placement: ExtensionWidgetPlacement) { + val widgets by controller.widgets.collectAsStateWithLifecycle() + ExtensionWidgetList(widgets.filter { it.placement == placement }) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ExtensionStatusItems(statuses: List) { + if (statuses.isEmpty()) return + val colors = LocalOmpColors.current + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + statuses.forEach { status -> + Surface( + shape = RoundedCornerShape(6.dp), + color = colors.accent.copy(alpha = 0.07f), + contentColor = colors.textMuted, + border = BorderStroke(1.dp, colors.accent.copy(alpha = 0.24f)), + ) { + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + status.key, + color = colors.accent, + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.labelSmall, + ) + Text( + stripAnsi(status.text), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 240.dp), + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + } +} + +@Composable +private fun ExtensionWidgetList(widgets: List) { + if (widgets.isEmpty()) return + val colors = LocalOmpColors.current + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + widgets.forEach { widget -> + Surface( + shape = RoundedCornerShape(6.dp), + color = colors.panel, + contentColor = colors.text, + border = BorderStroke(1.dp, colors.border), + ) { + AnsiLines( + lines = widget.lines, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 9.dp, vertical = 8.dp), + fontSize = 12.sp, + ) + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionStrings.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionStrings.kt new file mode 100644 index 000000000..f96682c9d --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionStrings.kt @@ -0,0 +1,38 @@ +package io.github.kahme247.ompweb.ui.panels.extension + +import android.content.Context +import io.github.kahme247.ompweb.R + +/** + * Localized text the extension controllers need but cannot read themselves + * (they hold no `Context`). Resolved once from a context; the defaults are the + * English copy of `strings_extension.xml`. + */ +data class ExtensionStrings( + val openInBrowserTemplate: String = "Open in your browser: %1\$s", + val unsafeUrlBlockedTemplate: String = "Blocked an unsafe URL %1\$s", + val notificationFallbackTemplate: String = "%1\$s: %2\$s", + val clipboardReadNotice: String = "An extension read your clipboard", + val clipboardWriteNotice: String = "An extension copied text to your clipboard", +) { + fun openInBrowser(url: String): String = String.format(openInBrowserTemplate, url) + + fun unsafeUrlBlocked(url: String): String = String.format(unsafeUrlBlockedTemplate, url) + + /** Snackbar text when no system notification could be posted. */ + fun notificationFallback(title: String, message: String): String = + if (message.isBlank()) title else String.format(notificationFallbackTemplate, title, message) + + companion object { + fun from(context: Context): ExtensionStrings { + val app = context.applicationContext + return ExtensionStrings( + openInBrowserTemplate = app.getString(R.string.extension_open_in_browser), + unsafeUrlBlockedTemplate = app.getString(R.string.extension_unsafe_url_blocked), + notificationFallbackTemplate = app.getString(R.string.extension_notification_fallback), + clipboardReadNotice = app.getString(R.string.extension_clipboard_read_notice), + clipboardWriteNotice = app.getString(R.string.extension_clipboard_write_notice), + ) + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionTerminalPanel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionTerminalPanel.kt new file mode 100644 index 000000000..34431a89f --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionTerminalPanel.kt @@ -0,0 +1,200 @@ +package io.github.kahme247.ompweb.ui.panels.extension + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isAltPressed +import androidx.compose.ui.input.key.isCtrlPressed +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.ui.theme.OmpDialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.domain.terminalKeyData +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive + +/** `\u0003` is the Ctrl+C the extension terminal expects from its Stop/Back action. */ +private const val TERMINAL_INTERRUPT = "\u0003" + +/** + * The custom extension panel (`method: "custom"` frames), a full-screen + * terminal that sends `extension_ui_input` for every key. Back and Close send + * Ctrl+C and close the panel locally, as the web modal does; the extension can + * still send `closed: true` to end it. + */ +@Composable +fun ExtensionTerminalPanel(controller: ExtensionUiController) { + val terminal by controller.terminal.collectAsStateWithLifecycle() + val state = terminal ?: return + var dismissed by remember(state.id) { mutableStateOf(false) } + if (dismissed) return + + val close = { + controller.sendInput(JsonPrimitive(TERMINAL_INTERRUPT)) + dismissed = true + } + OmpDialog( + onDismissRequest = close, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + ExtensionTerminalSheet( + lines = normalizeCustomPanelLines(state.lines), + onInput = controller::sendInput, + onClose = close, + ) + } +} + +@Composable +private fun ExtensionTerminalSheet( + lines: List, + onInput: (JsonElement) -> Unit, + onClose: () -> Unit, +) { + val colors = LocalOmpColors.current + var input by remember { mutableStateOf("") } + val send = { text: String -> + if (text.isNotEmpty()) onInput(JsonPrimitive(text)) + input = "" + } + Surface( + modifier = Modifier.fillMaxSize(), + color = colors.background, + contentColor = colors.text, + ) { + Column(Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + stringResource(R.string.extension_terminal_title), + style = MaterialTheme.typography.titleSmall, + ) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + TextButton(onClick = { onInput(JsonPrimitive(TERMINAL_INTERRUPT)) }) { + Text("Ctrl+C") + } + TextButton(onClick = onClose) { + Text(stringResource(R.string.extension_close)) + } + } + } + Surface( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 12.dp), + shape = RoundedCornerShape(8.dp), + color = colors.panel, + contentColor = colors.text, + border = BorderStroke(1.dp, colors.border), + ) { + AnsiLines( + lines = lines.ifEmpty { listOf("") }, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(10.dp), + fontSize = 13.sp, + lineHeight = 19.sp, + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedTextField( + value = input, + onValueChange = { input = it }, + placeholder = { Text(stringResource(R.string.extension_terminal_input)) }, + singleLine = true, + textStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), + keyboardActions = KeyboardActions(onSend = { send(input) }), + modifier = Modifier + .weight(1f) + .onPreviewKeyEvent { event -> + if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false + val key = terminalKeyName(event.key) ?: return@onPreviewKeyEvent false + val data = terminalKeyData( + key = key, + ctrl = event.isCtrlPressed, + alt = event.isAltPressed, + shift = event.isShiftPressed, + ) ?: return@onPreviewKeyEvent false + // A visible input line buffers what the web's hidden + // textarea fires per keystroke: Enter commits it first. + if (key == "Enter" && input.isNotEmpty()) { + onInput(JsonPrimitive(input)) + input = "" + } + onInput(JsonPrimitive(data)) + true + }, + ) + TextButton(onClick = { send(input) }, enabled = input.isNotEmpty()) { + Text(stringResource(R.string.extension_submit)) + } + } + } + } +} + +/** Compose key → the DOM name [terminalKeyData] understands. */ +private fun terminalKeyName(key: Key): String? = when (key) { + Key.Enter, Key.NumPadEnter -> "Enter" + Key.Tab -> "Tab" + Key.Backspace -> "Backspace" + Key.Escape -> "Escape" + Key.DirectionUp -> "ArrowUp" + Key.DirectionDown -> "ArrowDown" + Key.DirectionLeft -> "ArrowLeft" + Key.DirectionRight -> "ArrowRight" + Key.MoveHome -> "Home" + Key.MoveEnd -> "End" + Key.Insert -> "Insert" + Key.Delete -> "Delete" + Key.PageUp -> "PageUp" + Key.PageDown -> "PageDown" + else -> null +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionUiController.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionUiController.kt new file mode 100644 index 000000000..49369aacc --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/panels/extension/ExtensionUiController.kt @@ -0,0 +1,487 @@ +package io.github.kahme247.ompweb.ui.panels.extension + +import io.github.kahme247.ompweb.domain.ClipboardBridge +import io.github.kahme247.ompweb.domain.ClipboardPrompt +import io.github.kahme247.ompweb.domain.ExtensionDialogState +import io.github.kahme247.ompweb.domain.ExtensionNotice +import io.github.kahme247.ompweb.domain.ExtensionNoticeLevel +import io.github.kahme247.ompweb.domain.ExtensionOpenUrlPrompt +import io.github.kahme247.ompweb.domain.ExtensionResponse +import io.github.kahme247.ompweb.domain.ExtensionStatusItem +import io.github.kahme247.ompweb.domain.ExtensionTerminalState +import io.github.kahme247.ompweb.domain.ExtensionUiFrame +import io.github.kahme247.ompweb.domain.ExtensionUiState +import io.github.kahme247.ompweb.domain.ExtensionWidgetItem +import io.github.kahme247.ompweb.domain.HostUriRequest +import io.github.kahme247.ompweb.domain.CLIPBOARD_EMPTY_RESULT +import io.github.kahme247.ompweb.domain.CLIPBOARD_READ_DENIED +import io.github.kahme247.ompweb.domain.CLIPBOARD_UNAVAILABLE_READ +import io.github.kahme247.ompweb.domain.CLIPBOARD_UNAVAILABLE_WRITE +import io.github.kahme247.ompweb.domain.CLIPBOARD_WRITE_FAILED +import io.github.kahme247.ompweb.domain.extensionInputCommand +import io.github.kahme247.ompweb.domain.extensionResponseCommand +import io.github.kahme247.ompweb.domain.hostUriResource +import io.github.kahme247.ompweb.domain.hostUriResultCommand +import io.github.kahme247.ompweb.domain.isSafeOpenUrl +import io.github.kahme247.ompweb.domain.parseExtensionUiFrame +import io.github.kahme247.ompweb.domain.parseHostUriRequest +import io.github.kahme247.ompweb.domain.unknownHostUriResult +import io.github.kahme247.ompweb.net.ExtensionApi +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +/** + * The extension UI of the open session: blocking dialogs (`select`, `confirm`, + * `input`, `editor`, `ask`), the status bar/widgets, the custom/terminal panel, + * `extension_ui_input`, and the `pi-web://clipboard` host URI. + * + * Frame routing follows `hooks/useAgentSession.ts`: a replayed request keeps + * one dialog and `cancel` only clears a matching id. Unlike the web, an + * answered panel closes on the tap instead of 250 ms after the RPC: the route + * only queues the response (fire-and-forget) yet the round trip on a phone + * takes seconds, and a panel that stays up that long swallows every further + * Done/Cancel tap. + * + * Host access is consent-gated: `open_url` never opens on its own (the user + * taps Open on the notice) and clipboard reads are default-deny with a + * per-session approval. Writes and every read are announced in the chat. While + * the surface is hidden the prompts wait for the return (`open_url`) or are + * answered without asking (`host_uri_request`: a read is denied, a write is + * unavailable), because omp waits for its result either way. + */ +class ExtensionUiController( + private val api: ExtensionApi, + private val scope: CoroutineScope, + private val strings: ExtensionStrings = ExtensionStrings(), +) { + private val state = ExtensionUiState() + + private val _dialog = MutableStateFlow(null) + val dialog: StateFlow = _dialog.asStateFlow() + + private val _statusItems = MutableStateFlow>(emptyList()) + val statusItems: StateFlow> = _statusItems.asStateFlow() + + private val _widgets = MutableStateFlow>(emptyList()) + val widgets: StateFlow> = _widgets.asStateFlow() + + private val _terminal = MutableStateFlow(null) + val terminal: StateFlow = _terminal.asStateFlow() + + private val _notices = MutableSharedFlow( + extraBufferCapacity = 16, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + /** In-app notices for the chat's notice bar (`notify`, RPC failures, host access). */ + val notices: SharedFlow = _notices.asSharedFlow() + + private val _editorText = MutableSharedFlow(extraBufferCapacity = 4) + val editorText: SharedFlow = _editorText.asSharedFlow() + + private val _clipboardPrompt = MutableStateFlow(null) + + /** A clipboard read waiting for the user's Allow/Deny. */ + val clipboardPrompt: StateFlow = _clipboardPrompt.asStateFlow() + + private val _openUrlPrompt = MutableStateFlow(null) + + /** An `open_url` the user has not confirmed yet; the chat shows Open/Dismiss. */ + val openUrlPrompt: StateFlow = _openUrlPrompt.asStateFlow() + + private var sessionId: String? = null + /** + * The session whose panels [state] holds. It outlives [sessionId] while the + * chat surface is hidden: the chat keeps its live subscription there, so a + * dialog that arrives in the meantime exists only in this controller (the + * server replays it only to a *new* subscriber, and the hidden chat's + * subscription never stopped). + */ + private var panelSession: String? = null + /** The response awaiting its RPC; blocks a second answer of the same request. */ + private var submittingId: String? = null + private var expiryJob: Job? = null + private var urlOpener: ((String) -> Unit)? = null + private var clipboard: ClipboardBridge? = null + + /** Sessions the user allowed to read the clipboard; cleared when the chat changes. */ + private val clipboardAllowed = mutableSetOf() + + /** Sessions whose read request the user refused; further reads fail without asking. */ + private val clipboardDenied = mutableSetOf() + + private var clipboardRequest: CompletableDeferred? = null + + /** + * Attach the panels to one session. The chat changing drops them (another + * chat's dialog, widget or `open_url` prompt must never appear there, and + * its answered ids stay known); the surface merely leaving the stage + * (`null`) keeps them, so a request that arrived while nothing rendered it + * is still there when the chat comes back — from the list, a notification + * or a rotation. Only the session itself can settle it then (`cancel`, its + * own deadline, an answer from another tab). + */ + fun bind(sessionId: String?) { + if (this.sessionId == sessionId) return + this.sessionId = sessionId + submittingId = null + answerPendingClipboard(false) + clipboardAllowed.clear() + clipboardDenied.clear() + val now = System.currentTimeMillis() + if (sessionId != null && sessionId != panelSession) { + panelSession = sessionId + state.bindSession() + cancelExpiry() + // Another chat: its pending `open_url` must not appear here. + _openUrlPrompt.value = null + } else if (state.dialog == null) { + cancelExpiry() + } else if (state.expire(now)) { + cancelExpiry() + } else { + // Back on the chat that asked (or still away from it): the pending + // request kept its own deadline while nothing rendered it. + scheduleExpiry(now) + } + publish() + } + + /** + * Forget every panel and every answered id: the signed-in user is being + * dropped (sign-out, server switch, 401), so nothing of that installation + * may survive in the heap. [bind] is for the chat surface only. + */ + fun reset() { + this.sessionId = null + panelSession = null + submittingId = null + answerPendingClipboard(false) + clipboardAllowed.clear() + clipboardDenied.clear() + cancelExpiry() + _openUrlPrompt.value = null + state.reset() + publish() + } + + /** + * Consume one session-channel frame. Returns true for every + * `extension_ui_request` (even a method this client ignores) and for + * `host_uri_request`, so the chat engine never mistakes them for transcript + * frames. + */ + fun onFrame(frame: JsonObject): Boolean { + val now = System.currentTimeMillis() + parseExtensionUiFrame(frame, now)?.let { parsed -> + apply(parsed, now) + return true + } + val uri = parseHostUriRequest(frame) ?: return false + handleHostUri(uri) + return true + } + + /** + * Answer the shown dialog; the request id rides in the response. + * + * The panel closes on this tap, not on the RPC's answer. The route does not + * acknowledge the response (`extension_ui_response` only goes into the + * write queue), yet a phone on a tunnel still waits seconds for the round + * trip (far longer when the session has to be started first). While the + * panel stayed up it looked untouched and swallowed every further tap — + * Done and Cancel alike — so the user pressed again and again. The request + * is claimed before the send (its reconnect replay is filtered), it is sent + * exactly once, and a failed send puts the same panel back — but only in + * the session that asked, and only while the request has not expired. + */ + fun respond(response: ExtensionResponse) { + val id = response.id + // A second tap on the same request never reaches the wire. + if (submittingId == id) return + // No session, no claim: a panel without a chat cannot be answered, and + // claiming first would burn its id (the replay could not reopen it). + val sid = sessionId ?: return + // Only the dialog the user is looking at answers; a replay may have + // replaced it (or a newer question arrived) since the tap. + val request = state.dialog?.takeIf { it.id == id } ?: return + if (!state.claimAnswer(request)) return + submittingId = id + publish() + scope.launch { + try { + api.command(sid, extensionResponseCommand(response)) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + // The tap never reached omp: bring the panel back so the answer + // can be repeated, and say why. Only while the panels still + // belong to the session that asked: after a switch the old + // panel belongs to another chat and must not appear there, but + // a chat that merely left the stage keeps it (its subscription + // never stopped, so no replay would bring it back). An expired + // request stays gone (the server settled it), and a newer + // dialog keeps the screen. + if (panelSession == sid) { + val now = System.currentTimeMillis() + if (state.restore(request, now)) { + publish() + scheduleExpiry(now) + } + } + notice(error.message ?: "Unable to answer the extension dialog", ExtensionNoticeLevel.Error) + } finally { + if (submittingId == id) submittingId = null + } + } + } + + /** `extension_ui_input` for the open custom/terminal panel. */ + fun sendInput(data: JsonElement) { + val sid = sessionId ?: return + val id = state.terminal?.id ?: return + scope.launch { + try { + api.command(sid, extensionInputCommand(id, data)) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + notice(error.message ?: "Unable to send extension input", ExtensionNoticeLevel.Error) + } + } + } + + /** The user allowed the shown clipboard read. */ + fun approveClipboardRead() { + val pending = clipboardRequest ?: return + clipboardRequest = null + _clipboardPrompt.value = null + sessionId?.let { clipboardAllowed += it } + pending.complete(true) + } + + /** The user refused the shown clipboard read; later reads of this chat fail without asking. */ + fun denyClipboardRead() { + val pending = clipboardRequest ?: return + clipboardRequest = null + _clipboardPrompt.value = null + sessionId?.let { clipboardDenied += it } + pending.complete(false) + } + + /** Hand the pending `open_url` to the system (the notice's Open action). */ + fun openUrlNow() { + val prompt = _openUrlPrompt.value ?: return + _openUrlPrompt.value = null + if (!isSafeOpenUrl(prompt.url)) { + notice(strings.unsafeUrlBlocked(prompt.url), ExtensionNoticeLevel.Warning) + return + } + urlOpener?.invoke(prompt.url) + ?: notice(strings.openInBrowser(prompt.url), ExtensionNoticeLevel.Info) + } + + /** Dismiss the pending `open_url` without opening it. */ + fun dismissOpenUrl() { + _openUrlPrompt.value = null + } + + /** Attached by [ExtensionDialogHost]: Custom Tab opener and clipboard access. */ + fun attachPlatform(opener: (String) -> Unit, clipboard: ClipboardBridge) { + urlOpener = opener + this.clipboard = clipboard + } + + fun detachPlatform() { + urlOpener = null + clipboard = null + } + + private fun apply(frame: ExtensionUiFrame, now: Long) { + when (frame) { + is ExtensionUiFrame.Dialog -> { + // A replayed request that this client already answered must not + // reopen a panel nobody can answer; any other replay replaces + // the current one (drafts survive by request id). + if (state.request(frame.request)) { + publish() + scheduleExpiry(now) + } + } + + is ExtensionUiFrame.Cancel -> if (state.cancel(frame.targetId)) publish() + + is ExtensionUiFrame.Notify -> if (frame.message.isNotBlank()) { + notice(frame.message, frame.level) + } + + is ExtensionUiFrame.OpenUrl -> openUrl(frame) + + is ExtensionUiFrame.Status -> { + state.setStatus(frame.key, frame.text) + publish() + } + + is ExtensionUiFrame.Widget -> { + state.setWidget(frame.key, frame.lines, frame.placement) + publish() + } + + is ExtensionUiFrame.EditorText -> _editorText.tryEmit(frame.text) + + is ExtensionUiFrame.Terminal -> { + state.setTerminal(frame.id, frame.lines) + publish() + } + + is ExtensionUiFrame.TerminalClosed -> if (state.closeTerminal(frame.id)) publish() + + ExtensionUiFrame.Ignored -> Unit + } + } + + /** + * An extension asked to open a URL. Never auto-opens: the user confirms on + * the notice (web parity plus Android's explicit user gesture). + */ + private fun openUrl(frame: ExtensionUiFrame.OpenUrl) { + if (!isSafeOpenUrl(frame.url)) { + notice(strings.unsafeUrlBlocked(frame.url), ExtensionNoticeLevel.Warning) + return + } + _openUrlPrompt.value = ExtensionOpenUrlPrompt(frame.url, frame.instructions) + } + + private fun scheduleExpiry(now: Long) { + expiryJob?.cancel() + expiryJob = null + val request = state.dialog ?: return + val at = request.expiresAt ?: return + expiryJob = scope.launch { + delay((at - now).coerceAtLeast(0)) + // The deadline is known: compare against it, not against the wake + // time (a delayed coroutine still expires the request). + if (state.expire(at)) publish() + expiryJob = null + } + } + + /** Stop the deadline timer of a panel that is being dropped. */ + private fun cancelExpiry() { + expiryJob?.cancel() + expiryJob = null + } + + /** + * Answer one `host_uri_request` for the session whose stream delivered it. + * The surface may be hidden (list, rotation, notification) while that + * stream keeps running: omp waits for `host_uri_result`, so the answer + * goes out either way. While nothing is on screen, a read is denied + * instead of stacking a consent dialog nobody can see, and a write is + * refused because the chat that would announce it is not there. + */ + private fun handleHostUri(request: HostUriRequest) { + val sid = sessionId ?: panelSession ?: return + val onScreen = sessionId != null + scope.launch { + val resource = hostUriResource(request.url) + val command = when { + !resource.equals("clipboard", ignoreCase = true) -> + hostUriResultCommand(request.id, error = unknownHostUriResult(resource)) + + !onScreen -> hostUriResultCommand( + request.id, + error = if (request.write) CLIPBOARD_UNAVAILABLE_WRITE else CLIPBOARD_READ_DENIED, + ) + + else -> clipboardCommand(request) + } + try { + api.command(sid, command) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (error: Exception) { + notice(error.message ?: "Unable to answer the host request", ExtensionNoticeLevel.Error) + } + } + } + + private suspend fun clipboardCommand(request: HostUriRequest): JsonObject { + val bridge = clipboard + if (request.write) { + if (bridge == null) return hostUriResultCommand(request.id, error = CLIPBOARD_UNAVAILABLE_WRITE) + return try { + bridge.writeText(request.content.orEmpty()) + notice(strings.clipboardWriteNotice) + hostUriResultCommand(request.id) + } catch (error: Exception) { + hostUriResultCommand(request.id, error = CLIPBOARD_WRITE_FAILED) + } + } + if (bridge == null) return hostUriResultCommand(request.id, error = CLIPBOARD_UNAVAILABLE_READ) + if (!allowClipboardRead()) return hostUriResultCommand(request.id, error = CLIPBOARD_READ_DENIED) + return try { + val text = bridge.readText() + notice(strings.clipboardReadNotice) + hostUriResultCommand( + request.id, + content = text?.takeIf { it.isNotEmpty() } ?: CLIPBOARD_EMPTY_RESULT, + ) + } catch (error: Exception) { + hostUriResultCommand(request.id, error = CLIPBOARD_READ_DENIED) + } + } + + /** + * Default-deny read gate: the first read of a chat asks the user, a denial + * sticks for the session, and a second concurrent read is refused instead + * of stacking prompts. + */ + private suspend fun allowClipboardRead(): Boolean { + val sid = sessionId ?: return false + if (sid in clipboardAllowed) return true + if (sid in clipboardDenied) return false + if (clipboardRequest != null) return false + val decision = CompletableDeferred() + clipboardRequest = decision + _clipboardPrompt.value = ClipboardPrompt(sid) + val granted = try { + decision.await() + } finally { + if (clipboardRequest === decision) clipboardRequest = null + } + return granted + } + + /** Answer a read whose chat is going away; the request never hangs. */ + private fun answerPendingClipboard(granted: Boolean) { + val pending = clipboardRequest ?: return + clipboardRequest = null + _clipboardPrompt.value = null + pending.complete(granted) + } + + private fun notice(message: String, level: ExtensionNoticeLevel = ExtensionNoticeLevel.Info) { + _notices.tryEmit(ExtensionNotice(message, level)) + } + + private fun publish() { + _dialog.value = state.dialog + _statusItems.value = state.statusItems + _widgets.value = state.widgets + _terminal.value = state.terminal + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/AgentLinks.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/AgentLinks.kt new file mode 100644 index 000000000..38289130e --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/AgentLinks.kt @@ -0,0 +1,57 @@ +package io.github.kahme247.ompweb.ui.render + +/** + * Port of `lib/agent-links.ts`: `agent://[/...]` handles omp + * advertises for subagent output. A trailing sentence period is never part of + * a match, and the write-only broadcast address is never linked. + */ +private const val SEGMENT = """[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*""" +private const val AGENT_URL_SOURCE = "agent://$SEGMENT(?:/$SEGMENT)*" + +/** The lookbehind rejects handles glued to words or paths (`xagent://`, `a/agent://`). */ +private val AGENT_URL_PROSE_RE = Regex("""(? = + AGENT_URL_PROSE_RE.findAll(text) + .filter { it.value != BROADCAST_AGENT_URL } + .map { it.range } + .toList() + +/** + * Subagent ids an `agent://` href may name, most specific first. Mirrors omp's + * resolver: `agent://Parent/Child` names `Parent.Child` when that output + * exists, else the slash path is JSON extraction on `Parent`. + */ +fun agentLinkIds(href: String?): List { + if (href == null) return emptyList() + val match = AGENT_HREF_RE.matchEntire(href) ?: return emptyList() + val id = match.groups[1]!!.value + if ("agent://$id" == BROADCAST_AGENT_URL) return emptyList() + val path = match.groups[2]?.value.orEmpty() + val segments = path.split('/').filter { it.isNotEmpty() } + if (segments.isNotEmpty() && segments.all { CHILD_SEGMENT_RE.matches(it) }) { + return listOf((listOf(id) + segments).joinToString("."), id) + } + return listOf(id) +} + +/** + * The link target for an `agent://` handle, or null for the broadcast address + * and for hrefs that are not subagent handles. + */ +fun agentLinkTarget(href: String?): LinkTarget.Agent? = + if (isAgentHref(href) && agentLinkIds(href).isNotEmpty()) LinkTarget.Agent(href!!) else null diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/Clipboard.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/Clipboard.kt new file mode 100644 index 000000000..33f3e0b49 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/Clipboard.kt @@ -0,0 +1,41 @@ +package io.github.kahme247.ompweb.ui.render + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import android.os.PersistableBundle + +/** + * A Binder transaction cannot carry arbitrarily long text: an oversized clip + * throws out of the clipboard binding and the copy silently fails. + */ +const val MAX_CLIPBOARD_CHARS = 100_000 + +/** + * Copies [text] and reports whether the clipboard accepted it. [sensitive] + * marks the clip so Android 13+ hides its preview and other apps cannot read + * it; text above [MAX_CLIPBOARD_CHARS] is truncated for the same Binder + * reason. + */ +fun copyTextToClipboard( + context: Context, + label: String, + text: String, + sensitive: Boolean = true, +): Boolean { + val clipboard = context.getSystemService(ClipboardManager::class.java) ?: return false + return try { + val clip = ClipData.newPlainText(label, text.take(MAX_CLIPBOARD_CHARS)) + if (sensitive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + clip.description.extras = PersistableBundle().apply { + putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) + } + } + clipboard.setPrimaryClip(clip) + true + } catch (_: Exception) { + false + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/CodeBlock.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/CodeBlock.kt new file mode 100644 index 000000000..a1015b420 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/CodeBlock.kt @@ -0,0 +1,228 @@ +package io.github.kahme247.ompweb.ui.render + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily + +/** + * Fenced code block with language label, copy button and syntax colours. + * Long lines scroll horizontally unless [wrap] is set; [maxCollapsedLines] + * folds the body behind a "Show all" control like the web's tool-output view. + * + * [revealLine] is the 1-based line a viewer was opened at (a Markdown link, a + * host tool): its y position in the window is reported once through + * [onRevealLine], and the caller scrolls its own container to it. + */ +@Composable +fun CodeBlock( + code: String, + language: String?, + modifier: Modifier = Modifier, + wrap: Boolean = false, + maxCollapsedLines: Int? = null, + revealLine: Int? = null, + onRevealLine: ((Float) -> Unit)? = null, +) { + val colors = LocalOmpColors.current + val context = LocalContext.current + var copied by remember(code) { mutableStateOf(false) } + var expanded by remember(code) { mutableStateOf(false) } + + val totalLines = remember(code) { code.count { it == '\n' } + 1 } + val collapsible = maxCollapsedLines != null && totalLines > maxCollapsedLines && !expanded + val visibleCode = remember(code, collapsible, maxCollapsedLines) { + maxCollapsedLines?.takeIf { collapsible }?.let { firstLines(code, it) } ?: code + } + + val annotated = remember(visibleCode, language, colors) { + highlightCode(visibleCode, language, colors) + } + + // Line reveal: the layout knows where the line sits inside the Text, the + // window position of the Text turns that into an absolute y for the caller. + var layout by remember(visibleCode, wrap) { mutableStateOf(null) } + var textTop by remember(visibleCode) { mutableStateOf(null) } + val revealOffset = remember(visibleCode, revealLine) { + revealLine?.takeIf { !collapsible }?.let { line -> lineStartOffset(visibleCode, line) } + } + LaunchedEffect(revealOffset, layout, textTop, onRevealLine) { + val result = layout ?: return@LaunchedEffect + val top = textTop ?: return@LaunchedEffect + val offset = revealOffset ?: return@LaunchedEffect + onRevealLine?.invoke(top + result.getLineTop(result.getLineForOffset(offset))) + } + + Column( + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + .border(1.dp, colors.border, RoundedCornerShape(8.dp)) + .background(colors.panel), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = language?.takeIf { it.isNotBlank() } ?: "text", + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Text( + text = if (copied) "Copied" else "Copy", + color = if (copied) colors.statusSuccess else colors.textMuted, + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .clickable { + copyTextToClipboard(context, "code", code) + copied = true + Toast.makeText(context, "Copied", Toast.LENGTH_SHORT).show() + } + .padding(horizontal = 8.dp, vertical = 4.dp), + ) + } + Box( + Modifier + .fillMaxWidth() + .background(colors.background), + ) { + Column { + val bodyModifier = if (wrap) { + Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp, vertical = 8.dp) + } else { + Modifier + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 10.dp, vertical = 8.dp) + } + Text( + text = annotated, + modifier = bodyModifier.onGloballyPositioned { textTop = it.positionInRoot().y }, + fontFamily = OmpMonoFontFamily, + fontSize = 12.sp, + lineHeight = 18.sp, + softWrap = wrap, + color = colors.text, + onTextLayout = { layout = it }, + ) + if (maxCollapsedLines != null && totalLines > maxCollapsedLines) { + Text( + text = if (expanded) "Show less" else "Show all ($totalLines lines)", + color = colors.accent, + fontSize = 11.sp, + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded } + .padding(horizontal = 10.dp, vertical = 6.dp), + ) + } + } + } + } +} + +/** + * Character offset where the 1-based [line] starts in [code]; null when the + * line does not exist. The line after a trailing newline is a real (empty) + * line, exactly as the renderer counts it. + */ +internal fun lineStartOffset(code: String, line: Int): Int? { + if (line < 1) return null + var index = 0 + var current = 1 + while (current < line) { + val next = code.indexOf('\n', index) + if (next < 0) return null + index = next + 1 + current++ + } + return index +} + +/** First [count] lines without the trailing newline. */ +internal fun firstLines(code: String, count: Int): String { + if (count <= 0) return "" + var seen = 0 + var index = 0 + while (index < code.length) { + if (code[index] == '\n') { + seen++ + if (seen >= count) return code.substring(0, index) + } + index++ + } + return code +} + +/** + * Maps the tokenizer output onto an [AnnotatedString]. Base colour is applied + * per span so the caller's `Text(color = …)` is never overridden unexpectedly. + */ +internal fun highlightCode(code: String, language: String?, colors: OmpColors): AnnotatedString { + val spans = SyntaxHighlighter.highlight(code, language) + return buildAnnotatedString { + var cursor = 0 + for (span in spans) { + if (span.start > cursor) append(code.substring(cursor, span.start)) + val color = when (span.kind) { + SyntaxHighlighter.Kind.Comment -> colors.textDim + SyntaxHighlighter.Kind.Keyword -> colors.accent + SyntaxHighlighter.Kind.Type -> colors.statusRenamed + SyntaxHighlighter.Kind.String -> colors.statusSuccess + SyntaxHighlighter.Kind.Number -> colors.statusWarning + SyntaxHighlighter.Kind.Literal -> colors.statusModified + } + withStyle( + SpanStyle( + color = color, + fontWeight = if (span.kind == SyntaxHighlighter.Kind.Keyword) FontWeight.Medium else null, + ), + ) { + append(code.substring(span.start, span.end.coerceAtMost(code.length))) + } + cursor = span.end.coerceAtMost(code.length) + } + if (cursor < code.length) append(code.substring(cursor)) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/DiffModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/DiffModel.kt new file mode 100644 index 000000000..d54d4ebe4 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/DiffModel.kt @@ -0,0 +1,380 @@ +package io.github.kahme247.ompweb.ui.render + +/** One side of a split diff row. */ +enum class DiffCellType { Context, Removed, Added, Empty } + +data class SplitDiffCell( + val lineNo: Int?, + val text: String, + val type: DiffCellType, +) + +sealed interface SplitDiffRow { + data class Hunk(val text: String) : SplitDiffRow + data class Line(val left: SplitDiffCell, val right: SplitDiffCell) : SplitDiffRow +} + +data class SplitDiffFile( + val oldPath: String?, + val newPath: String?, + val rows: List, +) + +/** + * Exact port of `parseUnifiedPatch` from `lib/patch.ts`: pairs removed/added + * lines into split cells, keeps `\ No newline` notes and path headers, and + * returns null when the text contains no hunk lines (caller shows plain text). + */ +object UnifiedPatchParser { + + fun parse(text: String): List? { + val files = ArrayList() + var current: MutableFile? = null + var pendingOldPath: String? = null + var oldLineNo = 0 + var newLineNo = 0 + var removed = ArrayList() + var added = ArrayList() + + fun emptyCell(): SplitDiffCell = SplitDiffCell(null, "", DiffCellType.Empty) + + fun flushChanges() { + val file = current ?: run { + removed = ArrayList() + added = ArrayList() + return + } + val count = maxOf(removed.size, added.size) + for (i in 0 until count) { + val left = removed.getOrNull(i)?.let { + SplitDiffCell(it.lineNo, it.text, DiffCellType.Removed) + } ?: emptyCell() + val right = added.getOrNull(i)?.let { + SplitDiffCell(it.lineNo, it.text, DiffCellType.Added) + } ?: emptyCell() + file.rows.add(SplitDiffRow.Line(left, right)) + } + removed = ArrayList() + added = ArrayList() + } + + for (line in text.split(Regex("\r?\n"))) { + if (line.startsWith("--- ")) { + flushChanges() + pendingOldPath = cleanPatchPath(line.substring(4)) + continue + } + if (line.startsWith("+++ ")) { + flushChanges() + current = MutableFile(pendingOldPath, cleanPatchPath(line.substring(4))) + files.add(current!!.toFile()) + continue + } + val hunk = HUNK.matchEntire(line) + if (hunk != null) { + if (current == null) { + current = MutableFile(null, null) + files.add(current!!.toFile()) + } + flushChanges() + oldLineNo = hunk.groups[1]!!.value.toInt() + newLineNo = hunk.groups[2]!!.value.toInt() + current!!.rows.add(SplitDiffRow.Hunk(line)) + continue + } + val file = current ?: continue + if (line.startsWith("\\ ")) { + flushChanges() + file.rows.add(SplitDiffRow.Hunk(line)) + continue + } + val prefix = line.firstOrNull() ?: continue + val content = line.substring(1) + when (prefix) { + ' ' -> { + flushChanges() + file.rows.add( + SplitDiffRow.Line( + SplitDiffCell(oldLineNo++, content, DiffCellType.Context), + SplitDiffCell(newLineNo++, content, DiffCellType.Context), + ), + ) + } + '-' -> removed.add(PendingLine(oldLineNo++, content)) + '+' -> added.add(PendingLine(newLineNo++, content)) + else -> { + if (line.isNotEmpty()) { + flushChanges() + file.rows.add(SplitDiffRow.Hunk(line)) + } + } + } + } + flushChanges() + + val result = files.filter { file -> file.rows.any { it is SplitDiffRow.Line } } + return result.ifEmpty { null } + } + + private data class PendingLine(val lineNo: Int, val text: String) + + private class MutableFile(val oldPath: String?, val newPath: String?) { + val rows = ArrayList() + + fun toFile(): SplitDiffFile = SplitDiffFile(oldPath, newPath, rows) + } + + private fun cleanPatchPath(path: String): String = path.substringBefore('\t').trim() + + private val HUNK = Regex("^@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@") +} + +/** + * Line diff for tool results that ship `oldText`/`newText` instead of a patch. + * Uses an LCS table with a work cap; oversized edits degrade to a whole-block + * replacement instead of allocating unbounded memory. + */ +object ComputedDiff { + + private const val CONTEXT = 3 + private const val MAX_LCS_CELLS = 2_000_000 + + fun split(oldText: String, newText: String): List? { + if (oldText == newText) return null + val oldLines = splitLines(oldText) + val newLines = splitLines(newText) + val ops = diffOps(oldLines, newLines) + if (ops.none { it is Op.Delete || it is Op.Insert }) return null + + val rows = ArrayList() + val changedIndices = ops.indices.filter { ops[it] !is Op.Equal } + if (changedIndices.isEmpty()) return null + + var groupStart = (changedIndices.first() - CONTEXT).coerceAtLeast(0) + var groupEnd = (changedIndices.first() + CONTEXT).coerceAtMost(ops.lastIndex) + var previousGroupEnd = -1 + + fun appendGroup(start: Int, end: Int) { + if (previousGroupEnd >= 0) { + rows.add(SplitDiffRow.Hunk("\u22ef ${start - previousGroupEnd - 1} unchanged lines")) + } + var oldNo = 1 + var newNo = 1 + for (i in 0 until start) { + when (ops[i]) { + is Op.Equal -> { oldNo++; newNo++ } + is Op.Delete -> oldNo++ + is Op.Insert -> newNo++ + } + } + val oldCount = ops.subList(start, end + 1).count { it !is Op.Insert } + val newCount = ops.subList(start, end + 1).count { it !is Op.Delete } + rows.add(SplitDiffRow.Hunk("@@ -$oldNo,$oldCount +$newNo,$newCount @@")) + + var pendingRemoved = ArrayList() + var pendingAdded = ArrayList() + fun flush() { + val count = maxOf(pendingRemoved.size, pendingAdded.size) + for (i in 0 until count) { + rows.add( + SplitDiffRow.Line( + pendingRemoved.getOrNull(i) ?: SplitDiffCell(null, "", DiffCellType.Empty), + pendingAdded.getOrNull(i) ?: SplitDiffCell(null, "", DiffCellType.Empty), + ), + ) + } + pendingRemoved = ArrayList() + pendingAdded = ArrayList() + } + for (i in start..end) { + when (val op = ops[i]) { + is Op.Equal -> { + flush() + rows.add( + SplitDiffRow.Line( + SplitDiffCell(oldNo, op.text, DiffCellType.Context), + SplitDiffCell(newNo, op.text, DiffCellType.Context), + ), + ) + oldNo++ + newNo++ + } + is Op.Delete -> pendingRemoved.add(SplitDiffCell(oldNo++, op.text, DiffCellType.Removed)) + is Op.Insert -> pendingAdded.add(SplitDiffCell(newNo++, op.text, DiffCellType.Added)) + } + } + flush() + previousGroupEnd = end + } + + for (index in changedIndices) { + if (index <= groupEnd) { + groupEnd = (index + CONTEXT).coerceAtMost(ops.lastIndex) + } else { + appendGroup(groupStart, groupEnd) + groupStart = (index - CONTEXT).coerceAtLeast(0) + groupEnd = (index + CONTEXT).coerceAtMost(ops.lastIndex) + } + } + appendGroup(groupStart, groupEnd) + + return listOf(SplitDiffFile(oldPath = null, newPath = null, rows = rows)) + } + + private sealed interface Op { + data class Equal(val text: String) : Op + data class Delete(val text: String) : Op + data class Insert(val text: String) : Op + } + + private fun diffOps(oldLines: List, newLines: List): List { + val prefix = oldLines.zip(newLines).takeWhile { (a, b) -> a == b }.size + var suffix = 0 + while ( + suffix < oldLines.size - prefix && suffix < newLines.size - prefix && + oldLines[oldLines.size - 1 - suffix] == newLines[newLines.size - 1 - suffix] + ) { + suffix++ + } + val oldMiddle = oldLines.subList(prefix, oldLines.size - suffix) + val newMiddle = newLines.subList(prefix, newLines.size - suffix) + + val ops = ArrayList(oldLines.size + newLines.size) + for (i in 0 until prefix) ops.add(Op.Equal(oldLines[i])) + + val cells = oldMiddle.size.toLong() * newMiddle.size.toLong() + if (oldMiddle.isEmpty()) { + for (line in newMiddle) ops.add(Op.Insert(line)) + } else if (newMiddle.isEmpty()) { + for (line in oldMiddle) ops.add(Op.Delete(line)) + } else if (cells <= MAX_LCS_CELLS) { + lcsOps(oldMiddle, newMiddle, ops) + } else { + for (line in oldMiddle) ops.add(Op.Delete(line)) + for (line in newMiddle) ops.add(Op.Insert(line)) + } + + for (i in oldLines.size - suffix until oldLines.size) ops.add(Op.Equal(oldLines[i])) + return ops + } + + private fun lcsOps(oldLines: List, newLines: List, ops: MutableList) { + val n = oldLines.size + val m = newLines.size + val width = m + 1 + val table = IntArray((n + 1) * width) + for (i in n - 1 downTo 0) { + for (j in m - 1 downTo 0) { + table[i * width + j] = if (oldLines[i] == newLines[j]) { + table[(i + 1) * width + j + 1] + 1 + } else { + maxOf(table[(i + 1) * width + j], table[i * width + j + 1]) + } + } + } + var i = 0 + var j = 0 + while (i < n && j < m) { + when { + oldLines[i] == newLines[j] -> { + ops.add(Op.Equal(oldLines[i])) + i++ + j++ + } + table[(i + 1) * width + j] >= table[i * width + j + 1] -> { + ops.add(Op.Delete(oldLines[i])) + i++ + } + else -> { + ops.add(Op.Insert(newLines[j])) + j++ + } + } + } + while (i < n) { + ops.add(Op.Delete(oldLines[i])) + i++ + } + while (j < m) { + ops.add(Op.Insert(newLines[j])) + j++ + } + } + + private fun splitLines(text: String): List { + val lines = text.split('\n') + return if (lines.isNotEmpty() && lines.last().isEmpty()) { + lines.dropLast(1) + } else { + lines + } + } +} + +/** + * Picks the patch text for [DiffView] from a tool result's `details` fields. + * + * A `patch`/`diff` that is already a unified patch is shown verbatim. `omp` + * tools instead report a compact numbered snapshot (`" 2|beta"` for an + * unchanged line, `"+4|delta"` for an added one, `-` for a removed one); that + * text is converted into a unified hunk so the row renders as a real split / + * unified diff instead of a wall of raw lines. Anything else keeps the raw-text + * fallback of `MessageView-diff-view.tsx`. + */ +object ToolDiff { + + private val COMPACT_LINE = Regex("^([ +-])(\\d+)\\|(.*)$") + + fun resolve(patch: String?, diff: String?): String? { + val candidates = listOfNotNull(patch?.takeIf { it.isNotBlank() }, diff?.takeIf { it.isNotBlank() }) + if (candidates.isEmpty()) return null + for (candidate in candidates) { + if (UnifiedPatchParser.parse(candidate) != null) return candidate + compactToUnified(candidate)?.let { return it } + } + return candidates.first() + } + + /** + * Converts `omp`'s numbered diff into a single-hunk unified patch. Returns + * null unless every line carries the `marker|text` shape, so a + * different format keeps its raw presentation. Gaps between hunks are not + * part of that format (the tool only prints changed lines plus context). + */ + fun compactToUnified(text: String): String? { + val body = StringBuilder() + var oldStart: Int? = null + var newStart: Int? = null + var oldCount = 0 + var newCount = 0 + var sawLine = false + for (line in text.lines()) { + if (line.isEmpty()) continue + val match = COMPACT_LINE.matchEntire(line) ?: return null + val lineNo = match.groupValues[2].toIntOrNull() ?: return null + val content = match.groupValues[3] + sawLine = true + when (match.groupValues[1]) { + " " -> { + if (oldStart == null) oldStart = lineNo + if (newStart == null) newStart = lineNo + oldCount++ + newCount++ + body.append(' ').append(content).append('\n') + } + "-" -> { + if (oldStart == null) oldStart = lineNo + oldCount++ + body.append('-').append(content).append('\n') + } + else -> { + if (newStart == null) newStart = lineNo + newCount++ + body.append('+').append(content).append('\n') + } + } + } + if (!sawLine) return null + return "@@ -${oldStart ?: 0},$oldCount +${newStart ?: 0},$newCount @@\n$body" + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/DiffView.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/DiffView.kt new file mode 100644 index 000000000..f83282676 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/DiffView.kt @@ -0,0 +1,335 @@ +package io.github.kahme247.ompweb.ui.render + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily + +private val SPLIT_BREAKPOINT = 600.dp + +/** + * Diff preview identical in spirit to `MessageView-diff-view.tsx`: a unified + * patch is paired into split cells and shown side by side on wide screens, + * stacked on phones. Without a patch, `oldText`/`newText` are diffed locally. + * Hunk headers and unchanged-line gaps are rendered as separators; there is no + * nested vertical scroller. + */ +@Composable +fun DiffView( + path: String?, + oldText: String?, + newText: String?, + unifiedPatch: String?, + modifier: Modifier = Modifier, +) { + val colors = LocalOmpColors.current + val files = remember(unifiedPatch, oldText, newText) { + when { + !unifiedPatch.isNullOrBlank() -> UnifiedPatchParser.parse(unifiedPatch) + oldText != null && newText != null -> ComputedDiff.split(oldText, newText)?.map { file -> + file.copy(oldPath = path, newPath = path) + } + else -> null + } + } + val plainPatch = unifiedPatch?.takeIf { !it.isBlank() && files == null } + + BoxWithConstraints(modifier.fillMaxWidth()) { + val split = maxWidth >= SPLIT_BREAKPOINT + SelectionContainer { + Column( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .border(1.dp, colors.border, RoundedCornerShape(8.dp)) + .background(colors.background), + ) { + when { + files != null -> files.forEachIndexed { index, file -> + if (index > 0) { + Text( + text = "", + modifier = Modifier + .fillMaxWidth() + .background(colors.border) + .padding(top = 1.dp), + ) + } + if (split) { + SplitFile(file, colors) + } else { + UnifiedFile(file, colors) + } + } + plainPatch != null -> PlainPatch(plainPatch, colors) + else -> Text( + text = "No changes", + color = colors.textDim, + fontSize = 12.sp, + modifier = Modifier.padding(10.dp), + ) + } + } + } + } +} + +@Composable +private fun SplitFile(file: SplitDiffFile, colors: OmpColors) { + val title = file.newPath ?: file.oldPath + if (title != null) { + Row( + Modifier + .fillMaxWidth() + .background(colors.panel), + ) { + Text( + text = file.oldPath ?: title, + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + .padding(horizontal = 10.dp, vertical = 5.dp), + ) + Text( + text = file.newPath ?: title, + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + .border(width = 1.dp, color = colors.border) + .padding(horizontal = 10.dp, vertical = 5.dp), + ) + } + } + for (row in file.rows) { + when (row) { + is SplitDiffRow.Hunk -> HunkHeader(row.text, colors) + is SplitDiffRow.Line -> Row(Modifier.fillMaxWidth()) { + SplitCell(row.left, colors, Modifier.weight(1f)) + SplitCell(row.right, colors, Modifier.weight(1f)) + } + } + } +} + +@Composable +private fun SplitCell(cell: SplitDiffCell, colors: OmpColors, modifier: Modifier = Modifier) { + val background = when (cell.type) { + DiffCellType.Added -> colors.statusSuccess.copy(alpha = 0.12f) + DiffCellType.Removed -> colors.statusError.copy(alpha = 0.13f) + DiffCellType.Empty -> colors.backgroundSubtle + DiffCellType.Context -> Color.Transparent + } + val marker = when (cell.type) { + DiffCellType.Added -> "+" + DiffCellType.Removed -> "-" + else -> " " + } + val markerColor = when (cell.type) { + DiffCellType.Added -> colors.statusSuccess + DiffCellType.Removed -> colors.statusError + else -> colors.textDim + } + Row(modifier.background(background)) { + Text( + text = cell.lineNo?.toString().orEmpty(), + modifier = Modifier + .width(38.dp) + .background(colors.panel) + .padding(horizontal = 4.dp), + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + textAlign = TextAlign.End, + maxLines = 1, + ) + Text( + text = marker, + modifier = Modifier.width(16.dp), + color = markerColor, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + textAlign = TextAlign.Center, + maxLines = 1, + ) + Text( + text = cell.text.ifEmpty { " " }, + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + color = if (cell.type == DiffCellType.Empty) colors.textDim else colors.text, + fontSize = 11.sp, + lineHeight = 17.sp, + fontFamily = OmpMonoFontFamily, + ) + } +} + +@Composable +private fun UnifiedFile(file: SplitDiffFile, colors: OmpColors) { + val title = file.newPath ?: file.oldPath + if (title != null) { + Text( + text = title, + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .background(colors.panel) + .padding(horizontal = 10.dp, vertical = 5.dp), + ) + } + for (row in file.rows) { + when (row) { + is SplitDiffRow.Hunk -> HunkHeader(row.text, colors) + is SplitDiffRow.Line -> { + if (row.left.type == DiffCellType.Context && row.right.type == DiffCellType.Context) { + UnifiedCell(" ", row.left, colors) + } else { + if (row.left.type != DiffCellType.Empty) UnifiedCell("-", row.left, colors) + if (row.right.type != DiffCellType.Empty) UnifiedCell("+", row.right, colors) + } + } + } + } +} + +@Composable +private fun UnifiedCell(marker: String, cell: SplitDiffCell, colors: OmpColors) { + val background = when (cell.type) { + DiffCellType.Added -> colors.statusSuccess.copy(alpha = 0.12f) + DiffCellType.Removed -> colors.statusError.copy(alpha = 0.13f) + else -> Color.Transparent + } + val markerColor = when (cell.type) { + DiffCellType.Added -> colors.statusSuccess + DiffCellType.Removed -> colors.statusError + else -> colors.textDim + } + Row(Modifier.fillMaxWidth().background(background)) { + Text( + text = cell.lineNo?.toString().orEmpty(), + modifier = Modifier + .width(40.dp) + .background(colors.panel) + .padding(horizontal = 4.dp), + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + textAlign = TextAlign.End, + maxLines = 1, + ) + Text( + text = marker, + modifier = Modifier.width(16.dp), + color = markerColor, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + textAlign = TextAlign.Center, + maxLines = 1, + ) + Text( + text = cell.text.ifEmpty { " " }, + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + color = colors.text, + fontSize = 11.sp, + lineHeight = 17.sp, + fontFamily = OmpMonoFontFamily, + ) + } +} + +@Composable +private fun HunkHeader(text: String, colors: OmpColors) { + Text( + text = text, + modifier = Modifier + .fillMaxWidth() + .background(colors.backgroundSubtle) + .padding(horizontal = 10.dp, vertical = 4.dp), + color = colors.accent, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + ) +} + +@Composable +private fun PlainPatch(patch: String, colors: OmpColors) { + Column(Modifier.fillMaxWidth()) { + patch.split(Regex("\r?\n")).forEachIndexed { index, line -> + val kind = when { + line.startsWith("@@") -> 1 + line.startsWith("+") && !line.startsWith("+++") -> 2 + line.startsWith("-") && !line.startsWith("---") -> 3 + else -> 0 + } + val background = when (kind) { + 1 -> colors.accent.copy(alpha = 0.10f) + 2 -> colors.statusSuccess.copy(alpha = 0.12f) + 3 -> colors.statusError.copy(alpha = 0.13f) + else -> Color.Transparent + } + val color = when (kind) { + 1 -> colors.accent + 2 -> colors.statusSuccess + 3 -> colors.statusError + else -> colors.text + } + Row(Modifier.fillMaxWidth().background(background)) { + Text( + text = (index + 1).toString(), + modifier = Modifier + .width(44.dp) + .background(colors.panel) + .padding(horizontal = 6.dp), + color = colors.textDim, + fontSize = 11.sp, + fontFamily = OmpMonoFontFamily, + textAlign = TextAlign.End, + maxLines = 1, + ) + Text( + text = line.ifEmpty { " " }, + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp), + color = color, + fontSize = 11.sp, + lineHeight = 17.sp, + fontFamily = OmpMonoFontFamily, + ) + } + } + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/FileLinks.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/FileLinks.kt new file mode 100644 index 000000000..b078e2ded --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/FileLinks.kt @@ -0,0 +1,206 @@ +package io.github.kahme247.ompweb.ui.render + +import io.github.kahme247.ompweb.domain.isSafeOpenUrl +import java.net.URI + +/** + * Port of `lib/file-links.ts`: decides which markdown links and inline-code + * spans name a local file, and normalizes that path for the file API. + * + * The rules are exactly the web's, including its deliberate conservatism: + * anything with a scheme (except `file:`), an `/api/` route or a `//` prefix + * stays a normal link; relative paths need a base directory. + */ +private fun normalizeFilePathSlashes(filePath: String): String { + val windows = Regex("""^[a-zA-Z]:[\\/]""").containsMatchIn(filePath) || filePath.startsWith("\\\\") + return if (windows) filePath.replace('\\', '/') else filePath +} + +private fun safeDecode(value: String): String = + if (value.indexOf('%') < 0) { + value + } else { + try { + // URLDecoder turns `+` into a space; a path may legitimately contain one. + java.net.URLDecoder.decode(value.replace("+", "%2B"), Charsets.UTF_8.name()) + } catch (_: IllegalArgumentException) { + value + } + } + +private fun normalizeLocalPath(filePath: String): String { + val normalized = normalizeFilePathSlashes(filePath) + val isWindowsDrive = Regex("""^[a-zA-Z]:/""").containsMatchIn(normalized) + val isUnc = normalized.startsWith("//") + val leadingSlash = normalized.startsWith("/") && !isWindowsDrive && !isUnc + val parts = mutableListOf() + + for (part in normalized.split('/')) { + if (part.isEmpty() || part == ".") continue + if (part == "..") { + if (parts.isNotEmpty() && parts.last() != "..") { + parts.removeAt(parts.size - 1) + } else if (!leadingSlash && !isWindowsDrive && !isUnc) { + parts.add(part) + } + continue + } + parts.add(part) + } + + val joined = parts.joinToString("/") + return when { + isWindowsDrive -> joined + isUnc -> "//$joined" + leadingSlash -> "/$joined" + else -> joined + } +} + +private fun isPathInside(candidate: String, root: String): Boolean { + val normalizedCandidate = normalizeLocalPath(candidate).trimEnd('/') + val normalizedRoot = normalizeLocalPath(root).trimEnd('/') + val caseInsensitive = Regex("""^[a-zA-Z]:/""").containsMatchIn(normalizedCandidate) || + Regex("""^[a-zA-Z]:/""").containsMatchIn(normalizedRoot) + val filePath = if (caseInsensitive) normalizedCandidate.lowercase() else normalizedCandidate + val rootPath = if (caseInsensitive) normalizedRoot.lowercase() else normalizedRoot + return filePath == rootPath || filePath.startsWith("$rootPath/") +} + +private fun looksLikeRelativeFileHref(href: String): Boolean { + if (href.startsWith("#") || href.startsWith("?")) return false + if (href.startsWith("./") || href.startsWith("../")) return true + if (href.contains('/')) return true + return Regex("""(^|/)\.?[^/]+\.[^/.]+$""").containsMatchIn(href) +} + +private fun fileUrlToPath(href: String): String? { + return try { + val url = URI(href) + if (url.scheme != "file") return null + val pathname = safeDecode(url.rawPath ?: "") + val hostname = url.host + when { + !hostname.isNullOrEmpty() -> "//$hostname${if (pathname.startsWith("/")) pathname else "/$pathname"}" + Regex("""^/[a-zA-Z]:/""").containsMatchIn(pathname) -> pathname.substring(1) + else -> pathname + } + } catch (_: Exception) { + null + } +} + +private val LINE_SUFFIX = Regex("""^(.*?):(\d+)(?::\d+)?$""") +private val LINE_SUFFIX_END = Regex(""":\d+(?::\d+)?$""") + +/** A resolved local file path with the line/column suffix split off. */ +data class LocalFileTarget(val path: String, val line: Int?) + +/** + * Resolves [href] to a local path, or null when it is not a local file link. + * A `:line[:col]` suffix is split off into [LocalFileTarget.line]. + */ +fun resolveLocalFileTarget(href: String?, baseDir: String?, relativeRoot: String? = baseDir): LocalFileTarget? { + val raw = href?.substringBefore('#')?.substringBefore('?')?.trim() ?: return null + if (raw.isEmpty()) return null + val decoded = safeDecode(raw) + val suffix = LINE_SUFFIX.matchEntire(decoded) + val withoutLine = suffix?.groups?.get(1)?.value ?: decoded + val resolved = resolveLocalFileHref(withoutLine, baseDir, relativeRoot) ?: return null + return LocalFileTarget(resolved, suffix?.groups?.get(2)?.value?.toIntOrNull()) +} + +/** + * Port of `resolveLocalFileHref(href, baseDir, relativeRoot)`. + * + * Accepts absolute POSIX paths, `~/…` (expanded server-side), `C:\`/`C:/` + * drive paths, UNC paths and `file:` URLs; relative hrefs resolve against + * [baseDir] and are rejected when they escape it. + */ +fun resolveLocalFileHref(href: String?, baseDir: String?, relativeRoot: String? = baseDir): String? { + if (href.isNullOrEmpty()) return null + + val cleanHref = href.substringBefore('#').substringBefore('?').trim() + if (cleanHref.isEmpty()) return null + + val decodedHref = safeDecode(cleanHref) + val isBackslashUncPath = decodedHref.startsWith("\\\\") + val normalizedHref = normalizeFilePathSlashes(decodedHref) + val lowerHref = normalizedHref.lowercase() + val isWindowsDrivePath = Regex("""^[a-zA-Z]:/""").containsMatchIn(normalizedHref) + + if (lowerHref.startsWith("/api/") || lowerHref.startsWith("/_next/")) return null + if (!isBackslashUncPath && normalizedHref.startsWith("//")) return null + if (Regex("""^[a-zA-Z][a-zA-Z0-9+.-]*:""").containsMatchIn(normalizedHref) && + !lowerHref.startsWith("file:") && !isWindowsDrivePath + ) { + return null + } + + var candidate: String? = null + var candidateKind: String? = null + + when { + lowerHref.startsWith("file:") -> { + candidate = fileUrlToPath(normalizedHref) + candidateKind = if (candidate != null) "absolute" else null + } + isWindowsDrivePath -> { + candidate = normalizedHref + candidateKind = "absolute" + } + normalizedHref.startsWith("/") -> { + candidate = normalizedHref + candidateKind = "absolute" + } + normalizedHref.startsWith("~/") -> { + // Kept as `~/…`: the file API expands it to the server user's home. + candidate = normalizedHref + candidateKind = "absolute" + } + baseDir != null && looksLikeRelativeFileHref(normalizedHref) -> { + candidate = "${normalizeFilePathSlashes(baseDir).trimEnd('/')}/$normalizedHref" + candidateKind = "relative" + } + } + + val resolvedCandidate = candidate ?: return null + val filePath = normalizeLocalPath(resolvedCandidate).replace(LINE_SUFFIX_END, "") + if (candidateKind == "relative" && !relativeRoot.isNullOrEmpty() && !isPathInside(filePath, relativeRoot)) { + return null + } + // `~/../x` normalizes out of the home dir; it no longer names a home path. + if (normalizedHref.startsWith("~/") && !filePath.startsWith("~/")) return null + return filePath +} + +private const val PATH_SEGMENT = """[\w.@+~\[\]-]+""" + +/** + * Inline code that is exactly one local path, with an optional `:line[:col]`. + * Conservative on purpose — anything with spaces, quotes, parens, `?`, `#` or + * a scheme stays code (same expression as `lib/file-links.ts`). + */ +private val INLINE_CODE_PATH_RE = Regex( + "^(?:" + + "/$PATH_SEGMENT(?:/$PATH_SEGMENT)+" + + "|~/$PATH_SEGMENT(?:/$PATH_SEGMENT)*" + + """|[A-Za-z]:[\\/]$PATH_SEGMENT(?:[\\/]$PATH_SEGMENT)*""" + + """|(?:$PATH_SEGMENT/)+[\w.@+~\[\]-]*\.[A-Za-z][A-Za-z0-9]*""" + + """)(?::\d+(?::\d+)?)?$""", +) + +/** True when an inline code span is exactly one local file path (before cwd resolution). */ +fun isInlineCodeFilePath(text: String): Boolean = INLINE_CODE_PATH_RE.matches(text) + +/** + * Classifies any markdown href into the click target the chat view handles. + * Order matters: agent handles, then local files, then GitHub issue URLs, + * then external URLs the system may open; everything else stays inert. + */ +fun classifyLink(href: String, baseDir: String?): LinkTarget { + agentLinkTarget(href)?.let { return it } + resolveLocalFileTarget(href, baseDir)?.let { return LinkTarget.File(it.path, it.line) } + githubIssueFromUrl(href)?.let { return it } + return if (isSafeOpenUrl(href)) LinkTarget.External(href) else LinkTarget.Inert +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/GithubRefs.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/GithubRefs.kt new file mode 100644 index 000000000..b78652ea0 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/GithubRefs.kt @@ -0,0 +1,63 @@ +package io.github.kahme247.ompweb.ui.render + +/** + * Port of `lib/github-refs.ts`: links GitHub issue/PR references in prose. + * + * `owner/repo#N` always links; bare `#N` links only when a repository is known. + * The lookbehind rejects references glued to words or paths (`C#12`, `file.ts#12`, + * `a/b/c#12`, `{`). Code spans, existing links and link labels are not + * scanned — the markdown renderer only calls this for plain text runs. + */ +private val GITHUB_REF_RE = Regex( + """(? { + val defaultSlug = repo?.takeIf { it.owner.isNotBlank() && it.repo.isNotBlank() }?.let { "${it.owner}/${it.repo}" } + return GITHUB_REF_RE.findAll(text).mapNotNull { match -> + val slug = match.groups[1]?.value ?: defaultSlug ?: return@mapNotNull null + val owner = slug.substringBefore('/') + val name = slug.substringAfter('/', "") + if (owner.isEmpty() || name.isEmpty()) return@mapNotNull null + val number = match.groups[2]?.value?.toIntOrNull() ?: return@mapNotNull null + GithubRef( + start = match.range.first, + end = match.range.last + 1, + text = match.value, + target = LinkTarget.GithubIssue(owner = owner, repo = name, number = number), + ) + }.toList() +} + +/** `/issues/N` links a PR too, so both spellings classify as a GitHub issue. */ +private val GITHUB_ISSUE_URL = Regex( + """^https?://(?:www\.)?github\.com/([A-Za-z0-9][A-Za-z0-9-]*)/([A-Za-z0-9._-]+)/(?:issues|pull)/([1-9]\d{0,9})/?$""", + RegexOption.IGNORE_CASE, +) + +/** Classifies an explicit GitHub issue/PR URL, ignoring query strings and fragments. */ +fun githubIssueFromUrl(url: String): LinkTarget.GithubIssue? { + val clean = url.substringBefore('#').substringBefore('?').trimEnd('/') + val match = GITHUB_ISSUE_URL.matchEntire(clean) ?: return null + val number = match.groups[3]?.value?.toIntOrNull() ?: return null + return LinkTarget.GithubIssue( + owner = match.groups[1]!!.value, + repo = match.groups[2]!!.value, + number = number, + ) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/ImageSource.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/ImageSource.kt new file mode 100644 index 000000000..464409f68 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/ImageSource.kt @@ -0,0 +1,26 @@ +package io.github.kahme247.ompweb.ui.render + +/** Image data the transcript can reference: a server path/URL or inline base64. */ +sealed interface ImageSource { + data class Url(val path: String) : ImageSource + data class Inline(val mimeType: String, val base64: String) : ImageSource +} + +/** + * Classifies an image reference. Markdown images may carry a `data:` URI, + * which is split into an inline source instead of becoming an unloadable URL. + */ +fun imageSourceFor(src: String): ImageSource { + if (src.startsWith("data:", ignoreCase = true)) { + val comma = src.indexOf(',') + if (comma > 5) { + val header = src.substring(5, comma) + if (header.contains(";base64", ignoreCase = true)) { + val mimeType = header.substringBefore(';').ifBlank { "image/png" } + val payload = src.substring(comma + 1) + if (payload.isNotBlank()) return ImageSource.Inline(mimeType, payload) + } + } + } + return ImageSource.Url(src) +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/LinkTarget.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/LinkTarget.kt new file mode 100644 index 000000000..b1c3b8f13 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/LinkTarget.kt @@ -0,0 +1,28 @@ +package io.github.kahme247.ompweb.ui.render + +/** + * What a click on a link in a rendered message should do. + * + * Mirrors the routing rules of the web's `MarkdownBody` (`components/MarkdownBody.tsx`) + * and `lib/agent-links.ts`; the chat screen owns the actual navigation. + */ +sealed interface LinkTarget { + /** Any link that is not a local file, an `agent://` handle or a GitHub ref. */ + data class External(val url: String) : LinkTarget + + /** A local path, already resolved and normalized like `resolveLocalFileHref`. */ + data class File(val path: String, val line: Int?) : LinkTarget + + /** `agent://…` — [handle] is the full URL, e.g. `agent://Parent/Child`. */ + data class Agent(val handle: String) : LinkTarget + + /** `#N` / `owner/repo#N`; `/issues/N` redirects to the PR when N is one. */ + data class GithubIssue(val owner: String, val repo: String, val number: Int) : LinkTarget + + /** + * A href the app must not hand to the system: any scheme outside + * http/https/mailto (`javascript:`, `intent:`, `content:`, …), fragments + * and relative paths. Such links render as plain text. + */ + data object Inert : LinkTarget +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/Markdown.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/Markdown.kt new file mode 100644 index 000000000..e3578a7f8 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/Markdown.kt @@ -0,0 +1,698 @@ +package io.github.kahme247.ompweb.ui.render + +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.hrm.latex.renderer.LatexAutoWrap +import com.hrm.latex.renderer.model.LatexConfig +import com.hrm.latex.renderer.model.LatexTheme +import com.hrm.latex.renderer.model.LatexThemeColors +import com.hrm.latex.renderer.measure.LatexMeasurerState +import com.hrm.latex.renderer.measure.rememberLatexMeasurer +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import io.github.kahme247.ompweb.ui.theme.OmpColors +import io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily + +/** `owner/repo` of the GitHub repository bare `#N` references resolve against. */ +data class GithubRepoRef(val owner: String, val repo: String) + +/** + * Base directory used to resolve relative file links in markdown (the session + * cwd). Absolute paths, `~/` paths and `file:` URLs work without it. + */ +val LocalFileLinkBaseDir = staticCompositionLocalOf { null } + +/** + * Renders markdown the way `components/MarkdownBody.tsx` does: GFM blocks, + * native math, mermaid, syntax-highlighted code, GitHub/agent/file link + * routing and selectable text. + * + * [onLink] receives the classified target; the chat screen decides whether to + * open a Custom Tab, the subagent dialog or the file viewer. + */ +@Composable +fun MarkdownText( + markdown: String, + modifier: Modifier = Modifier, + githubRepo: GithubRepoRef? = null, + onLink: (LinkTarget) -> Unit, + selectable: Boolean = true, +) { + val baseDir = LocalFileLinkBaseDir.current + val cache = remember(githubRepo, baseDir) { MarkdownDocumentCache() } + val document = remember(markdown, githubRepo, baseDir) { cache.parse(markdown, githubRepo) } + + val body: @Composable () -> Unit = { + MarkdownBlocks( + blocks = document.blocks, + githubRepo = githubRepo, + baseDir = baseDir, + onLink = onLink, + ) + } + Box(modifier) { + if (selectable) { + SelectionContainer { body() } + } else { + body() + } + } +} + +/** + * Keeps the parsed prefix of a streaming message and only re-parses the tail. + * Appends are the common case; any rewrite (edited message, a delimiter that + * changes meaning backwards) falls back to a full parse. + */ +internal class MarkdownDocumentCache { + private var document: MarkdownDocument? = null + private var repo: GithubRepoRef? = null + + /** Number of full parses; tests use it to prove appends stay incremental. */ + internal var fullParses: Int = 0 + private set + + fun parse(raw: String, githubRepo: GithubRepoRef?): MarkdownDocument { + // CRLF would leave a `\r` on every line, and the offsets the cache + // compares must be the ones of the document the parser produced. + val markdown = MarkdownParser.normalizeLineEndings(raw) + val normalized = MarkdownParser.normalizeDisplayMath(markdown) + val previous = document + if ( + previous != null && githubRepo == repo && + normalized.length >= previous.text.length && normalized.startsWith(previous.text) + ) { + if (normalized.length == previous.text.length) return previous + // A single growing paragraph: extend its inline tree instead of + // re-parsing it (the usual streaming case). + if (previous.blocks.size == 1) { + appendPlainSuffix(previous, normalized, githubRepo)?.let { + document = it + repo = githubRepo + return it + } + } + // Only the tail can gain meaning from appended text, but a block + // that starts on the next line without a blank line can dissolve + // into the paragraph above it (`*` stops being a list item once a + // letter follows), so the tail starts at the first block after + // such a run. + var keep = previous.blocks.size - 1 + while (keep > 0 && !separatedByBlankLine(previous, keep)) keep-- + if (keep > 0) { + val tail = MarkdownParser.parseTail(normalized, previous.starts[keep], githubRepo) + val merged = MarkdownDocument( + text = normalized, + blocks = previous.blocks.take(keep) + tail.map { it.block }, + starts = previous.starts.take(keep) + tail.map { it.start }, + ends = previous.ends.take(keep) + tail.map { it.end }, + ) + document = merged + repo = githubRepo + return merged + } + } + fullParses++ + return MarkdownParser.parse(markdown, githubRepo).also { + document = it + repo = githubRepo + } + } + + /** + * Appends a syntactically inert suffix to a single-paragraph document. + * + * The suffix must contain no inline construct character (so it cannot + * start emphasised/code/link text). When the paragraph ends in plain text, + * its trailing token extended by the suffix must still linkify to plain + * text (so a bare URL or handle cannot form across the boundary); when it + * ends in a closed construct, the suffix forms its own token, which must + * linkify to exactly the appended text. A link at the end is never safe: + * it may have been cut out of end-of-text text by linkification and can + * keep growing (`www.exam` + `ple.com`). Returns null when a check fails; + * the caller then re-parses the paragraph. + */ + private fun appendPlainSuffix( + previous: MarkdownDocument, + normalized: String, + githubRepo: GithubRepoRef?, + ): MarkdownDocument? { + val paragraph = previous.blocks.singleOrNull() as? MdBlock.Paragraph ?: return null + if (previous.ends.singleOrNull() != previous.text.length) return null + val suffix = normalized.substring(previous.text.length) + if (suffix.isEmpty() || !PLAIN_SUFFIX.matches(suffix)) return null + // Appending can turn the current last line into a block marker + // ("1" + "." becomes a list item), which ends the paragraph here. + if (MarkdownParser.lastLineStartsBlock(normalized)) return null + val last = paragraph.inlines.lastOrNull() ?: return null + val inlines = if (last is MdInline.Text) { + val token = last.text.substring(last.text.indexOfLast { it.isWhitespace() } + 1) + val extended = token + suffix + val stillPlain = MarkdownParser.parseInlines(extended, githubRepo).singleOrNull() as? MdInline.Text + if (stillPlain?.text != extended) return null + paragraph.inlines.dropLast(1) + MdInline.Text(last.text + suffix) + } else { + if (!isSealedConstruct(last, previous.text)) return null + // The paragraph ends with a closed construct, so the suffix starts a + // new token that no earlier construct can reach into. + val plain = MarkdownParser.parseInlines(suffix, githubRepo).singleOrNull() as? MdInline.Text + if (plain?.text != suffix) return null + paragraph.inlines + plain + } + return MarkdownDocument( + text = normalized, + blocks = listOf(MdBlock.Paragraph(inlines)), + starts = previous.starts, + ends = listOf(normalized.length), + ) + } + + /** + * Whether a block boundary at [index] has a blank line before it. Without + * one the block may merge back into the paragraph above once appended text + * changes how its first line is read. + */ + private fun separatedByBlankLine(document: MarkdownDocument, index: Int): Boolean { + if (index <= 0 || index >= document.starts.size) return false + val from = document.ends[index - 1] + val to = document.starts[index] + if (from < 0 || from >= to || to > document.text.length) return false + return BLANK_LINE.containsMatchIn(document.text.substring(from, to)) + } + + private companion object { + /** Characters that would start or extend an inline construct. */ + val PLAIN_SUFFIX = Regex("""^[^`*_~$\[\]()<>!#|@:/\\&\r\n]*$""") + + /** A blank line between two block spans (line endings vary). */ + val BLANK_LINE = Regex("""\n[ \t\r]*\n""") + } +} + +/** + * Inline nodes whose source cannot absorb appended plain text, verified by + * probing every character the suffix allows: the node stays the same and the + * suffix becomes one plain text node. Underscore emphasis, inline math and + * links are excluded because their closing delimiter depends on what follows + * (`_a_` + `b` is no longer emphasis, `$a$` + `2` no longer math) or because + * linkification can cut them out of end-of-text content. + */ +private fun isSealedConstruct(inline: MdInline, source: String): Boolean = when (inline) { + is MdInline.Code, is MdInline.Strike, is MdInline.Image -> true + // Asterisk emphasis keeps closing before plain text; `_a_` + `b` does not. + is MdInline.Strong, is MdInline.Em -> source.lastOrNull() == '*' + else -> false +} + +@Composable +private fun MarkdownBlocks( + blocks: List, + githubRepo: GithubRepoRef?, + baseDir: String?, + onLink: (LinkTarget) -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + blocks.forEach { block -> + MarkdownBlock(block, githubRepo, baseDir, onLink) + } + } +} + +@Composable +private fun MarkdownBlock( + block: MdBlock, + githubRepo: GithubRepoRef?, + baseDir: String?, + onLink: (LinkTarget) -> Unit, +) { + when (block) { + is MdBlock.Heading -> { + val size = when (block.level) { + 1 -> 25.sp + 2 -> 22.sp + 3 -> 19.sp + 4 -> 17.sp + 5 -> 16.sp + else -> 15.sp + } + InlineText( + inlines = block.inlines, + style = MaterialTheme.typography.bodyLarge.copy( + fontSize = size, + fontWeight = FontWeight.Bold, + lineHeight = size * 1.3f, + ), + modifier = Modifier + .fillMaxWidth() + .padding(top = if (block.level == 1) 8.dp else 4.dp), + baseDir = baseDir, + onLink = onLink, + ) + } + + is MdBlock.Paragraph -> InlineText( + inlines = block.inlines, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.fillMaxWidth(), + baseDir = baseDir, + onLink = onLink, + ) + + is MdBlock.CodeFence -> { + if (block.language?.lowercase() == "mermaid") { + MermaidBlock(block.code) + } else { + CodeBlock( + code = block.code, + language = block.language, + modifier = Modifier.fillMaxWidth(), + ) + } + } + + is MdBlock.MathBlock -> MathFormula( + tex = block.tex, + modifier = Modifier.fillMaxWidth(), + ) + + is MdBlock.Quote -> { + val barColor = LocalOmpColors.current.border + Box( + Modifier + .fillMaxWidth() + .drawBehind { + drawRect( + color = barColor, + size = androidx.compose.ui.geometry.Size(3.dp.toPx(), size.height), + ) + }, + ) { + Box(Modifier.padding(start = 13.dp)) { + MarkdownBlocks(block.blocks, githubRepo, baseDir, onLink) + } + } + } + + is MdBlock.ListBlock -> MarkdownList(block, githubRepo, baseDir, onLink) + + is MdBlock.Table -> MarkdownTable(block, baseDir, onLink) + + MdBlock.Rule -> HorizontalDivider(color = LocalOmpColors.current.border) + + is MdBlock.Image -> RemoteImage( + source = imageSourceFor(block.src), + contentDescription = block.alt.ifBlank { null }, + modifier = Modifier + .fillMaxWidth() + .widthIn(max = 480.dp), + ) + + is MdBlock.Group -> Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + block.blocks.forEach { MarkdownBlock(it, githubRepo, baseDir, onLink) } + } + } +} + +@Composable +private fun MarkdownList( + block: MdBlock.ListBlock, + githubRepo: GithubRepoRef?, + baseDir: String?, + onLink: (LinkTarget) -> Unit, +) { + val colors = LocalOmpColors.current + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + block.items.forEachIndexed { index, item -> + Row(Modifier.fillMaxWidth()) { + val marker = when { + item.task != null -> if (item.task) "\u2611" else "\u2610" + block.ordered -> "${block.start + index}." + else -> "•" + } + Text( + text = marker, + style = MaterialTheme.typography.bodyMedium, + color = if (item.task != null) colors.accent else colors.textMuted, + modifier = Modifier + .width(if (block.ordered) 30.dp else 20.dp) + .padding(top = 1.dp), + textAlign = if (block.ordered) TextAlign.End else TextAlign.Center, + ) + Spacer(Modifier.width(8.dp)) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + item.blocks.forEach { MarkdownBlock(it, githubRepo, baseDir, onLink) } + } + } + } + } +} + +/** Plain text of an inline run, used to measure table column widths. */ +internal fun plainText(inlines: List): String = buildString { + fun appendAll(nodes: List) { + for (node in nodes) { + when (node) { + is MdInline.Text -> append(node.text) + is MdInline.Code -> append(node.text) + is MdInline.Strong -> appendAll(node.children) + is MdInline.Em -> appendAll(node.children) + is MdInline.Strike -> appendAll(node.children) + is MdInline.Link -> appendAll(node.children) + is MdInline.Image -> append(node.alt) + is MdInline.Math -> append(node.tex) + MdInline.SoftBreak -> append(' ') + MdInline.HardBreak -> append(' ') + } + } + } + appendAll(inlines) +} + +@Composable +private fun MarkdownTable( + block: MdBlock.Table, + baseDir: String?, + onLink: (LinkTarget) -> Unit, +) { + val colors = LocalOmpColors.current + val scroll = rememberScrollState() + val density = LocalDensity.current + val measurer = rememberTextMeasurer() + val style = MaterialTheme.typography.bodySmall + val widths = remember(block, style, density) { + val columnCount = maxOf( + block.header.size, + block.rows.maxOfOrNull { it.size } ?: 0, + ) + (0 until columnCount).map { column -> + var widest = 0 + fun measureRow(cells: List>) { + val cell = cells.getOrNull(column) ?: return + val result = measurer.measure( + text = AnnotatedString(plainText(cell)), + style = style, + softWrap = false, + maxLines = 1, + ) + widest = maxOf(widest, result.size.width) + } + measureRow(block.header) + block.rows.forEach(::measureRow) + with(density) { + (widest + 20.dp.roundToPx()).toDp().coerceIn(96.dp, 260.dp) + } + } + } + Box( + Modifier + .fillMaxWidth() + .horizontalScroll(scroll), + ) { + Column(Modifier.widthIn(min = 120.dp)) { + TableRow( + cells = block.header, + widths = widths, + align = block.align, + header = true, + baseDir = baseDir, + onLink = onLink, + ) + block.rows.forEach { row -> + HorizontalDivider(color = colors.border) + TableRow( + cells = row, + widths = widths, + align = block.align, + header = false, + baseDir = baseDir, + onLink = onLink, + ) + } + } + } +} + +@Composable +private fun TableRow( + cells: List>, + widths: List, + align: List, + header: Boolean, + baseDir: String?, + onLink: (LinkTarget) -> Unit, +) { + val colors = LocalOmpColors.current + Row(Modifier.background(if (header) colors.backgroundSubtle else Color.Transparent)) { + cells.forEachIndexed { index, cell -> + val textAlign = when (align.getOrElse(index) { MdAlign.None }) { + MdAlign.Left -> TextAlign.Start + MdAlign.Center -> TextAlign.Center + MdAlign.Right -> TextAlign.End + MdAlign.None -> TextAlign.Start + } + InlineText( + inlines = cell, + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = if (header) FontWeight.SemiBold else FontWeight.Normal, + ), + modifier = Modifier + .width(widths.getOrElse(index) { 96.dp }) + .padding(horizontal = 10.dp, vertical = 7.dp), + baseDir = baseDir, + onLink = onLink, + textAlign = textAlign, + ) + } + } +} + +/** + * Builds the annotated text for one inline run. Math is embedded through the + * LaTeX measurer; if measurement fails the source remains as alternate text. + */ +@Composable +private fun InlineText( + inlines: List, + style: TextStyle, + modifier: Modifier, + baseDir: String?, + onLink: (LinkTarget) -> Unit, + textAlign: TextAlign? = null, + maxLines: Int = Int.MAX_VALUE, +) { + val colors = LocalOmpColors.current + val currentOnLink by rememberUpdatedState(onLink) + val measurer = rememberLatexMeasurer( + LatexConfig( + fontSize = if (style.fontSize.value.isFinite() && style.fontSize.value > 0f) style.fontSize else 16.sp, + theme = latexTheme(colors), + ), + ) + val inlineContent = remember { LinkedHashMap() } + val annotated = remember(inlines, style, colors, measurer, baseDir) { + inlineContent.clear() + buildAnnotatedString { + appendInlines( + inlines = inlines, + style = style, + colors = colors, + measurer = measurer, + inlineContent = inlineContent, + baseDir = baseDir, + onLink = { target -> currentOnLink(target) }, + ) + } + } + Text( + text = annotated, + modifier = modifier, + style = style, + color = colors.text, + inlineContent = inlineContent, + textAlign = textAlign, + maxLines = maxLines, + ) +} + +@Composable +private fun latexTheme(colors: OmpColors): LatexTheme = LatexTheme.auto( + light = LatexThemeColors(color = colors.text, backgroundColor = Color.Transparent), + dark = LatexThemeColors(color = colors.text, backgroundColor = Color.Transparent), +) + +private fun AnnotatedString.Builder.appendInlines( + inlines: List, + style: TextStyle, + colors: OmpColors, + measurer: LatexMeasurerState, + inlineContent: MutableMap, + baseDir: String?, + onLink: (LinkTarget) -> Unit, +) { + var annotationIndex = 0 + + fun appendNodes(nodes: List, span: SpanStyle) { + for (node in nodes) { + when (node) { + is MdInline.Text -> withStyle(span) { append(node.text) } + is MdInline.SoftBreak -> withStyle(span) { append(' ') } + is MdInline.HardBreak -> withStyle(span) { append('\n') } + is MdInline.Strong -> appendNodes(node.children, span + SpanStyle(fontWeight = FontWeight.Bold)) + is MdInline.Em -> appendNodes(node.children, span + SpanStyle(fontStyle = FontStyle.Italic)) + is MdInline.Strike -> appendNodes(node.children, span + SpanStyle(textDecoration = TextDecoration.LineThrough)) + is MdInline.Code -> { + val codeStyle = span + SpanStyle( + fontFamily = OmpMonoFontFamily, + fontSize = (style.fontSize.value * 0.92f).sp, + background = colors.text.copy(alpha = 0.08f), + ) + val target = inlineCodeTarget(node.text, baseDir) + if (target != null) { + val link = LinkAnnotation.Clickable( + tag = "md-inline-${annotationIndex++}", + styles = linkStyles(colors), + linkInteractionListener = { onLink(target) }, + ) + withLink(link) { withStyle(codeStyle) { append(node.text) } } + } else { + withStyle(codeStyle) { append(node.text) } + } + } + is MdInline.Link -> { + val target = classifyLink(node.href, baseDir) + if (target is LinkTarget.Inert) { + // Unsupported scheme: plain text, never a click target. + appendNodes(node.children, span) + } else { + val link = LinkAnnotation.Clickable( + tag = "md-link-${annotationIndex++}", + styles = linkStyles(colors), + linkInteractionListener = { onLink(target) }, + ) + withLink(link) { appendNodes(node.children, span) } + } + } + is MdInline.Image -> { + val target = classifyLink(node.src, baseDir) + if (target is LinkTarget.Inert) { + withStyle(span) { append(node.alt.ifBlank { node.src }) } + } else { + val link = LinkAnnotation.Clickable( + tag = "md-image-${annotationIndex++}", + styles = linkStyles(colors), + linkInteractionListener = { onLink(target) }, + ) + withLink(link) { withStyle(span) { append(node.alt.ifBlank { node.src }) } } + } + } + is MdInline.Math -> { + val id = "math-${annotationIndex++}" + val content = measurer.inlineContent(node.tex) + if (content != null) { + inlineContent[id] = content + appendInlineContent(id, node.tex) + } else { + // Never drop the formula: show its source instead. + withStyle(span + SpanStyle(fontFamily = OmpMonoFontFamily)) { append(node.tex) } + } + } + } + } + } + + appendNodes(inlines, SpanStyle(color = colors.text)) +} + +private fun linkStyles(colors: OmpColors): TextLinkStyles = TextLinkStyles( + style = SpanStyle(color = colors.accent, textDecoration = TextDecoration.Underline), + focusedStyle = SpanStyle(color = colors.accent), + hoveredStyle = SpanStyle(color = colors.accentHover), + pressedStyle = SpanStyle(color = colors.accentHover), +) + +/** Exact `agent://` handle or a conservative inline-code file path. */ +internal fun inlineCodeTarget(text: String, baseDir: String?): LinkTarget? { + if (isExactAgentHandle(text)) return LinkTarget.Agent(text) + if (!isInlineCodeFilePath(text)) return null + val file = resolveLocalFileTarget(text, baseDir) ?: return null + return LinkTarget.File(file.path, file.line) +} + +/** Display math and inline math share the native renderer; source never disappears. */ +@Composable +private fun MathFormula(tex: String, modifier: Modifier) { + val colors = LocalOmpColors.current + val renderable = remember(tex) { isLatexRenderable(tex) } + if (!renderable) { + CodeBlock(code = tex, language = "latex", modifier = modifier) + return + } + LatexAutoWrap( + latex = tex, + modifier = modifier, + config = LatexConfig( + fontSize = 18.sp, + theme = latexTheme(colors), + ), + ) +} + +private fun isLatexRenderable(tex: String): Boolean = try { + val parser = com.hrm.latex.parser.IncrementalLatexParser() + parser.append(tex) + parser.getCurrentDocument().children.isNotEmpty() +} catch (_: Exception) { + false +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/MarkdownModel.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/MarkdownModel.kt new file mode 100644 index 000000000..13435ce0d --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/MarkdownModel.kt @@ -0,0 +1,86 @@ +package io.github.kahme247.ompweb.ui.render + +/** + * Parsed markdown used by [MarkdownText]. The model is deliberately small: it + * covers the GFM subset the web renders (`remark-gfm` + sanitized raw HTML) + * and keeps math as source until the renderer draws it. + */ +sealed interface MdInline { + data class Text(val text: String) : MdInline + + /** Code span; the renderer links exact agent handles and file paths. */ + data class Code(val text: String) : MdInline + + data class Strong(val children: List) : MdInline + + data class Em(val children: List) : MdInline + + data class Strike(val children: List) : MdInline + + /** An explicit markdown link; its [href] is classified when rendered. */ + data class Link(val children: List, val href: String) : MdInline + + /** Alt text and target of an image; block-level images are [MdBlock.Image]. */ + data class Image(val alt: String, val src: String) : MdInline + + /** Inline (`$…$`) or inline-occurring display (`$$…$$`) math. */ + data class Math(val tex: String, val display: Boolean) : MdInline + + data object SoftBreak : MdInline + + data object HardBreak : MdInline +} + +enum class MdAlign { None, Left, Center, Right } + +sealed interface MdBlock { + data class Heading(val level: Int, val inlines: List) : MdBlock + + data class Paragraph(val inlines: List) : MdBlock + + data class CodeFence(val language: String?, val code: String) : MdBlock + + data class MathBlock(val tex: String) : MdBlock + + data class Quote(val blocks: List) : MdBlock + + data class ListBlock( + val ordered: Boolean, + val start: Int, + val items: List, + ) : MdBlock + + data class Table( + val header: List>, + val align: List, + val rows: List>>, + ) : MdBlock + + data object Rule : MdBlock + + data class Image(val alt: String, val src: String) : MdBlock + + /** Consecutive blocks parsed from one source run (text + images in order). */ + data class Group(val blocks: List) : MdBlock +} + +data class MdListItem( + /** `null` = not a task item; otherwise the checkbox state. */ + val task: Boolean?, + val blocks: List, +) + +/** One top-level block plus the source range it was parsed from. */ +internal data class BlockSpan(val block: MdBlock, val start: Int, val end: Int) + +/** + * A parsed transcript message. [starts]/[ends] are character offsets of the + * top-level blocks and let [MarkdownDocumentCache] re-parse only the tail + * while a message streams. + */ +class MarkdownDocument( + val text: String, + val blocks: List, + internal val starts: List, + internal val ends: List, +) diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/MarkdownParser.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/MarkdownParser.kt new file mode 100644 index 000000000..9a0661e1f --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/MarkdownParser.kt @@ -0,0 +1,1146 @@ +package io.github.kahme247.ompweb.ui.render + +import java.util.Locale + +/** + * Markdown parser for [MarkdownText]. + * + * Ports the GFM subset the web renders (headings, lists, task lists, tables, + * quotes, code fences, images, hr) plus the two transforms that must happen + * before block parsing: [normalizeDisplayMath] and the GitHub/agent/file + * linkification of `lib/github-refs.ts`, `lib/agent-links.ts` and + * `lib/file-links.ts`. + * + * The parser is pure Kotlin so unit tests can drive it on the JVM. + */ +object MarkdownParser { + + // --------------------------------------------------------------------- + // Document parsing + // --------------------------------------------------------------------- + + /** + * Files written on Windows (and text the server reads verbatim) use CRLF. + * The block parser splits on `\n`, so a trailing `\r` stops every anchored + * marker from matching and a whole document degrades to plain text — + * headings, lists and code fences included. Normalize like the web + * renderer does, before any offset is computed. + */ + fun normalizeLineEndings(markdown: String): String = + if (markdown.indexOf('\r') < 0) { + markdown + } else { + markdown.replace("\r\n", "\n").replace('\r', '\n') + } + + fun parse(markdown: String, githubRepo: GithubRepoRef?): MarkdownDocument { + val normalized = normalizeDisplayMath(normalizeLineEndings(markdown)) + val spans = parseSpans(normalized, 0, githubRepo) + return MarkdownDocument( + text = normalized, + blocks = spans.map { it.block }, + starts = spans.map { it.start }, + ends = spans.map { it.end }, + ) + } + + /** + * Re-parses only the tail that starts at [fromOffset]. Used by + * [MarkdownDocumentCache] while a message streams; [githubRepo] must be + * the same one the prefix was parsed with. + */ + internal fun parseTail(markdown: String, fromOffset: Int, githubRepo: GithubRepoRef?): List = + parseSpans(markdown, fromOffset, githubRepo) + + /** + * Whether the last line of [text] would start a block on its own. + * + * A paragraph never contains such a line, so appending plain text that + * turns the current last line into one ("1" + "." = an empty ordered list + * item) changes the block structure; [MarkdownDocumentCache] then has to + * re-parse instead of extending the paragraph in place. + */ + internal fun lastLineStartsBlock(text: String): Boolean { + val lineStart = text.lastIndexOf('\n') + 1 + val line = text.substring(lineStart) + if (line.isBlank()) return false + if (line.trim() == "$$") return true + if (FENCE_LINE.matches(line) || ATX_HEADING.matches(line) || RULE.matches(line)) return true + if (QUOTE_LINE.containsMatchIn(line)) return true + if (LIST_LINE.matches(line)) return true + if (lineStart > 0 && TABLE_DELIMITER.matches(line)) { + val before = text.substring(0, lineStart - 1) + val previousStart = before.lastIndexOf('\n') + 1 + if (before.substring(previousStart).contains('|')) return true + } + return false + } + + private fun parseSpans(text: String, fromOffset: Int, githubRepo: GithubRepoRef?): List { + if (fromOffset >= text.length) return emptyList() + val parser = BlockParser(text.substring(fromOffset), githubRepo) + return parser.parse().map { span -> + span.copy(start = span.start + fromOffset, end = span.end + fromOffset) + } + } + + // --------------------------------------------------------------------- + // Inline parsing (public for tests) + // --------------------------------------------------------------------- + + fun parseInlines(source: String, githubRepo: GithubRepoRef? = null): List = + linkify(parseInlinesInternal(source, githubRepo), githubRepo) + + // --------------------------------------------------------------------- + // Display-math normalization (port of lib/markdown.ts) + // --------------------------------------------------------------------- + + /** + * Converts `\[…\]` display delimiters and glued `$$` fences into the + * three-line `$$` form block parsing understands, and `\(…\)` into `$…$`. + * Never touches fenced code, inline code, raw code tags, escaped + * delimiters or link destinations. + */ + fun normalizeDisplayMath(markdown: String): String { + val lineBreak = if (markdown.contains("\r\n")) "\r\n" else "\n" + val lines = markdown.split(Regex("\r?\n")) + val normalized = ArrayList(lines.size) + var fence: Pair? = null + var inlineCodeMarkerSize = 0 + var rawCodeTag: String? = null + val unmatchedDisplayMathUntil = HashMap() + + var index = 0 + while (index < lines.size) { + val line = lines[index] + + if (rawCodeTag != null) { + normalized.add(line) + if (Regex("", RegexOption.IGNORE_CASE).containsMatchIn(line)) rawCodeTag = null + index++ + continue + } + + val fenceMatch = FENCE_OPEN.matchEntire(line) + if (fenceMatch != null) { + val marker = fenceMatch.groups[1]!!.value[0] + val size = fenceMatch.groups[1]!!.value.length + if (fence == null) { + fence = marker to size + } else if (marker == fence.first && size >= fence.second) { + fence = null + } + inlineCodeMarkerSize = 0 + normalized.add(line) + index++ + continue + } + + if (fence != null) { + normalized.add(line) + index++ + continue + } + + val rawCodeOpen = RAW_CODE_OPEN.find(line) + if (rawCodeOpen != null) { + val tag = rawCodeOpen.groups[1]!!.value.lowercase(Locale.ROOT) + val remainder = line.substring(rawCodeOpen.range.last + 1) + if (!Regex("", RegexOption.IGNORE_CASE).containsMatchIn(remainder)) rawCodeTag = tag + inlineCodeMarkerSize = 0 + normalized.add(line) + index++ + continue + } + + if (INDENTED_OR_BLANK.containsMatchIn(line) || line.isBlank()) { + inlineCodeMarkerSize = 0 + normalized.add(line) + index++ + continue + } + + if (inlineCodeMarkerSize != 0 || line.contains('`')) { + inlineCodeMarkerSize = updateInlineCodeMarker(line, inlineCodeMarkerSize) + normalized.add(line) + index++ + continue + } + + val bracketOneLine = BRACKET_DISPLAY_ONE_LINE.matchEntire(line) + if (bracketOneLine != null) { + val math = bracketOneLine.groups[2]!!.value.trim() + if (math.isNotEmpty()) { + val indent = bracketOneLine.groups[1]!!.value + normalized.add("$indent$$") + normalized.add("$indent$math") + normalized.add("$indent$$") + index++ + continue + } + } + + val bracketStart = BRACKET_DISPLAY_START.matchEntire(line) + if (bracketStart != null) { + val closingIndex = findBracketDisplayClose(lines, index + 1) + if (closingIndex != -1) { + val indent = bracketStart.groups[1]!!.value + normalized.add("$indent$$") + for (j in index + 1 until closingIndex) { + normalized.add(indentDisplayMathContent(lines[j], indent)) + } + normalized.add("$indent$$") + index = closingIndex + 1 + continue + } + } + + val displayOneLine = DISPLAY_ONE_LINE.matchEntire(line) + if (displayOneLine != null) { + val math = displayOneLine.groups[2]!!.value.trim() + if (math.isNotEmpty()) { + val indent = displayOneLine.groups[1]!!.value + normalized.add("$indent$$") + normalized.add("$indent$math") + normalized.add("$indent$$") + index++ + continue + } + } + + // Models may glue display fences to the first or last formula line. + val displayMultiLine = DISPLAY_MULTI_LINE.matchEntire(line) + if (displayMultiLine != null) { + val indent = displayMultiLine.groups[1]!!.value + val firstLine = displayMultiLine.groups[2]!!.value.trimEnd() + // Keep `$$x$$ and text` inline; it is not a display block opener. + if (firstLine.isNotEmpty() && !firstLine.contains("$$")) { + val closing = findDisplayMathClose(lines, index + 1, indent, unmatchedDisplayMathUntil) + if (closing != null) { + normalized.add("$indent$$") + normalized.add("$indent$firstLine") + for (j in index + 1 until closing.index) { + normalized.add(indentDisplayMathContent(lines[j], indent)) + } + if (closing.content.isNotEmpty()) normalized.add("$indent${closing.content}") + normalized.add("$indent$$") + index = closing.index + 1 + continue + } + } + } + + val bareOpen = DISPLAY_BARE_OPEN.matchEntire(line) + if (bareOpen != null) { + val indent = bareOpen.groups[1]!!.value + val closing = findDisplayMathClose(lines, index + 1, indent, unmatchedDisplayMathUntil) + if (closing != null && (closing.glued || indent.isNotEmpty())) { + normalized.add("$indent$$") + for (j in index + 1 until closing.index) { + normalized.add(indentDisplayMathContent(lines[j], indent)) + } + if (closing.content.isNotEmpty()) normalized.add("$indent${closing.content}") + normalized.add("$indent$$") + index = closing.index + 1 + continue + } + } + + normalized.add(normalizeInlineLatexMath(line)) + index++ + } + + return normalized.joinToString(lineBreak) + } + + private data class DisplayMathClose(val index: Int, val content: String, val glued: Boolean) + + private fun findDisplayMathClose( + lines: List, + startIndex: Int, + indent: String, + unmatchedUntil: MutableMap, + ): DisplayMathClose? { + val known = unmatchedUntil[indent] + if (known != null && startIndex < known) return null + + for (index in startIndex until lines.size) { + val line = lines[index] + if (isDisplayMathFence(line, indent)) return DisplayMathClose(index, "", false) + if (isDisplayMathBlockBoundary(line) || isDisplayMathOpeningLine(line)) { + unmatchedUntil[indent] = index + return null + } + val content = getDisplayMathGluedCloseContent(line, indent) + if (content != null) return DisplayMathClose(index, content, true) + } + + unmatchedUntil[indent] = lines.size + return null + } + + private fun isDisplayMathFence(line: String, indent: String): Boolean = + if (indent.isEmpty()) { + Regex("^ {0,3}\\$\\$\\s*$").matches(line) + } else { + line.startsWith(indent) && Regex("^\\$\\$\\s*$").matches(line.substring(indent.length)) + } + + private fun getDisplayMathGluedCloseContent(line: String, indent: String): String? { + if (!line.startsWith(indent)) return null + val match = Regex("^(.+?)\\$\\$\\s*$").matchEntire(line.substring(indent.length)) ?: return null + val content = match.groups[1]!!.value.trimEnd() + return if (content.isNotEmpty() && !content.contains("$$")) content else null + } + + private fun isDisplayMathOpeningLine(line: String): Boolean = + Regex("^ {0,3}\\$\\$(?:\\S|[ \\t]+\\S)").containsMatchIn(line) + + private fun isDisplayMathBlockBoundary(line: String): Boolean = + Regex("^ {0,3}(`{3,}|~{3,})").containsMatchIn(line) || + Regex("^[ \\t]*(?:[-+*]|\\d{1,9}[.)])(?:[ \\t]+|$)").containsMatchIn(line) || + Regex("^ {0,3}#{1,6}(?:[ \\t]+|$)").containsMatchIn(line) || + Regex("^ {0,3}>").containsMatchIn(line) || + Regex("<(code|pre|script|style)\\b", RegexOption.IGNORE_CASE).containsMatchIn(line) + + private fun indentDisplayMathContent(line: String, indent: String): String { + if (indent.isEmpty() || line.isEmpty() || line.startsWith("\t")) return line + val leadingSpaces = line.takeWhile { it == ' ' }.length + if (leadingSpaces >= indent.length) return line + return indent.substring(leadingSpaces) + line + } + + private fun findBracketDisplayClose(lines: List, startIndex: Int): Int { + for (index in startIndex until lines.size) { + val line = lines[index] + if (Regex("^ {0,3}\\\\\\][ \\t]*$").matches(line)) return index + if ( + Regex("^ {0,3}(`{3,}|~{3,})").containsMatchIn(line) || + Regex("^ {0,3}\\\\\\[[ \\t]*$").matches(line) || + Regex("<(code|pre|script|style)\\b", RegexOption.IGNORE_CASE).containsMatchIn(line) + ) { + return -1 + } + } + return -1 + } + + private fun updateInlineCodeMarker(line: String, initialMarkerSize: Int): Int { + var markerSize = initialMarkerSize + var cursor = 0 + while (cursor < line.length) { + if (line[cursor] != '`') { + cursor++ + continue + } + var end = cursor + 1 + while (end < line.length && line[end] == '`') end++ + val runSize = end - cursor + if (markerSize == 0) markerSize = runSize else if (runSize == markerSize) markerSize = 0 + cursor = end + } + return markerSize + } + + private fun normalizeInlineLatexMath(line: String): String { + if ( + Regex("^\\s{0,3}\\[[^]]+\\]:").containsMatchIn(line) || + Regex("]\\s*\\(").containsMatchIn(line) || + Regex("<(?:!--|/?[A-Za-z][^>]*>)").containsMatchIn(line) || + Regex("\\b(?:https?|file|mailto):", RegexOption.IGNORE_CASE).containsMatchIn(line) || + Regex("\\b[A-Za-z]:\\\\").containsMatchIn(line) + ) { + return line + } + return INLINE_LATEX_PAREN.replace(line) { match -> + val math = match.groups[1]!!.value + if (math.trim().isNotEmpty()) "$$math$" else match.value + } + } + + // --------------------------------------------------------------------- + // Regular expressions + // --------------------------------------------------------------------- + + private val FENCE_OPEN = Regex("^ {0,3}(`{3,}|~{3,})") + private val RAW_CODE_OPEN = Regex("<(code|pre|script|style)\\b", RegexOption.IGNORE_CASE) + private val INDENTED_OR_BLANK = Regex("^(?: {4}|\\t)") + private val BRACKET_DISPLAY_ONE_LINE = Regex("^([ ]{0,3})\\\\\\[[ \\t]*(.+?)[ \\t]*\\\\\\][ \\t]*$") + private val BRACKET_DISPLAY_START = Regex("^([ ]{0,3})\\\\\\[[ \\t]*$") + private val DISPLAY_ONE_LINE = Regex("^([ \\t]{0,3})\\$\\$(.+)\\$\\$[ \\t]*$") + private val DISPLAY_MULTI_LINE = Regex("^([ \\t]{0,3})\\$\\$(.+)$") + private val DISPLAY_BARE_OPEN = Regex("^([ \\t]{0,3})\\$\\$\\s*$") + private val INLINE_LATEX_PAREN = Regex("(? = text.split('\n') + private val starts: IntArray = IntArray(lines.size).also { offsets -> + var offset = 0 + for (i in lines.indices) { + offsets[i] = offset + offset += lines[i].length + 1 + } + } + + fun parse(): List { + val out = ArrayList() + var i = 0 + while (i < lines.size) { + if (lines[i].isBlank()) { + i++ + continue + } + val blockStart = i + val parsed = parseBlock(i) + val next = parsed.second + val block = parsed.first + val end = if (next <= blockStart) starts[blockStart] + lines[blockStart].length else lineEnd(next - 1) + out.add(BlockSpan(block, starts[blockStart], end)) + i = if (next <= blockStart) blockStart + 1 else next + } + return out + } + + private fun lineEnd(index: Int): Int = starts[index] + lines[index].length + + private fun parseBlock(from: Int): Pair { + val line = lines[from] + + val fenceMatch = FENCE_LINE.matchEntire(line) + if (fenceMatch != null) return parseFence(from, fenceMatch) + + if (line.trim() == "$$") return parseMathBlock(from) + + val heading = ATX_HEADING.matchEntire(line) + if (heading != null) { + val level = heading.groups[1]!!.value.length + val content = heading.groups[2]?.value.orEmpty() + .trimEnd() + .replace(Regex("[ \\t]+#+$"), "") + .trimEnd() + return MdBlock.Heading(level, inline(content)) to from + 1 + } + + if (RULE.matches(line)) return MdBlock.Rule to from + 1 + + if (QUOTE_LINE.containsMatchIn(line)) return parseQuote(from) + + if (LIST_LINE.matches(line)) return parseList(from) + + if (isTableStart(from)) return parseTable(from) + + if (INDENTED_CODE.containsMatchIn(line)) return parseIndentedCode(from) + + return parseParagraph(from) + } + + private fun parseFence(from: Int, opening: MatchResult): Pair { + val marker = opening.groups[1]!!.value + val info = opening.groups[2]?.value.orEmpty().trim() + val language = info.split(Regex("\\s+")).firstOrNull()?.takeIf { it.isNotEmpty() } + val code = StringBuilder() + var i = from + 1 + var closed = false + while (i < lines.size) { + val close = FENCE_CLOSE.matchEntire(lines[i]) + if (close != null) { + val closeMarker = close.groups[1]!!.value + if (closeMarker[0] == marker[0] && closeMarker.length >= marker.length) { + closed = true + i++ + break + } + } + if (code.isNotEmpty()) code.append('\n') + code.append(lines[i]) + i++ + } + if (!closed) { + // An unclosed fence while streaming is still a code block. + return MdBlock.CodeFence(language, code.toString()) to i + } + return MdBlock.CodeFence(language, code.toString()) to i + } + + private fun parseMathBlock(from: Int): Pair { + val math = StringBuilder() + var i = from + 1 + while (i < lines.size && lines[i].trim() != "$$") { + if (math.isNotEmpty()) math.append('\n') + math.append(lines[i]) + i++ + } + val next = if (i < lines.size) i + 1 else i + return MdBlock.MathBlock(math.toString().trim()) to next + } + + private fun parseQuote(from: Int): Pair { + val body = ArrayList() + var i = from + while (i < lines.size) { + val match = QUOTE_LINE.matchEntire(lines[i]) + if (match != null) { + body.add(match.groups[1]!!.value) + i++ + continue + } + if (lines[i].isBlank() && i + 1 < lines.size && QUOTE_LINE.containsMatchIn(lines[i + 1])) { + body.add("") + i++ + continue + } + break + } + // Trailing blank lines only separated the quote from the next block. + while (body.isNotEmpty() && body.last().isBlank()) body.removeAt(body.size - 1) + val nested = BlockParser(body.joinToString("\n"), githubRepo).parse().map { it.block } + return MdBlock.Quote(nested) to i + } + + private fun parseList(from: Int): Pair { + val first = LIST_LINE.matchEntire(lines[from])!! + val firstIndent = first.groups[1]!!.value.length + val firstMarker = first.groups[2]!!.value + val ordered = firstMarker[0].isDigit() + val startNumber = first.groups[3]?.value?.toIntOrNull() ?: 1 + val items = ArrayList() + var i = from + + while (i < lines.size) { + val match = LIST_LINE.matchEntire(lines[i]) ?: break + val indent = match.groups[1]!!.value.length + val marker = match.groups[2]!!.value + if (indent != firstIndent || marker[0].isDigit() != ordered) break + + val separator = match.groups[4]!!.value + val contentIndent = indent + marker.length + separator.length + val body = ArrayList() + body.add(match.groups[5]!!.value) + var j = i + 1 + while (j < lines.size) { + val current = lines[j] + if (current.isBlank()) { + body.add("") + j++ + continue + } + val lead = current.takeWhile { it == ' ' }.length + if (lead >= contentIndent) { + body.add(current.drop(contentIndent)) + j++ + continue + } + // A sibling item ends this item. + val nextItem = LIST_LINE.matchEntire(current) + if (nextItem != null && nextItem.groups[1]!!.value.length == firstIndent) break + // Lazy paragraph continuation. + if (body.lastOrNull()?.isNotBlank() == true && !startsBlock(current, j)) { + body.add(current.trimStart()) + j++ + continue + } + break + } + while (body.isNotEmpty() && body.last().isBlank()) body.removeAt(body.size - 1) + + var nestedBlocks = BlockParser(body.joinToString("\n"), githubRepo).parse().map { it.block } + var task: Boolean? = null + val firstBlock = nestedBlocks.firstOrNull() + if (firstBlock is MdBlock.Paragraph) { + // Only a leading `[ ]`/`[x]` text node counts as a task marker. + val leading = firstBlock.inlines.firstOrNull() as? MdInline.Text + val markerMatch = leading?.let { TASK_PREFIX.matchAt(it.text, 0) } + if (leading != null && markerMatch != null) { + task = markerMatch.groups[1]!!.value.equals("x", ignoreCase = true) + val rest = leading.text.substring(markerMatch.range.last + 1) + val inlines = if (rest.isEmpty()) { + firstBlock.inlines.drop(1) + } else { + listOf(MdInline.Text(rest)) + firstBlock.inlines.drop(1) + } + nestedBlocks = if (inlines.isEmpty()) nestedBlocks.drop(1) else listOf(MdBlock.Paragraph(inlines)) + nestedBlocks.drop(1) + } + } + items.add(MdListItem(task = task, blocks = nestedBlocks)) + + i = j + while (i < lines.size && lines[i].isBlank()) i++ + } + + return MdBlock.ListBlock(ordered = ordered, start = startNumber, items = items) to i + } + + private fun parseTable(from: Int): Pair { + val header = splitTableRow(lines[from]) + val delimiter = splitTableRow(lines[from + 1]) + val align = delimiter.map { cell -> + val left = cell.startsWith(":") + val right = cell.endsWith(":") + when { + left && right -> MdAlign.Center + right -> MdAlign.Right + left -> MdAlign.Left + else -> MdAlign.None + } + } + val headerCells = normalizeCells(header, align.size).map { inline(it) } + val rows = ArrayList>>() + var i = from + 2 + while (i < lines.size) { + val line = lines[i] + if (line.isBlank() || !line.contains('|')) break + rows.add(normalizeCells(splitTableRow(line), align.size).map { inline(it) }) + i++ + } + return MdBlock.Table(headerCells, align, rows) to i + } + + private fun parseIndentedCode(from: Int): Pair { + val code = StringBuilder() + var i = from + while (i < lines.size) { + val line = lines[i] + if (line.isBlank()) { + code.append('\n') + i++ + continue + } + if (!INDENTED_CODE.containsMatchIn(line)) break + code.append(line.removePrefix(" ").removePrefix("\t")) + code.append('\n') + i++ + } + return MdBlock.CodeFence(null, code.toString().trimEnd('\n')) to i + } + + private fun parseParagraph(from: Int): Pair { + val content = ArrayList() + var i = from + while (i < lines.size) { + val line = lines[i] + if (line.isBlank()) break + if (content.isNotEmpty() && (startsBlock(line, i) || SETEXT_1.matches(line) || SETEXT_2.matches(line))) break + content.add(line) + i++ + } + + if (content.size == 1) { + val next = if (i < lines.size) lines[i] else null + if (next != null && SETEXT_1.matches(next)) { + return MdBlock.Heading(1, inline(content[0].trim())) to i + 1 + } + if (next != null && SETEXT_2.matches(next) && content[0].isNotBlank()) { + return MdBlock.Heading(2, inline(content[0].trim())) to i + 1 + } + } + + val source = content.joinToString("\n") + val inlines = inline(source) + val blocks = splitImages(inlines) + if (blocks.size == 1) return blocks[0] to i + // Mixed text and images: emit a group block that renders in order. + return MdBlock.Group(blocks) to i + } + + private fun splitImages(inlines: List): List { + if (inlines.none { it is MdInline.Image }) { + return if (inlines.isEmpty()) emptyList() else listOf(MdBlock.Paragraph(inlines)) + } + val result = ArrayList() + var current = ArrayList() + fun flush() { + if (current.isNotEmpty()) { + result.add(MdBlock.Paragraph(trimInlines(current))) + current = ArrayList() + } + } + for (inline in inlines) { + if (inline is MdInline.Image) { + flush() + result.add(MdBlock.Image(inline.alt, inline.src)) + } else { + current.add(inline) + } + } + flush() + return result + } + + private fun trimInlines(inlines: List): List { + val copy = inlines.toMutableList() + while (copy.isNotEmpty() && (copy.first() as? MdInline.Text)?.text?.isBlank() == true) copy.removeAt(0) + while (copy.isNotEmpty() && (copy.last() as? MdInline.Text)?.text?.isBlank() == true) copy.removeAt(copy.size - 1) + return copy + } + + private fun inline(source: String): List = MarkdownParser.parseInlines(source, githubRepo) + + private fun isTableStart(index: Int): Boolean { + if (index + 1 >= lines.size) return false + if (!lines[index].contains('|')) return false + val candidate = lines[index + 1] + if (!TABLE_DELIMITER.matches(candidate)) return false + return lines[index].count { it == '|' } >= 1 + } + + private fun startsBlock(line: String, index: Int): Boolean { + if (FENCE_LINE.matches(line)) return true + if (ATX_HEADING.matches(line)) return true + if (RULE.matches(line)) return true + if (QUOTE_LINE.containsMatchIn(line)) return true + if (LIST_LINE.matches(line)) return true + if (line.trim() == "$$") return true + if (isTableStart(index)) return true + return false + } + } + + // --------------------------------------------------------------------- + // Inline scanner + // --------------------------------------------------------------------- + + private fun parseInlinesInternal(source: String, githubRepo: GithubRepoRef?): List { + val nodes = ArrayList() + val buffer = StringBuilder() + var i = 0 + val n = source.length + + fun flush() { + if (buffer.isNotEmpty()) { + nodes.add(MdInline.Text(buffer.toString())) + buffer.setLength(0) + } + } + + while (i < n) { + val c = source[i] + when { + c == '\\' && i + 1 < n && source[i + 1] == '\n' -> { + flush() + nodes.add(MdInline.HardBreak) + i += 2 + } + c == '\\' && i + 1 < n && source[i + 1] in ESCAPABLE -> { + buffer.append(source[i + 1]) + i += 2 + } + c == '\n' -> { + val hard = buffer.endsWith(" ") + flush() + nodes.add(if (hard) MdInline.HardBreak else MdInline.SoftBreak) + i++ + } + c == '`' -> { + val run = countRun(source, i, '`') + val close = findCodeSpanClose(source, i + run, run) + if (close < 0) { + buffer.append(source, i, i + run) + i += run + } else { + var content = source.substring(i + run, close).replace('\n', ' ') + if (content.length >= 2 && content.startsWith(" ") && content.endsWith(" ") && content.isNotBlank()) { + content = content.substring(1, content.length - 1) + } + flush() + nodes.add(MdInline.Code(content)) + i = close + run + } + } + c == '!' && i + 1 < n && source[i + 1] == '[' -> { + val parsed = parseLinkOrImage(source, i, image = true, githubRepo) + if (parsed != null) { + flush(); nodes.add(parsed.first); i = parsed.second + } else { + buffer.append(c); i++ + } + } + c == '[' -> { + val parsed = parseLinkOrImage(source, i, image = false, githubRepo) + if (parsed != null) { + flush(); nodes.add(parsed.first); i = parsed.second + } else { + buffer.append(c); i++ + } + } + c == '<' -> { + val end = source.indexOf('>', i + 1) + if (end > i) { + val inner = source.substring(i + 1, end) + when { + inner.startsWith("http://") || inner.startsWith("https://") -> { + flush() + nodes.add(MdInline.Link(listOf(MdInline.Text(inner)), inner)) + i = end + 1 + } + inner.startsWith("mailto:") -> { + flush() + nodes.add(MdInline.Link(listOf(MdInline.Text(inner.removePrefix("mailto:"))), inner)) + i = end + 1 + } + EMAIL_ADDRESS.matches(inner) -> { + flush() + nodes.add(MdInline.Link(listOf(MdInline.Text(inner)), "mailto:$inner")) + i = end + 1 + } + inner.equals("br", ignoreCase = true) || inner.equals("br/", ignoreCase = true) -> { + flush() + nodes.add(MdInline.HardBreak) + i = end + 1 + } + inner.startsWith("!--") || HTML_TAG.matches(inner) -> { + // Raw HTML is sanitized away, like rehype-sanitize. + i = end + 1 + } + else -> { + buffer.append(c); i++ + } + } + } else { + buffer.append(c); i++ + } + } + c == '$' -> { + val parsed = parseMath(source, i) + if (parsed != null) { + flush(); nodes.add(parsed.first); i = parsed.second + } else { + buffer.append(c); i++ + } + } + c == '*' || c == '_' -> { + val parsed = parseEmphasis(source, i, c, githubRepo) + if (parsed != null) { + flush(); nodes.add(parsed.first); i = parsed.second + } else { + buffer.append(c); i++ + } + } + c == '~' && i + 1 < n && source[i + 1] == '~' -> { + val close = findInlineDelimiter(source, i + 2, "~~") + if (close > i + 2) { + val inner = parseInlinesInternal(source.substring(i + 2, close), githubRepo) + flush(); nodes.add(MdInline.Strike(inner)); i = close + 2 + } else { + buffer.append(c); i++ + } + } + else -> { + buffer.append(c); i++ + } + } + } + flush() + return nodes + } + + private fun parseEmphasis( + source: String, + start: Int, + marker: Char, + githubRepo: GithubRepoRef?, + ): Pair? { + val n = source.length + val isDouble = start + 1 < n && source[start + 1] == marker + if (isDouble) { + if (start + 2 >= n || source[start + 2].isWhitespace()) return null + val close = findInlineDelimiter(source, start + 2, "$marker$marker") + if (close <= start + 2) return null + val inner = parseInlinesInternal(source.substring(start + 2, close), githubRepo) + return MdInline.Strong(inner) to close + 2 + } + if (marker == '_' && start > 0 && isWordChar(source[start - 1])) return null + if (start + 1 >= n || source[start + 1].isWhitespace()) return null + val close = findInlineDelimiter(source, start + 1, marker.toString()) + if (close <= start + 1) return null + if (source[close - 1].isWhitespace()) return null + if (marker == '_' && close + 1 < n && isWordChar(source[close + 1])) return null + val inner = parseInlinesInternal(source.substring(start + 1, close), githubRepo) + return MdInline.Em(inner) to close + 1 + } + + private fun parseLinkOrImage( + source: String, + start: Int, + image: Boolean, + githubRepo: GithubRepoRef?, + ): Pair? { + val labelStart = if (image) start + 2 else start + 1 + val labelEnd = findMatchingBracket(source, labelStart) ?: return null + if (labelEnd + 1 >= source.length || source[labelEnd + 1] != '(') return null + val destEnd = findMatchingParen(source, labelEnd + 2) ?: return null + val rawDestination = source.substring(labelEnd + 2, destEnd).trim() + val href = parseDestination(rawDestination) ?: return null + val label = source.substring(labelStart, labelEnd) + val next = destEnd + 1 + if (image) { + return MdInline.Image(altText(label), href) to next + } + val children = parseInlinesInternal(label, githubRepo) + return MdInline.Link(children, href) to next + } + + private fun parseDestination(raw: String): String? { + if (raw.isEmpty()) return null + if (raw.startsWith("<")) { + val end = raw.indexOf('>') + if (end > 1) return raw.substring(1, end).trim().ifEmpty { null } + } + val token = raw.split(Regex("[ \\t]")).firstOrNull().orEmpty() + return token.ifEmpty { null } + } + + private fun altText(label: String): String = + plainText(parseInlinesInternal(label, null)).trim() + + private fun parseMath(source: String, start: Int): Pair? { + val n = source.length + if (start + 1 < n && source[start + 1] == '$') { + val close = source.indexOf("$$", start + 2) + if (close > start + 2) { + return MdInline.Math(source.substring(start + 2, close).trim(), true) to close + 2 + } + return null + } + if (start + 1 >= n || source[start + 1].isWhitespace() || source[start + 1] == '$') return null + var i = start + 1 + while (i < n) { + when (source[i]) { + '\\' -> i += 2 + '\n' -> return null + '$' -> { + val closingFollowedByDigit = i + 1 < n && source[i + 1].isDigit() + if (i > start + 1 && !source[i - 1].isWhitespace() && !closingFollowedByDigit) { + return MdInline.Math(source.substring(start + 1, i).trim(), false) to i + 1 + } + return null + } + else -> i++ + } + } + return null + } + + // --------------------------------------------------------------------- + // Linkification of plain text (GitHub refs, agent handles, bare URLs) + // --------------------------------------------------------------------- + + private data class TextMatch(val start: Int, val end: Int, val inline: MdInline, val priority: Int) + + private fun linkify(nodes: List, repo: GithubRepoRef?): List = + nodes.flatMap { node -> + when (node) { + is MdInline.Text -> linkifyText(node.text, repo) + is MdInline.Strong -> listOf(MdInline.Strong(linkify(node.children, repo))) + is MdInline.Em -> listOf(MdInline.Em(linkify(node.children, repo))) + is MdInline.Strike -> listOf(MdInline.Strike(linkify(node.children, repo))) + // Existing links stay untouched, exactly like mdast-util-find-and-replace's `ignore`. + else -> listOf(node) + } + } + + private fun linkifyText(text: String, repo: GithubRepoRef?): List { + if (text.isEmpty()) return emptyList() + val matches = ArrayList() + + for (ref in findGithubRefs(text, repo)) { + matches.add( + TextMatch( + ref.start, + ref.end, + MdInline.Link(listOf(MdInline.Text(ref.text)), githubUrl(ref.target)), + priority = 0, + ), + ) + } + for (range in findAgentHandles(text)) { + val handle = text.substring(range.first, range.last + 1) + matches.add(TextMatch(range.first, range.last + 1, MdInline.Link(listOf(MdInline.Text(handle)), handle), priority = 1)) + } + for (match in BARE_URL.findAll(text)) { + var url = match.value + while (url.isNotEmpty() && url.last() in TRAILING_URL_PUNCTUATION) { + url = url.dropLast(1) + } + if (url.length <= 4) continue + val href = if (url.startsWith("www.", ignoreCase = true)) "https://$url" else url + matches.add(TextMatch(match.range.first, match.range.first + url.length, MdInline.Link(listOf(MdInline.Text(url)), href), priority = 2)) + } + + if (matches.isEmpty()) return listOf(MdInline.Text(text)) + + matches.sortWith(compareBy { it.start }.thenBy { it.priority }) + val accepted = ArrayList() + var coveredUntil = -1 + for (match in matches) { + if (match.start < coveredUntil) continue + accepted.add(match) + coveredUntil = match.end + } + + val out = ArrayList() + var cursor = 0 + for (match in accepted) { + if (match.start > cursor) out.add(MdInline.Text(text.substring(cursor, match.start))) + out.add(match.inline) + cursor = match.end + } + if (cursor < text.length) out.add(MdInline.Text(text.substring(cursor))) + return out + } + + private fun githubUrl(target: LinkTarget.GithubIssue): String = + "https://github.com/${target.owner}/${target.repo}/issues/${target.number}" + + // --------------------------------------------------------------------- + // Small helpers + // --------------------------------------------------------------------- + + private fun countRun(source: String, start: Int, char: Char): Int { + var end = start + while (end < source.length && source[end] == char) end++ + return end - start + } + + private fun findCodeSpanClose(source: String, from: Int, run: Int): Int { + var i = from + while (i < source.length) { + if (source[i] == '`') { + val size = countRun(source, i, '`') + if (size == run) return i + i += size + } else { + i++ + } + } + return -1 + } + + private fun findInlineDelimiter(source: String, from: Int, delimiter: String): Int { + var i = from + while (i < source.length) { + when { + source[i] == '\\' -> i += 2 + source[i] == '`' -> { + val run = countRun(source, i, '`') + val close = findCodeSpanClose(source, i + run, run) + i = if (close < 0) i + run else close + run + } + source.startsWith(delimiter, i) -> return i + else -> i++ + } + } + return -1 + } + + private fun findMatchingBracket(source: String, from: Int): Int? { + var depth = 1 + var i = from + while (i < source.length) { + when (source[i]) { + '\\' -> i++ + '[' -> depth++ + ']' -> { + depth-- + if (depth == 0) return i + } + } + i++ + } + return null + } + + private fun findMatchingParen(source: String, from: Int): Int? { + var depth = 1 + var i = from + while (i < source.length) { + when (source[i]) { + '\\' -> i++ + '(' -> depth++ + ')' -> { + depth-- + if (depth == 0) return i + } + '\n' -> { + // A link destination never spans a line. + if (depth == 1) return null + } + } + i++ + } + return null + } + + private fun isWordChar(c: Char): Boolean = c.isLetterOrDigit() || c == '_' + + internal fun plainText(nodes: List): String = buildString { + fun appendAll(list: List) { + for (node in list) { + when (node) { + is MdInline.Text -> append(node.text) + is MdInline.Code -> append(node.text) + is MdInline.Strong -> appendAll(node.children) + is MdInline.Em -> appendAll(node.children) + is MdInline.Strike -> appendAll(node.children) + is MdInline.Link -> appendAll(node.children) + is MdInline.Image -> append(node.alt) + is MdInline.Math -> append(node.tex) + MdInline.SoftBreak -> append(' ') + MdInline.HardBreak -> append('\n') + } + } + } + appendAll(nodes) + } + + private fun splitTableRow(line: String): List { + val trimmed = line.trim().removePrefix("|").removeSuffix("|") + val cells = ArrayList() + val current = StringBuilder() + var escaped = false + for (c in trimmed) { + when { + escaped -> { + current.append(c) + escaped = false + } + c == '\\' -> escaped = true + c == '|' -> { + cells.add(current.toString().trim()) + current.setLength(0) + } + else -> current.append(c) + } + } + cells.add(current.toString().trim()) + return cells + } + + private fun normalizeCells(cells: List, count: Int): List { + if (cells.size == count) return cells + val result = ArrayList(count) + for (i in 0 until count) result.add(cells.getOrElse(i) { "" }) + return result + } + + private val FENCE_LINE = Regex("^ {0,3}(`{3,}|~{3,})(.*)$") + private val FENCE_CLOSE = Regex("^ {0,3}(`{3,}|~{3,})[ \\t]*$") + private val ATX_HEADING = Regex("^ {0,3}(#{1,6})(?:[ \\t]+(.*))?$") + private val RULE = Regex("^ {0,3}(?:(?:\\*[ \\t]*){3,}|(?:-[ \\t]*){3,}|(?:_[ \\t]*){3,})$") + private val QUOTE_LINE = Regex("^ {0,3}> ?(.*)$") + private val LIST_LINE = Regex("^( {0,3})([-+*]|(\\d{1,9})[.)])([ \\t]+|$)(.*)$") + private val INDENTED_CODE = Regex("^(?: {4}|\\t)") + private val TABLE_DELIMITER = Regex("^ {0,3}\\|?[ \\t]*:?-+:?[ \\t]*(?:\\|[ \\t]*:?-+:?[ \\t]*)*\\|?[ \\t]*$") + private val SETEXT_1 = Regex("^ {0,3}=+[ \\t]*$") + private val SETEXT_2 = Regex("^ {0,3}-+[ \\t]*$") + private val TASK_PREFIX = Regex("^\\[([ xX])\\](?:[ \\t]+|$)") + private val EMAIL_ADDRESS = Regex("^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+$") + private val HTML_TAG = Regex("^/?[A-Za-z][A-Za-z0-9-]*(?:\\s[^>]*)?/?$") + private val BARE_URL = Regex("""(?"']+""", RegexOption.IGNORE_CASE) + + private const val ESCAPABLE = "\\`*_{}[]()#+-.!|~>$" + private const val TRAILING_URL_PUNCTUATION = ".,;:!?" +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/Mermaid.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/Mermaid.kt new file mode 100644 index 000000000..8f56ea525 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/Mermaid.kt @@ -0,0 +1,450 @@ +package io.github.kahme247.ompweb.ui.render + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.graphics.Canvas +import android.webkit.JavascriptInterface +import android.webkit.RenderProcessGoneDetail +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.graphics.createBitmap +import androidx.webkit.WebViewAssetLoader +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import kotlinx.coroutines.delay +import org.json.JSONObject +import java.util.Locale +import kotlin.math.roundToInt + +/** + * Mermaid diagram with the same interaction model as `MermaidBlock.tsx`: + * the source is shown by default and a "Preview" toggle renders it (cached + * bitmap). Rendering runs in a sandboxed offscreen WebView with the bundled + * `assets/mermaid.min.js`; there is no network and the only JS bridge is the + * result callback. Any failure (parse error, timeout, empty capture) falls + * back to the source block. + */ +@Composable +fun MermaidBlock(code: String, modifier: Modifier = Modifier) { + val colors = LocalOmpColors.current + val dark = colors.background.luminance() < 0.5f + val cacheKey = remember(code, dark) { MermaidBitmapCache.key(code, dark) } + var preview by remember(code) { mutableStateOf(false) } + var bitmap by remember(code, dark) { mutableStateOf(MermaidBitmapCache.get(cacheKey)) } + var error by remember(code) { mutableStateOf(null) } + var attempt by remember(code) { mutableIntStateOf(0) } + + Column(modifier.fillMaxWidth()) { + Row( + Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = if (preview) "mermaid" else "", + color = colors.textDim, + fontSize = 11.sp, + fontFamily = io.github.kahme247.ompweb.ui.theme.OmpMonoFontFamily, + modifier = Modifier.weight(1f), + ) + Text( + text = "Source", + color = if (!preview) colors.text else colors.textMuted, + fontSize = 11.sp, + modifier = Modifier + .clickable { preview = false } + .padding(horizontal = 8.dp, vertical = 4.dp), + ) + Text( + text = "Preview", + color = if (preview) colors.text else colors.textMuted, + fontSize = 11.sp, + modifier = Modifier + .clickable { + preview = true + if (bitmap == null && error != null) { + error = null + attempt++ + } + } + .padding(horizontal = 8.dp, vertical = 4.dp), + ) + } + + if (!preview) { + CodeBlock(code = code, language = "mermaid", modifier = Modifier.fillMaxWidth()) + } else { + when { + bitmap != null -> Image( + bitmap = bitmap!!.asImageBitmap(), + contentDescription = "Mermaid diagram", + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)), + // The capture is already at the SVG's natural size; drawing it + // 1:1 keeps the diagram's scale identical to the web render. + alignment = Alignment.TopStart, + contentScale = ContentScale.None, + ) + error != null -> { + Column( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(colors.backgroundSubtle) + .padding(10.dp), + ) { + Text( + text = "Mermaid render failed: $error", + color = colors.statusError, + fontSize = 12.sp, + ) + Text( + text = "Retry", + color = colors.accent, + fontSize = 12.sp, + modifier = Modifier + .clickable { + error = null + attempt++ + } + .padding(top = 6.dp), + ) + } + // Fallback: the source is never dropped. + CodeBlock( + code = code, + language = "mermaid", + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp), + ) + } + else -> MermaidWebView( + code = code, + dark = dark, + attempt = attempt, + onRendered = { rendered -> + MermaidBitmapCache.put(cacheKey, rendered) + bitmap = rendered + }, + onError = { message -> error = message ?: "unknown error" }, + ) + } + } + } +} + +// Lint cannot see the Kotlin override on the anonymous client below; the +// handler is implemented (`onRenderProcessGone`) and destroys the WebView. +@SuppressLint("SetJavaScriptEnabled", "MissingOnRenderProcessGone") +@Composable +private fun MermaidWebView( + code: String, + dark: Boolean, + attempt: Int, + onRendered: (Bitmap) -> Unit, + onError: (String?) -> Unit, +) { + val context = androidx.compose.ui.platform.LocalContext.current + val density = LocalDensity.current + val colors = LocalOmpColors.current + var webView by remember { mutableStateOf(null) } + var targetHeightPx by remember { mutableIntStateOf(220 * density.density.roundToInt()) } + var contentWidthPx by remember { mutableIntStateOf(0) } + var contentHeightPx by remember { mutableIntStateOf(0) } + var reported by remember { mutableStateOf(false) } + val backgroundArgb = colors.background.toArgbCompat() + + DisposableEffect(Unit) { + onDispose { + webView?.apply { + stopLoading() + removeJavascriptInterface("ompMermaid") + destroy() + } + webView = null + } + } + + LaunchedEffect(reported, contentWidthPx, contentHeightPx, webView) { + if (!reported) return@LaunchedEffect + val view = webView ?: return@LaunchedEffect + // Give Chromium a couple of frames to reflow the SVG after the resize. + delay(120) + repeat(4) { + val bitmap = drawWebView(view) + if (bitmap != null && hasVisibleContent(bitmap, backgroundArgb)) { + onRendered(cropToContent(bitmap, contentWidthPx, contentHeightPx)) + return@LaunchedEffect + } + delay(120) + } + onError("empty capture") + } + + LaunchedEffect(attempt, code) { + delay(12_000) + if (!reported) onError("timeout") + } + + Box( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(colors.background), + ) { + AndroidView( + modifier = Modifier + .fillMaxWidth() + .height(with(density) { targetHeightPx.toDp() }), + factory = { ctx -> + WebView(ctx).apply { + setBackgroundColor(backgroundArgb) + settings.javaScriptEnabled = true + settings.domStorageEnabled = false + settings.allowFileAccess = false + settings.allowContentAccess = false + settings.setSupportZoom(false) + settings.builtInZoomControls = false + settings.blockNetworkLoads = true + settings.cacheMode = WebSettings.LOAD_NO_CACHE + settings.mediaPlaybackRequiresUserGesture = true + settings.javaScriptCanOpenWindowsAutomatically = false + + val assetLoader = WebViewAssetLoader.Builder() + .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(ctx)) + .build() + webViewClient = object : WebViewClient() { + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest, + ): WebResourceResponse? = assetLoader.shouldInterceptRequest(request.url) + + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest, + ): Boolean = !allowsMermaidNavigation( + scheme = request.url.scheme, + host = request.url.host, + path = request.url.path, + ) + + override fun onRenderProcessGone( + view: WebView, + detail: RenderProcessGoneDetail, + ): Boolean { + view.destroy() + onError("renderer crashed") + return true + } + } + + addJavascriptInterface( + object { + @JavascriptInterface + fun postMessage(value: String) { + post { + try { + val json = JSONObject(value) + if (!json.optBoolean("ok")) { + onError(json.optString("error").ifBlank { "parse error" }) + return@post + } + val w = json.optDouble("w", 1.0).coerceAtLeast(1.0) + val h = json.optDouble("h", 1.0).coerceAtLeast(1.0) + // The SVG box in CSS px; captured at device density + // so the bitmap keeps the diagram at its natural size. + contentWidthPx = (w * density.density).roundToInt().coerceAtLeast(1) + contentHeightPx = (h * density.density).roundToInt().coerceAtLeast(1) + targetHeightPx = contentHeightPx.coerceIn(60, 2400) + reported = true + } catch (e: Exception) { + onError(e.message) + } + } + } + }, + "ompMermaid", + ) + + loadDataWithBaseURL( + "https://$ASSET_HOST/assets/", + mermaidHtml(code, dark = dark, backgroundArgb = backgroundArgb), + "text/html", + "UTF-8", + null, + ) + webView = this + } + }, + update = { view -> webView = view }, + ) + } +} + +/** + * The origin the bundled assets are served from: `WebViewAssetLoader` answers + * `https://appassets.androidplatform.net/assets/...` from APK assets, so the + * document never needs a real network origin. + */ +private const val ASSET_HOST = "appassets.androidplatform.net" + +/** + * Whether a navigation may replace the rendered document. Only the bundled + * asset origin under `/assets/` qualifies: a diagram can carry a link, and + * anything else — a `data:`/`about:` load, the asset host with another path, a + * foreign origin — is blocked instead of being handed to the WebView. The + * loader only answers `/assets/`, so this adds nothing it would serve anyway. + */ +internal fun allowsMermaidNavigation(scheme: String?, host: String?, path: String?): Boolean = + scheme == "https" && host == ASSET_HOST && path.orEmpty().startsWith("/assets/") + +private fun mermaidHtml(code: String, dark: Boolean, backgroundArgb: Int): String { + val theme = if (dark) "dark" else "default" + val background = String.format(Locale.US, "#%06X", backgroundArgb and 0xFFFFFF) + val escaped = JSONObject.quote(code) + .replace("<", "\\u003c") + .replace("\u2028", "\\u2028") + .replace("\u2029", "\\u2029") + return """ + + + + + + +
    + + + """.trimIndent() +} + +/** + * The WebView spans the full chat width, but the SVG keeps its natural width + * (mermaid's inline `max-width`), so the capture has unused background to the + * right and below. Crop to the reported SVG box so the transcript does not + * reserve that empty space. + */ +private fun cropToContent(bitmap: Bitmap, width: Int, height: Int): Bitmap { + val w = width.coerceIn(1, bitmap.width) + val h = height.coerceIn(1, bitmap.height) + if (w == bitmap.width && h == bitmap.height) return bitmap + return Bitmap.createBitmap(bitmap, 0, 0, w, h) +} + +private fun drawWebView(view: WebView): Bitmap? { + val width = view.width + val height = view.height + if (width <= 0 || height <= 0) return null + return try { + val bitmap = createBitmap(width, height) + view.draw(Canvas(bitmap)) + bitmap + } catch (_: Exception) { + null + } +} + +/** Guards against capturing before Chromium painted the diagram. */ +private fun hasVisibleContent(bitmap: Bitmap, background: Int): Boolean { + val stepX = (bitmap.width / 24).coerceAtLeast(1) + val stepY = (bitmap.height / 24).coerceAtLeast(1) + var y = stepY / 2 + while (y < bitmap.height) { + var x = stepX / 2 + while (x < bitmap.width) { + if (bitmap.getPixel(x, y) != background) return true + x += stepX + } + y += stepY + } + return false +} + +private fun androidx.compose.ui.graphics.Color.toArgbCompat(): Int = + android.graphics.Color.argb(alpha, red, green, blue) + +/** LRU of rendered diagrams; bitmaps are large, so the cache stays tiny. */ +private object MermaidBitmapCache { + private const val MAX_ENTRIES = 4 + private val entries = object : LinkedHashMap(MAX_ENTRIES, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = + size > MAX_ENTRIES + } + + fun key(code: String, dark: Boolean): String = "${if (dark) "dark" else "light"}:$code" + + @Synchronized + fun get(key: String): Bitmap? = entries[key] + + @Synchronized + fun put(key: String, bitmap: Bitmap) { + entries[key] = bitmap + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/MessageSize.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/MessageSize.kt new file mode 100644 index 000000000..12135dc24 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/MessageSize.kt @@ -0,0 +1,22 @@ +package io.github.kahme247.ompweb.ui.render + +import java.util.Locale + +/** + * Above this many characters a message is rendered as a collapsed block with + * an explicit "show" action instead of full markdown (web `SAFE_MARKDOWN_LIMIT`): + * a multi-megabyte paste must not lock the UI thread in the markdown parser. + */ +const val MAX_MARKDOWN_CHARS = 100_000 + +fun isLargeMessage(text: String): Boolean = text.length > MAX_MARKDOWN_CHARS + +/** Human size of a message, e.g. `123 KB` or `1.0 MB` (chars ≈ bytes here). */ +fun formatMessageSize(chars: Int): String { + val kb = chars / 1000.0 + return if (kb >= 1000) { + String.format(Locale.US, "%.1f MB", kb / 1000) + } else { + String.format(Locale.US, "%.0f KB", kb) + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/RemoteImage.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/RemoteImage.kt new file mode 100644 index 000000000..7fd87667d --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/RemoteImage.kt @@ -0,0 +1,631 @@ +package io.github.kahme247.ompweb.ui.render + +import android.content.ClipData +import android.content.Context +import android.content.Intent +import android.util.Base64 +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.kahme247.ompweb.ui.theme.OmpDialog +import androidx.compose.ui.window.DialogProperties +import androidx.core.content.FileProvider +import coil3.ImageLoader +import coil3.compose.AsyncImage +import coil3.compose.SubcomposeAsyncImage +import coil3.decode.DataSource +import coil3.decode.ImageSource as CoilImageSource +import coil3.fetch.FetchResult +import coil3.fetch.Fetcher +import coil3.svg.SvgDecoder +import coil3.fetch.SourceFetchResult +import coil3.network.okhttp.OkHttpNetworkFetcherFactory +import coil3.request.ImageRequest +import coil3.request.Options +import coil3.request.crossfade +import io.github.kahme247.ompweb.R +import io.github.kahme247.ompweb.ui.theme.LocalOmpColors +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.Buffer +import okio.FileSystem +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.InputStream +import kotlin.math.min + +/** Rendering environment supplied once by the app container. */ +object RenderEnvironment { + @Volatile + private var baseUrlProvider: (() -> String?)? = null + + @Volatile + private var httpClient: OkHttpClient? = null + + @Volatile + private var imageLoader: ImageLoader? = null + + @Volatile + private var generation = 0 + + /** + * Wires the authenticated OkHttp client (cookie jar + policy interceptor) + * and the origin resolver into the render layer. Called from + * `AppContainer` before the first chat screen composes. + */ + fun configure(baseUrlProvider: () -> String?, httpClient: OkHttpClient) { + synchronized(this) { + this.baseUrlProvider = baseUrlProvider + this.httpClient = httpClient + imageLoader = null + generation++ + } + } + + /** Current server origin without trailing slash, or null while unknown. */ + fun baseUrl(): String? = baseUrlProvider?.invoke()?.trimEnd('/')?.takeIf { it.isNotEmpty() } + + fun httpClient(): OkHttpClient? = httpClient + + internal fun generation(): Int = generation + + internal fun loader(context: Context): ImageLoader? { + val client = httpClient ?: return null + imageLoader?.let { return it } + return synchronized(this) { + imageLoader ?: ImageLoader.Builder(context.applicationContext) + .components { + add(InlineImageFetcher.Factory()) + // SVG files have no platform decoder (Bug 17 in the Phase 9 + // review): `coil-svg` renders them, so an `.svg` in the + // explorer opens instead of showing an empty box. + add(SvgDecoder.Factory()) + add(OkHttpNetworkFetcherFactory(callFactory = { client })) + } + .crossfade(true) + .build() + .also { imageLoader = it } + } + } +} + +/** A decoded-on-demand inline image (base64 payload from the transcript). */ +internal class InlineImage(val mimeType: String, val bytes: ByteArray) + +/** Coil fetcher for inline transcript images; never touches network or disk. */ +internal class InlineImageFetcher(private val image: InlineImage) : Fetcher { + override suspend fun fetch(): FetchResult? = SourceFetchResult( + source = CoilImageSource( + source = Buffer().write(image.bytes), + fileSystem = FileSystem.SYSTEM, + ), + mimeType = image.mimeType.ifBlank { null }, + dataSource = DataSource.MEMORY, + ) + + class Factory : Fetcher.Factory { + override fun create(data: InlineImage, options: Options, imageLoader: ImageLoader): Fetcher = + InlineImageFetcher(data) + } +} + +/** + * Resolves a transcript image path against the server origin. Absolute URLs + * and opaque schemes pass through untouched. + */ +internal fun resolveImageUrl(path: String, baseUrl: String?): String { + if (path.isEmpty()) return path + val lower = path.lowercase() + if ( + lower.startsWith("http://") || lower.startsWith("https://") || + lower.startsWith("data:") || lower.startsWith("content:") || + lower.startsWith("android.resource:") + ) { + return path + } + if (baseUrl.isNullOrBlank()) return path + val base = baseUrl.trimEnd('/') + return if (path.startsWith("/")) base + path else "$base/$path" +} + +/** Media thumbnails are served by the same endpoint with `?thumb=1`. */ +internal fun withThumbnail(url: String, thumbnail: Boolean): String { + if (!thumbnail || !url.contains("/api/media/")) return url + if (url.contains("thumb=")) return url + return url + if (url.contains('?')) "&thumb=1" else "?thumb=1" +} + +/** Hard cap for one image download; a message must not stream an unbounded file. */ +internal const val MAX_IMAGE_BYTES = 16L * 1024 * 1024 + +/** Longest inline base64 payload that is decoded at all (~12 MB of bytes). */ +internal const val MAX_INLINE_BASE64_CHARS = 16_000_000 + +/** Decoded bitmap edge cap: a phone screen never needs more than this. */ +internal const val MAX_IMAGE_EDGE_PX = 2560 + +/** Whether an image may load without asking the reader. */ +internal sealed interface ImageLoadDecision { + /** Local data or the app's own origin. */ + data object Auto : ImageLoadDecision + + /** A foreign host (or unsupported scheme); the row asks before loading. */ + data class External(val host: String) : ImageLoadDecision +} + +private val SCHEME_PREFIX = Regex("""^([a-zA-Z][a-zA-Z0-9+.-]*):""") + +private data class HttpOrigin(val scheme: String, val host: String, val port: Int) + +/** + * Auto-load policy: `data:`/`content:`/`android.resource:` images and images + * served by the app's own origin load immediately; everything else waits for + * a tap, so an assistant-authored message cannot make the device fetch from + * an arbitrary server (IP leak, tracking pixel). + */ +internal fun imageLoadDecision(path: String, baseUrl: String?): ImageLoadDecision { + if (path.isEmpty()) return ImageLoadDecision.Auto + val lower = path.lowercase() + if ( + lower.startsWith("data:") || lower.startsWith("content:") || + lower.startsWith("android.resource:") + ) { + return ImageLoadDecision.Auto + } + // Protocol-relative `//host/…` points at a foreign origin. + if (path.startsWith("//")) { + val host = authorityHost(path.substring(2)) + return if (host.isEmpty()) ImageLoadDecision.Auto else ImageLoadDecision.External(host) + } + val match = SCHEME_PREFIX.find(path) ?: return ImageLoadDecision.Auto // relative → app origin + val scheme = match.groupValues[1].lowercase() + val rest = path.substring(match.range.last + 1).removePrefix("//") + if (scheme != "http" && scheme != "https") { + // `file:`, `ftp:`, `intent:` … are not remote previews. + return ImageLoadDecision.External(authorityHost(rest).ifEmpty { scheme }) + } + val origin = parseOrigin(scheme, rest) + val baseOrigin = baseUrl?.let { base -> + val baseMatch = SCHEME_PREFIX.find(base) ?: return@let null + parseOrigin(baseMatch.groupValues[1].lowercase(), base.substring(baseMatch.range.last + 1).removePrefix("//")) + } + val sameOrigin = origin != null && baseOrigin != null && + origin.scheme == baseOrigin.scheme && + origin.host.equals(baseOrigin.host, ignoreCase = true) && + origin.port == baseOrigin.port + return if (sameOrigin) { + ImageLoadDecision.Auto + } else { + ImageLoadDecision.External(origin?.host?.takeIf { it.isNotEmpty() } ?: scheme) + } +} + +private fun authorityHost(authorityAndPath: String): String = + authorityAndPath.substringBefore('/').substringBefore('?').substringBefore('#') + .substringAfterLast('@').substringBefore(':') + +private fun parseOrigin(scheme: String, authorityAndPath: String): HttpOrigin? { + val authority = authorityAndPath.substringBefore('/').substringBefore('?').substringBefore('#') + val hostPort = authority.substringAfterLast('@') + if (hostPort.isEmpty()) return null + val host: String + val portText: String + if (hostPort.startsWith("[")) { + // IPv6 literal: `[::1]:8080`. + val close = hostPort.indexOf(']') + if (close < 0) return null + host = hostPort.substring(0, close + 1) + portText = hostPort.substring(close + 1).removePrefix(":") + } else { + host = hostPort.substringBefore(':') + portText = if (hostPort.contains(':')) hostPort.substringAfter(':') else "" + } + if (host.isEmpty()) return null + val port = portText.toIntOrNull() ?: if (scheme == "https") 443 else 80 + return HttpOrigin(scheme, host, port) +} + +/** + * Thumbnail that opens a full-screen lightbox. Relative URLs are resolved + * against [RenderEnvironment.baseUrl]; media URLs request the server-side + * WebP preview unless [thumbnail] is false. + */ +@Composable +fun RemoteImage( + source: ImageSource, + contentDescription: String?, + modifier: Modifier = Modifier, + thumbnail: Boolean = true, + onOpen: (() -> Unit)? = null, +) { + val context = LocalContext.current + val colors = LocalOmpColors.current + val loader = rememberRenderImageLoader() + var lightbox by remember(source) { mutableStateOf(false) } + val baseUrl = RenderEnvironment.baseUrl() + val decision = remember(source, baseUrl) { + imageLoadDecision((source as? ImageSource.Url)?.path.orEmpty(), baseUrl) + } + var allowExternal by remember(source) { mutableStateOf(false) } + // The external image waiting for the user's consent; null = nothing to ask. + val blockedExternal = (decision as? ImageLoadDecision.External)?.takeUnless { allowExternal } + // Base64 payloads are decoded off the main thread instead of during + // composition; an oversized payload is refused outright. + val inlineBytes by produceState(initialValue = null, source) { + value = when (val inline = source as? ImageSource.Inline) { + null -> null + else -> withContext(Dispatchers.Default) { decodeInline(inline) } + } + } + val model = remember(source, thumbnail, loader, blockedExternal, inlineBytes) { + if (blockedExternal != null) null else imageModel(context, source, thumbnail, inlineBytes) + } + val shape = RoundedCornerShape(6.dp) + + if (blockedExternal != null) { + Box( + modifier + .clip(shape) + .background(colors.backgroundSubtle) + .clickable { allowExternal = true } + .padding(12.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(R.string.chat_image_external, blockedExternal.host), + color = colors.accent, + fontSize = 12.sp, + maxLines = 2, + ) + } + } else if (loader == null || model == null) { + Box( + modifier + .clip(shape) + .background(colors.backgroundSubtle) + .padding(12.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = contentDescription ?: "Image", + color = colors.textDim, + fontSize = 12.sp, + maxLines = 2, + ) + } + } else { + SubcomposeAsyncImage( + model = model, + contentDescription = contentDescription, + imageLoader = loader, + modifier = modifier + .clip(shape) + .background(colors.backgroundSubtle) + .clickable { + if (onOpen != null) onOpen() else lightbox = true + }, + contentScale = ContentScale.Fit, + loading = { + Box( + Modifier + .fillMaxWidth() + .padding(12.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = contentDescription ?: "…", + color = colors.textDim, + fontSize = 12.sp, + maxLines = 2, + ) + } + }, + error = { + Box( + Modifier + .fillMaxWidth() + .padding(12.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = contentDescription ?: "Image unavailable", + color = colors.textDim, + fontSize = 12.sp, + maxLines = 2, + ) + } + }, + ) + } + + if (lightbox) { + ImageLightbox(source = source, onDismiss = { lightbox = false }) + } +} + +@Composable +private fun rememberRenderImageLoader(): ImageLoader? { + val context = LocalContext.current + return remember(RenderEnvironment.generation()) { + RenderEnvironment.loader(context) + } +} + +private fun imageModel( + context: Context, + source: ImageSource, + thumbnail: Boolean, + inlineBytes: ByteArray?, +): ImageRequest? { + val data: Any = when (source) { + is ImageSource.Url -> { + val url = withThumbnail(resolveImageUrl(source.path, RenderEnvironment.baseUrl()), thumbnail) + if (url.isBlank()) return null + url + } + is ImageSource.Inline -> InlineImage(source.mimeType, inlineBytes ?: return null) + } + return ImageRequest.Builder(context) + .data(data) + .crossfade(true) + .size(MAX_IMAGE_EDGE_PX, MAX_IMAGE_EDGE_PX) + .build() +} + +private fun decodeInline(source: ImageSource.Inline): ByteArray? { + // The payload is base64 (~4/3 the bytes); refuse it before allocating. + if (source.base64.length > MAX_INLINE_BASE64_CHARS) return null + return try { + Base64.decode(source.base64, Base64.DEFAULT) + } catch (_: IllegalArgumentException) { + null + } +} + +/** + * Full-screen viewer with pinch/double-tap zoom, 1:1 and fit controls plus + * share and save-as. Loads bytes with the authenticated client so private + * media endpoints work. + */ +@Composable +fun ImageLightbox(source: ImageSource, onDismiss: () -> Unit) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val loader = rememberRenderImageLoader() + val model = remember(source, loader) { + val bytes = if (source is ImageSource.Inline) decodeInline(source) else null + imageModel(context, source, thumbnail = false, inlineBytes = bytes) + } + + var scale by remember { mutableFloatStateOf(1f) } + var offset by remember { mutableStateOf(Offset.Zero) } + var boxSize by remember { mutableStateOf(IntSize.Zero) } + var naturalSize by remember { mutableStateOf(IntSize.Zero) } + var bytes by remember(source) { mutableStateOf(null) } + + val fitScale = remember(naturalSize, boxSize) { + if (naturalSize.width <= 0 || naturalSize.height <= 0 || boxSize.width <= 0 || boxSize.height <= 0) { + 1f + } else { + min( + 1f, + min( + boxSize.width.toFloat() / naturalSize.width, + boxSize.height.toFloat() / naturalSize.height, + ), + ) + } + } + + fun reset() { + scale = 1f + offset = Offset.Zero + } + + fun actualSize() { + scale = (1f / fitScale.coerceAtLeast(0.01f)).coerceIn(0.1f, 8f) + offset = Offset.Zero + } + + val saveLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("image/png"), + ) { uri -> + val data = bytes ?: return@rememberLauncherForActivityResult + if (uri == null) return@rememberLauncherForActivityResult + scope.launch { + val ok = withContext(Dispatchers.IO) { + try { + context.contentResolver.openOutputStream(uri, "wt")?.use { stream -> + stream.write(data) + } != null + } catch (_: Exception) { + false + } + } + Toast.makeText(context, if (ok) "Saved" else "Could not save", Toast.LENGTH_SHORT).show() + } + } + + LaunchedEffect(source) { + bytes = loadImageBytes(source) + } + + OmpDialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false), + ) { + Box( + Modifier + .fillMaxSize() + .background(Color.Black), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { boxSize = it } + .pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, _ -> + scale = (scale * zoom).coerceIn(0.1f, 8f) + offset += pan + } + } + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { + if (scale > 1.01f) reset() else actualSize() + }, + ) + }, + contentAlignment = Alignment.Center, + ) { + if (loader != null && model != null) { + AsyncImage( + model = model, + contentDescription = null, + imageLoader = loader, + modifier = Modifier + .fillMaxSize() + .graphicsLayer( + scaleX = scale, + scaleY = scale, + translationX = offset.x, + translationY = offset.y, + ), + contentScale = ContentScale.Fit, + onSuccess = { state -> + val image = state.result.image + naturalSize = IntSize(image.width, image.height) + }, + ) + } else { + Text("Image unavailable", color = Color.White) + } + } + + Row( + Modifier + .align(Alignment.TopEnd) + // The dialog draws edge-to-edge (decorFitsSystemWindows = + // false), so without this the action row sits under the + // status bar and its taps never reach the buttons. + .statusBarsPadding() + .padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LightboxAction("Fit") { reset() } + LightboxAction("1:1") { actualSize() } + LightboxAction("Share") { shareImage(context, bytes) } + LightboxAction("Save") { saveLauncher.launch("image.png") } + LightboxAction("Close") { onDismiss() } + } + } + } +} + +@Composable +private fun LightboxAction(label: String, onClick: () -> Unit) { + TextButton(onClick = onClick) { + Text(text = label, color = Color.White, fontSize = 12.sp) + } +} + +private suspend fun loadImageBytes(source: ImageSource): ByteArray? = + withContext(Dispatchers.IO) { + when (source) { + is ImageSource.Inline -> decodeInline(source) + is ImageSource.Url -> { + val url = resolveImageUrl(source.path, RenderEnvironment.baseUrl()) + if (!url.startsWith("http://") && !url.startsWith("https://")) return@withContext null + val client = RenderEnvironment.httpClient() ?: return@withContext null + try { + client.newCall(Request.Builder().url(url).build()).execute().use { response -> + if (!response.isSuccessful) return@withContext null + val body = response.body + // Bulky bodies are refused before any allocation. + if (body.contentLength() > MAX_IMAGE_BYTES) return@withContext null + body.byteStream().use { input -> readCapped(input, MAX_IMAGE_BYTES) } + } + } catch (_: Exception) { + null + } + } + } + } + +/** Reads at most [limit] bytes; a longer stream is rejected instead of buffered. */ +private fun readCapped(input: InputStream, limit: Long): ByteArray? { + val out = ByteArrayOutputStream() + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var total = 0L + while (true) { + val read = input.read(buffer) + if (read < 0) break + total += read + if (total > limit) return null + out.write(buffer, 0, read) + } + return out.toByteArray() +} + +private fun shareImage(context: Context, bytes: ByteArray?) { + if (bytes == null) { + Toast.makeText(context, "Image not ready", Toast.LENGTH_SHORT).show() + return + } + try { + val directory = File(context.cacheDir, "exports").apply { mkdirs() } + val file = File(directory, "image-${System.currentTimeMillis()}.png") + file.writeBytes(bytes) + val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) + val intent = Intent(Intent.ACTION_SEND).apply { + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + clipData = ClipData.newRawUri("image", uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(intent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } catch (_: Exception) { + Toast.makeText(context, "Could not share", Toast.LENGTH_SHORT).show() + } +} diff --git a/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/SyntaxHighlight.kt b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/SyntaxHighlight.kt new file mode 100644 index 000000000..4657264c0 --- /dev/null +++ b/android/app/src/main/java/io/github/kahme247/ompweb/ui/render/SyntaxHighlight.kt @@ -0,0 +1,405 @@ +package io.github.kahme247.ompweb.ui.render + +/** + * Small, dependency-free syntax highlighter for fenced code blocks. + * + * It is intentionally a tokenizer, not a parser: strings and comments win over + * keywords, escapes are honoured, and unknown languages simply produce no + * spans (plain text). Mirrors the visual role of `lib/syntax-highlight.ts` + * without pulling highlight.js into the APK. + */ +object SyntaxHighlighter { + + enum class Kind { Keyword, Type, String, Comment, Number, Literal } + + data class Span(val start: Int, val end: Int, val kind: Kind) + + fun highlight(code: String, language: String?): List { + val spec = specFor(language) ?: return emptyList() + if (code.isEmpty()) return emptyList() + val spans = ArrayList() + var i = 0 + val n = code.length + while (i < n) { + val c = code[i] + + val lineComment = spec.lineComments.firstOrNull { code.startsWith(it, i) } + if (lineComment != null) { + val end = code.indexOf('\n', i).let { if (it < 0) n else it } + spans.add(Span(i, end, Kind.Comment)) + i = end + continue + } + + if (spec.blockComment != null && code.startsWith(spec.blockComment.first, i)) { + val close = code.indexOf(spec.blockComment.second, i + spec.blockComment.first.length) + val end = if (close < 0) n else close + spec.blockComment.second.length + spans.add(Span(i, end, Kind.Comment)) + i = end + continue + } + + val quote = when { + spec.tripleQuotes && (code.startsWith("\"\"\"", i) || code.startsWith("'''", i)) -> code.substring(i, i + 3) + c in spec.quotes -> c.toString() + else -> null + } + if (quote != null) { + var j = i + quote.length + while (j < n) { + if (code[j] == '\\' && quote.length == 1) { + j += 2 + continue + } + if (code.startsWith(quote, j)) { + j += quote.length + break + } + if (code[j] == '\n' && quote.length == 1 && quote != "`") { + break + } + j++ + } + spans.add(Span(i, j.coerceAtMost(n), Kind.String)) + i = j.coerceAtMost(n) + continue + } + + if (c.isLetter() || c == '_') { + var j = i + 1 + while (j < n && (code[j].isLetterOrDigit() || code[j] == '_' || code[j] == '$')) j++ + val word = code.substring(i, j) + when { + spec.literals.contains(word) -> spans.add(Span(i, j, Kind.Literal)) + spec.types.contains(word) -> spans.add(Span(i, j, Kind.Type)) + spec.keywords.contains(word) -> spans.add(Span(i, j, Kind.Keyword)) + } + i = j + continue + } + + if (c.isDigit()) { + var j = i + 1 + while (j < n) { + val next = code[j] + if (next.isLetterOrDigit() || next == '_' || next == '.') { + // Stop `1..5` from becoming one number literal. + if (next == '.' && j + 1 < n && code[j + 1] == '.') break + j++ + } else { + break + } + } + spans.add(Span(i, j, Kind.Number)) + i = j + continue + } + + i++ + } + return spans + } + + private data class LanguageSpec( + val keywords: Set, + val types: Set, + val literals: Set, + val lineComments: List, + val blockComment: Pair?, + val quotes: Set, + val tripleQuotes: Boolean = false, + ) + + private val COMMON_LITERALS = setOf("true", "false", "null", "nil", "None", "True", "False", "undefined") + + private fun specFor(language: String?): LanguageSpec? { + val name = language?.trim()?.lowercase()?.removePrefix("language-")?.removePrefix("lang-") ?: return null + return when (name) { + "kt", "kotlin", "kts" -> LanguageSpec( + keywords = KOTLIN_KEYWORDS, + types = KOTLIN_TYPES, + literals = COMMON_LITERALS, + lineComments = listOf("//"), + blockComment = "/*" to "*/", + quotes = setOf('"', '\''), + ) + "java" -> LanguageSpec( + keywords = JAVA_KEYWORDS, + types = JAVA_TYPES, + literals = COMMON_LITERALS, + lineComments = listOf("//"), + blockComment = "/*" to "*/", + quotes = setOf('"', '\''), + ) + "js", "javascript", "jsx", "mjs", "cjs" -> LanguageSpec( + keywords = JS_KEYWORDS, + types = JS_TYPES, + literals = COMMON_LITERALS, + lineComments = listOf("//"), + blockComment = "/*" to "*/", + quotes = setOf('"', '\'', '`'), + ) + "ts", "typescript", "tsx" -> LanguageSpec( + keywords = JS_KEYWORDS + setOf("interface", "type", "enum", "namespace", "declare", "abstract", "implements", "readonly", "is", "keyof", "infer", "satisfies", "as"), + types = JS_TYPES + setOf("string", "number", "boolean", "any", "unknown", "never", "void", "object"), + literals = COMMON_LITERALS, + lineComments = listOf("//"), + blockComment = "/*" to "*/", + quotes = setOf('"', '\'', '`'), + ) + "py", "python", "python3" -> LanguageSpec( + keywords = PYTHON_KEYWORDS, + types = PYTHON_TYPES, + literals = COMMON_LITERALS, + lineComments = listOf("#"), + blockComment = null, + quotes = setOf('"', '\''), + tripleQuotes = true, + ) + "go", "golang" -> LanguageSpec( + keywords = GO_KEYWORDS, + types = GO_TYPES, + literals = COMMON_LITERALS, + lineComments = listOf("//"), + blockComment = "/*" to "*/", + quotes = setOf('"', '\'', '`'), + ) + "rs", "rust" -> LanguageSpec( + keywords = RUST_KEYWORDS, + types = RUST_TYPES, + literals = COMMON_LITERALS, + lineComments = listOf("//"), + blockComment = "/*" to "*/", + quotes = setOf('"'), + ) + "c", "h", "cpp", "c++", "cc", "cxx", "hpp" -> LanguageSpec( + keywords = C_KEYWORDS, + types = C_TYPES, + literals = COMMON_LITERALS, + lineComments = listOf("//"), + blockComment = "/*" to "*/", + quotes = setOf('"', '\''), + ) + "cs", "csharp", "c#" -> LanguageSpec( + keywords = CSHARP_KEYWORDS, + types = CSHARP_TYPES, + literals = COMMON_LITERALS, + lineComments = listOf("//"), + blockComment = "/*" to "*/", + quotes = setOf('"', '\''), + ) + "swift" -> LanguageSpec( + keywords = SWIFT_KEYWORDS, + types = SWIFT_TYPES, + literals = COMMON_LITERALS, + lineComments = listOf("//"), + blockComment = "/*" to "*/", + quotes = setOf('"'), + ) + "sh", "bash", "shell", "zsh", "console" -> LanguageSpec( + keywords = SHELL_KEYWORDS, + types = emptySet(), + literals = COMMON_LITERALS, + lineComments = listOf("#"), + blockComment = null, + quotes = setOf('"', '\''), + ) + "json", "jsonc" -> LanguageSpec( + keywords = emptySet(), + types = emptySet(), + literals = setOf("true", "false", "null"), + lineComments = if (name == "jsonc") listOf("//") else emptyList(), + blockComment = if (name == "jsonc") "/*" to "*/" else null, + quotes = setOf('"'), + ) + "yaml", "yml" -> LanguageSpec( + keywords = setOf("true", "false", "null", "yes", "no", "on", "off"), + types = emptySet(), + literals = emptySet(), + lineComments = listOf("#"), + blockComment = null, + quotes = setOf('"', '\''), + ) + "toml", "ini", "properties", "dotenv", "env" -> LanguageSpec( + keywords = emptySet(), + types = emptySet(), + literals = setOf("true", "false"), + lineComments = listOf("#", ";"), + blockComment = null, + quotes = setOf('"', '\''), + ) + "sql" -> LanguageSpec( + keywords = SQL_KEYWORDS, + types = SQL_TYPES, + literals = setOf("NULL", "TRUE", "FALSE"), + lineComments = listOf("--"), + blockComment = "/*" to "*/", + quotes = setOf('\'', '"'), + ) + "xml", "html", "xhtml", "svg", "vue" -> LanguageSpec( + keywords = emptySet(), + types = emptySet(), + literals = emptySet(), + lineComments = listOf(" + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_monochrome.xml b/android/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 000000000..c12b784cf --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_notification.xml b/android/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 000000000..c027b983a --- /dev/null +++ b/android/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_notify_completed.xml b/android/app/src/main/res/drawable/ic_notify_completed.xml new file mode 100644 index 000000000..1c0a4d62f --- /dev/null +++ b/android/app/src/main/res/drawable/ic_notify_completed.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_notify_connection.xml b/android/app/src/main/res/drawable/ic_notify_connection.xml new file mode 100644 index 000000000..e85eba102 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_notify_connection.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_notify_error.xml b/android/app/src/main/res/drawable/ic_notify_error.xml new file mode 100644 index 000000000..385bb9549 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_notify_error.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_notify_input.xml b/android/app/src/main/res/drawable/ic_notify_input.xml new file mode 100644 index 000000000..15399996d --- /dev/null +++ b/android/app/src/main/res/drawable/ic_notify_input.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_notify_message.xml b/android/app/src/main/res/drawable/ic_notify_message.xml new file mode 100644 index 000000000..f2e1a4cec --- /dev/null +++ b/android/app/src/main/res/drawable/ic_notify_message.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_notify_model_switch.xml b/android/app/src/main/res/drawable/ic_notify_model_switch.xml new file mode 100644 index 000000000..1ed92a641 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_notify_model_switch.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_notify_summary.xml b/android/app/src/main/res/drawable/ic_notify_summary.xml new file mode 100644 index 000000000..d5d891858 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_notify_summary.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..c78bee3b5 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..c78bee3b5 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..b067c90a2 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..5f9870fc5 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..a267c02c6 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..976332409 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..2fd27c092 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..9d6b36cc4 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..6771e1ea3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..78b05edd3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..3da259e15 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..22a1fb70b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 000000000..7c79646a3 --- /dev/null +++ b/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1,13 @@ + + + #1B1916 + + + #7DD8A8 + #EC5BAB + #FF9B92 + #7CA8FF + #C9C9D1 + #F5F5F5 + #EC5BAB + diff --git a/android/app/src/main/res/values-night/themes.xml b/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..8c50ebad3 --- /dev/null +++ b/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,9 @@ + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..f58647e69 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,15 @@ + + + #0F0A14 + #000000 + #FAF9F6 + + + #18794E + #B03E22 + #B42318 + #2563EB + #69635A + #2B2823 + #B03E22 + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..4dd4c3ccb --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,308 @@ + + + ompweb + + Connect to your server + Enter the address of your ompweb server. You can find it in ompweb under Settings → Mobile app. + Server address + 100.64.0.10 or my-pc.tailnet.ts.net + Connect + Checking server… + Requests go to %1$s + Over Tailscale this is usually a 100.x address or a …ts.net name. A home-network address only works while your phone is on that network. + + Server misconfigured + ompweb is running, but its password is not configured correctly on the PC. Set OMP_WEB_PASSWORD_HASH and restart the server. + Request blocked + The server refused the request. A proxy in front of ompweb may be blocking native clients. + Not an ompweb server + Something answered, but it does not look like ompweb. Check the address and the port. + Cannot reach the server + Is Tailscale connected and is ompweb shared on the network? Check that the address and port are right. + Secure connection failed + The server certificate could not be verified. Check that the address matches the certificate. Plain HTTP is only accepted for an address that resolves into your own private network. + + Enter password + ompweb at %1$s is password-protected. + ompweb is password-protected. + Password + Sign in + Signing in… + Change server + Incorrect password. + The server rejected the password as too long. + ompweb is running, but its password is not configured correctly on the PC. Set OMP_WEB_PASSWORD_HASH and restart the server. + The server refused the request. A proxy in front of ompweb may be blocking native clients. + Cannot reach the server. Check Tailscale and the address. + The server answered with an error. Try again. + + Server too old + The server at %1$s does not offer the API this app needs. Update ompweb on the PC, then check again. + Reported server version: %1$s + On the PC that runs ompweb: + npm install -g @kahme247/ompweb@latest + Then restart ompweb and check again. + Check again + This app needs /api/app-info with API version 1. Older servers answer the password screen but cannot serve the native app. + + More + Theme + Sign out + Change server + No sessions yet + Start a session on the PC and pull down to refresh. + Cannot reach the server. + The server refused the request. + The server is misconfigured. + The server answered with an error (%1$d). + Retry + Reconnecting to the server… + Not connected to the server. + Live updates are unavailable. + Running + Elsewhere + Another omp process is running this session + Untitled session + just now + %1$d min ago + %1$d h ago + %1$d d ago + Theme + System + Close + + + Workspace + Search sessions + Close search + Running only + + Updating… + Go to session + Add project + Archived sessions + Import session + Back + Forward + Projects could not be loaded. + Project actions + Expand project + Collapse project + New session here + Rename project + Launch configuration + Move up + Move down + Hide project + Hide project? + The project disappears from this app. Its sessions stay on the PC. + Hide + No sessions in this project + Project name + Show more (%1$d) + Show less + Unread + Exited + Session actions + Show forks + Hide forks + Archive + Sessions with forks cannot be archived + Rename + Auto-name + Copy transcript + Export HTML + Copy session ID + Delete + Delete session? + The session file is removed from the PC. This cannot be undone. + Rename session + Session title + Fork of %1$s + Worktree: %1$s + Select a session + Pick a session from the list or start a new one. + Project + Working directory + Branch + Worktree + Model + Thinking + Created + Updated + %1$d messages + This session could not be loaded. + Session actions + New session + Workspace + Browse… + Worktree + Starting… + Search sessions + Start a new session + No matching sessions + Archived sessions + Search archive + No archived sessions + Refresh + Restore + Restoring… + Copy ID + %1$d messages + Select an archived session. + The archive could not be read. + Select directory + Path + Go + Parent folder + Drives + Folders + No subfolders + Loading… + Retry + Clone repository (optional) + https://github.com/user/repo.git + Clones into %1$s + Unsupported Git URL + Clone here + Select this folder + Cancel + Cancel clone + Profile + omp profile (optional) + Enable advisor + Extra arguments (one per line) + Extra arguments may not set --mode, --cwd or --resume. + Launch configuration + Profile + Enable advisor + Extra arguments (one per line) + Save + Clear + Cancel + Cancel + Delete + Rename + Close + Retry + Transcript copied + Could not copy the transcript + Transcript truncated to %1$d characters + Could not copy + Session ID copied + Could not export the session + Export saved + Could not import the session + + %1$d running + %1$d unread + %1$d exited + Share HTML + + No matching sessions + + + + Loading conversation… + Could not load the conversation + Retry + No messages yet + Type a prompt below to start the session. + Send + Stop + Jump to latest + Copy + Read + New session + Forking… + Fork cancelled — the agent is still running + The session could not be forked + Branch summary + Thinking + Dismiss + Sending… + omp is busy — your message may still be running. + Stop not confirmed yet — omp is busy. + %1$d queued + Reading aloud + Attached image + Done + Failed + Active branch + No branches + Branches + … %1$d more entries + Compaction + Conversation compacted + Earlier turns were folded into a summary by the agent. + No summary + File context (%1$s) + %1$d read + %1$d modified + Modified files + Read files + No message + Task subagents + %1$d subagents + Output saved + Could not save output + View full output + Loading full output… + Download + Load earlier messages + Loading earlier messages… + + Updating… + Running… + (no output) + Process details + %1$d messages + %1$d message + %1$d tool calls + %1$d tool call + Expand + Collapse + Show full input + Collapse input + Earlier turns are compacted into a summary + Pre-compaction history shown + Show earlier + Hide earlier + Retrying (%1$s) + Retrying + Reconnecting to the server… + Offline — live updates paused + compacting + Connecting… + working + waiting for model + running %1$s + running %1$s (+%2$d) + running command + Thinking… + Writing… + (+%1$d) + + You + Assistant + Subagent + Today + Yesterday + Full date and time + Show full + Large message (%1$s) — show + Edit from here + loading pages + %1$s tok/s + Loading… + Details + Advisor + Show all + Show rendered + Hide details + No app can open this link + Tap to load image from %1$s + Interrupted by user + Session details + Branches + diff --git a/android/app/src/main/res/values/strings_btw.xml b/android/app/src/main/res/values/strings_btw.xml new file mode 100644 index 000000000..afea4cbba --- /dev/null +++ b/android/app/src/main/res/values/strings_btw.xml @@ -0,0 +1,33 @@ + + + + Side question + Answering… + Answered + Cancelled + Failed + Interrupted + Thinking… + Cancel + Copy answer + Answer copied + Could not copy the answer + Close side question + Expand + Collapse + Follow up + Follow-up question + Ask a follow-up… + Ask + + + Side questions + No side questions yet. Ask one with /btw <question>. + + + /btw requires a newer omp. Update omp, then restart the session. + Side question failed + Side question failed: %1$s + Side questions can\'t include attachments. Remove them, or send the message as a normal prompt. + No active session + diff --git a/android/app/src/main/res/values/strings_chat_search.xml b/android/app/src/main/res/values/strings_chat_search.xml new file mode 100644 index 000000000..fed0353e8 --- /dev/null +++ b/android/app/src/main/res/values/strings_chat_search.xml @@ -0,0 +1,14 @@ + + + + Search chat + Search transcript + %1$d/%2$d + No matches + Previous match + Next match + Close search + Older history is not shown in this transcript (compacted). + Search full history + The searched message is not part of this transcript. + diff --git a/android/app/src/main/res/values/strings_composer.xml b/android/app/src/main/res/values/strings_composer.xml new file mode 100644 index 000000000..04cff9876 --- /dev/null +++ b/android/app/src/main/res/values/strings_composer.xml @@ -0,0 +1,139 @@ + + + + Message the agent… + Ask anything to start a session… + Queue + Shell command — runs in the project directory + Shell command — output stays out of the agent\'s context + Running shell command + Attach files + Attach image + Paste image + Take photo + Reading attachments… + %1$d image(s) skipped: images up to %2$s are supported. + %1$d file(s) skipped. + + + Set the goal for this session + [goal] + Create a step-by-step plan + [task] + Review project state and changes + [scope] + Fix a reported issue + [issue] + Write and run tests + [target] + Explain code or a concept + [thing] + Simplify code while preserving behavior + [target] + Stage and commit changes + [message] + Get an independent advisory review of the current work + [topic] + Repeat a task until done, up to N attempts + [count] [task] + Ask a side question without adding it to the conversation + [question] + Compress context, optionally with instructions + Reload extensions, skills, prompts, and tools + Set the session display name + Show session message, token, and cost stats + Copy the last assistant message + + + A shell command is still running, so the message was queued and runs when it finishes. + The run was stopped, but the queued messages may not have come back to the composer. Check the queue and the chat before resending. + The run was stopped, but some queued messages could not be taken back. The agent may still deliver them. + Usage: %1$s %2$s + Advisor is disabled — enable it with the advisor toggle in the composer menu. + No active session + Usage: /name <name> + Session renamed to %1$s + No assistant message to copy + Copied the last assistant message + Context compacted + Reloaded session resources + /%1$s is not a command this session can run — it was sent as plain text. + This message is no longer pending in that queue and could not be cancelled. + The queued follow-up is no longer available to steer. + Unable to create a session for the shell command + Could not check whether the agent is running — your message stays in the composer. + Your message was not sent — the chat was left before the agent\'s state arrived. Your draft is still in that chat. + Another omp process is running this chat, so the message was not sent. Use “Send anyway (copy)” above the composer to continue in a copy. + Another omp process is running this chat, so the command was not applied. Nothing was sent to omp. + + + Running somewhere else + Another omp-web instance is running this chat. You can read along here; sending would create a copy with a new chat id. + A terminal omp process is running this chat. You can read along here; sending would create a copy with a new chat id. + Another omp process is running this chat. You can read along here; sending would create a copy with a new chat id. + Send anyway… + Send into a copy? + omp lets only one process write a chat. Another one is running this one, so your message starts a copy with a new chat id. The original stays as it is and keeps running elsewhere; the copy contains your message. + Send anyway (copy) + Keep reading + + + Continued in a copy + Another omp process owned this chat, so your message continued in a copy. The original is unchanged and still belongs to that process. + Open original + Open copy + Dismiss + + + Queued follow-up + Queued steer + + Steer + Deliver this message as a steer + Edit + Delete + Remove this queued message + Cancel this queued message? Once cancellation is confirmed, it will be removed from the agent\'s queue. A message already delivered to the agent cannot be cancelled. + Delete + Cancel + Queued · %1$d + Show all queued prompts (%1$d) + + + Unsent drafts + You have unsent text in %1$d chat(s). Leave anyway? + Leave + Stay + + + Tools + + + Run + + + Retrying (%1$s) + Retrying + Stop waiting + + + Share to a chat + New chat + %1$d shared item(s) + The shared file could not be attached. + Cancel + + + The camera could not take a photo. + No image on the clipboard. + Add attachment + Minimize input + Expand input + Attachments + Remove attachment + Searching all files… + No tools + Default tools + All tools + diff --git a/android/app/src/main/res/values/strings_controls.xml b/android/app/src/main/res/values/strings_controls.xml new file mode 100644 index 000000000..f626b3784 --- /dev/null +++ b/android/app/src/main/res/values/strings_controls.xml @@ -0,0 +1,72 @@ + + + + Select model + Change model + Search models… + Providers + Loading models… + No available models + No matching models + Close + Back to providers + + + Reasoning + Change reasoning level + Applies to next prompt + + + Fast + Slow + Advisor + Turn Fast mode on or off for this model + Slow: changes a saved omp setting shared by every session and terminal, not just this one. + Slow: switches only this session to the provider\'s slower, cheaper flex tier. + Enable advisor for this chat + Disable advisor for this chat + + + low priority until %1$s + low priority until %1$s · %2$s%% left + low priority + low priority · %1$s%% left + limit reached · wrapping up + limit reached · wrapping up · resets %1$s + limit reached · wrap-up, then extra usage + + + Context + No session yet. Send the first message to see context stats. + Loading stats… + %1$s window + max out %1$s + %1$s t/s · avg %2$s + %1$s t/s + Compact + Stop compaction + Compacted + %1$s compacted + %1$s %2$s → %3$s tokens (%4$s saved) + Session Info + Messages + Tokens + Name + File + ID + User + Assistant + Tool Calls + Tool Results + Total + Input + Output + Cache Read + Cache Write + Cache Hit + Cost + In-memory + Copy file path + Copy session ID + Copied + diff --git a/android/app/src/main/res/values/strings_dictation.xml b/android/app/src/main/res/values/strings_dictation.xml new file mode 100644 index 000000000..284085cdc --- /dev/null +++ b/android/app/src/main/res/values/strings_dictation.xml @@ -0,0 +1,40 @@ + + + Start dictation + Uploading recording… + Transcribing… + Pause recording + Resume recording + Stop and transcribe + Cancel dictation + Discard recording + Retry transcription + Play recording + Pause playback + %1$s of %2$s + + Microphone access + ompweb records your voice and sends it to your server for transcription. The recording stays on the server until the transcript is claimed or discarded. + ompweb uses this phone\'s speech recognition. The audio is processed on the device; only the recognized text is inserted into your message. + Allow + Not now + + Microphone access is denied. Allow it for ompweb in Android Settings to dictate. + Recording is not available on this device + No speech detected + Transcription failed + Transcription timed out + Transcription job not found + Too many transcriptions in progress + Recording not transcribed yet + + Listening… + Done + Cancel + + Speech recognition on this phone is not available (needs Android 12+ and the offline language pack). Use server transcription or install the language pack. + On-device speech recognition could not run. Try again. + This language is not installed for on-device recognition. Pick another dictation language or download the language data. + Speech recognition is busy. Try again in a moment. + Speech recognition failed + diff --git a/android/app/src/main/res/values/strings_extension.xml b/android/app/src/main/res/values/strings_extension.xml new file mode 100644 index 000000000..9c671641f --- /dev/null +++ b/android/app/src/main/res/values/strings_extension.xml @@ -0,0 +1,42 @@ + + + + extension request + Questions from the agent + Recommended + Other: type your own answer + Cancel + Confirm + Next + Submit + Close + Extension panel + Extension terminal input + + + Open in your browser: %1$s + Blocked an unsafe URL %1$s + %1$s: %2$s + + + An extension read your clipboard + An extension copied text to your clipboard + Allow clipboard access? + An extension in this chat wants to read your clipboard. You can allow it for this chat only. + Allow + Deny + An extension wants to open:\n%1$s + %1$s\n%2$s + Open + Dismiss + + + Open link? + The agent wants to open:\n%1$s + Open link from another session? + An agent in a session you are not viewing wants to open:\n%1$s + Open file from another session? + An agent in a session you are not viewing wants to open:\n%1$s + Open + Cancel + diff --git a/android/app/src/main/res/values/strings_files.xml b/android/app/src/main/res/values/strings_files.xml new file mode 100644 index 000000000..1ce490e9f --- /dev/null +++ b/android/app/src/main/res/values/strings_files.xml @@ -0,0 +1,77 @@ + + + + Files + Explorer + Close the files panel + File tab actions + Close %1$s + Close other tabs + Close all tabs + Open files + + + Refresh files + Collapse all + Search files + Search files… + No files match. + Search failed (%1$s) + Showing the first %1$d matches. + No workspace + Open a session or pick a workspace to browse its files. + Empty directory + Failed to load files (%1$s) + Retry + Upload files + Checking for existing files… + Uploading %1$s (%2$d%%) + File %1$d of %2$d + Files already exist + %1$s already exist in this directory. + These cannot be replaced: %1$s + Replace all + Skip existing + Cancel + Upload finished + %1$d uploaded + %1$d skipped + %1$d failed + Upload failed + Mention uploaded files + Dismiss + + + Open file + Open directory + File actions + Mention in the composer + Copy path + Path copied + Download + File saved + Could not download the file + Share + Could not share the file + Reveal in explorer + + + Source + Preview + Diff + Wrap long lines + Do not wrap long lines + Live + line %1$d + Binary file + This file has no text preview (%1$s, %2$s). Download it to open it elsewhere. + unknown type + The document preview could not be loaded. + Page %1$d of %2$d + Previous page + Next page + This PDF could not be rendered. + No textual diff for this file. + Failed to load the diff (%1$s) + No preview is available for this file. + diff --git a/android/app/src/main/res/values/strings_git.xml b/android/app/src/main/res/values/strings_git.xml new file mode 100644 index 000000000..0129c75f6 --- /dev/null +++ b/android/app/src/main/res/values/strings_git.xml @@ -0,0 +1,65 @@ + + + + Git + Filter files… + Clear filter + Refresh changes + Loading changes… + Failed to load changes (%1$s) + Retry + Not a git repository + Open a git checkout to review changed files and diffs here. + No workspace + Open a session or pick a workspace to review its changes. + No changes + The working tree matches HEAD. + No matching files + Generated, vendored, documentation, or -diff files per .gitattributes + Open file + + %1$d changed file + %1$d changed files + + + %1$d file marked in .gitattributes + %1$d files marked in .gitattributes + + + + Modified + Added + Deleted + Renamed + Untracked + Conflict + generated + vendored + docs + no diff + + + Worktrees + Worktrees are available in Git repository roots. + Loading worktrees… + New worktree… + Create a worktree checkout for a branch + branch name + Create + Creating… + Cancel + Force + main + Switch worktree + Remove worktree checkout %1$s; the branch is kept + Checkout at %1$s + Uncommitted changes. Force remove checkout? + Enter a branch name. + Invalid branch name. + That checkout directory already exists. + That directory is not a worktree of this repository. + The main worktree cannot be removed. + Access denied. + That directory does not exist. + The worktree has uncommitted changes. + diff --git a/android/app/src/main/res/values/strings_notify.xml b/android/app/src/main/res/values/strings_notify.xml new file mode 100644 index 000000000..4c91dbc2b --- /dev/null +++ b/android/app/src/main/res/values/strings_notify.xml @@ -0,0 +1,62 @@ + + + + Needs input + The agent is waiting for your answer or approval. + Errors + A run failed, crashed, or ran out of retries. + Completed + A prompt and the background work it started are done. + Model switch + omp switched models on its own (fallback or hand-off). + Connection status + The silent status of the notification service. + Agent messages + Short messages agents send through the notify host tool. + + + (no vibration) + + + + %1$d session in %2$s + %1$d sessions in %2$s + + + + ompweb + Reconnecting… + Server unreachable + + Connected · %1$d agent running + Connected · %1$d agents running + + + + ompweb notification + + + Reply + Type a reply… + Sent + Already answered + Failed — open app + Notifications are working. + + + Allow notifications? + ompweb keeps one connection to your PC so you learn when an agent finishes, needs an answer, or fails. Allow notifications to see those alerts. Battery optimisation can delay them — allow ompweb to run unrestricted in the battery settings. + Allow notifications + Not now + Could not register this device + + + Test notification sent + Test notification shown + Android does not allow notifications for ompweb + Could not send the test + + %1$d agent running + %1$d agents running + + diff --git a/android/app/src/main/res/values/strings_notify_ui.xml b/android/app/src/main/res/values/strings_notify_ui.xml new file mode 100644 index 000000000..508be05c1 --- /dev/null +++ b/android/app/src/main/res/values/strings_notify_ui.xml @@ -0,0 +1,74 @@ + + + + Notifications + + Notifications, %1$d unread + Notifications, %1$d unread + + Notifications + Mark all as read + Clear notifications + Clear notification history? + All entries are removed from the server, for every device. + Clear + Cancel + Retry + No recent notifications + Dismiss notification + omp session + + + Task finished. + Waiting for your answer: %1$s + Waiting for your answer. + Run failed: %1$s + Run failed. + Switched %1$s → %2$s: %3$s + Switched %1$s → %2$s. + Notifications are working. + Session activity. + + + Notifications + Get notified about sessions you are not looking at. The server keeps these settings per device. + Enable notifications + Shows a notification when a session finishes, waits for input or fails. Asks Android for permission when you turn this on. + Android permission + Not requested yet + Allowed + Blocked in Android settings + Not needed on this Android version + Allow + Open settings + Android has not allowed notifications for ompweb yet. Nothing reaches the notification shade until it does. + Android blocks notifications for ompweb. Allow them in the app settings, then return here. + Battery optimization + Not restricted + Restricted · notifications may be delayed while the phone sleeps + Fix + Connection + Off + Connecting… + Connected + Server unreachable + Notify me about + Feedback + Vibrate + One short pulse when a task finishes, a double pulse when the agent needs an answer, one long pulse on errors. Never when you send. Follows silent mode and Do Not Disturb. + Task finished + A prompt and the background work it started are done. Subagents never notify. + Waiting for input + The agent asks a question or needs an approval. + Run failed + A run stops with an error after retries and fallbacks, or the omp process exits. + Model switched automatically + omp switches models on its own: a fallback after a refusal, rate limit, or usage limit, or a prewalk / plan hand-off. + While you are using ompweb elsewhere + Show an in-app toast + Always send a system notification + Send test notification + Dismiss + + Open + diff --git a/android/app/src/main/res/values/strings_panels.xml b/android/app/src/main/res/values/strings_panels.xml new file mode 100644 index 000000000..f95ec06b7 --- /dev/null +++ b/android/app/src/main/res/values/strings_panels.xml @@ -0,0 +1,59 @@ + + + + Expand + Collapse + + + Tasks + %1$d/%2$d complete + Show all tasks + Show fewer tasks + Blocked: %1$s + Task list + Pending + In progress + Completed + Blocked + Abandoned + + + Subagents + %1$d running · %2$d total + Completed (%1$d) + retrying %1$d/%2$d + attempt %1$d + Running + Done + Failed + Aborted + Detached from this session + %1$d nested + %1$s tok + %1$s/%2$s ctx + + + Subagent transcript + Close transcript + Task + Result + model + effort + Show transcript + Hide transcript + Load more + Loading… + No output yet + No messages yet + Output truncated + + + Goal active + Expand active goal + Collapse active goal + Planning in progress + %1$s %2$d -> %3$d tokens (%4$d saved) + Compacted + %1$s compacted + Compaction failed: %1$s + diff --git a/android/app/src/main/res/values/strings_sessions.xml b/android/app/src/main/res/values/strings_sessions.xml new file mode 100644 index 000000000..e6c6ca1e7 --- /dev/null +++ b/android/app/src/main/res/values/strings_sessions.xml @@ -0,0 +1,48 @@ + + + + Get started + 1. Add a project folder that exists on the PC. + 2. Start a session and send your first message. + Open workspaces + + Naming… + Title generated + Auto-name failed + + Current model + ↑↓ navigate · Enter open · Esc close + + Start a new session + Ask anything. The session runs in the workspace above. + App %1$s · Server %2$s + + This workspace is not available on the server. + + + Sessions + In messages + Search all messages + Searching messages… + Searching older messages… (page %1$d of %2$d) + No matches in messages + Try again + Load more results + The server stopped early; more matches may exist. + + %d session file was too large to search and was skipped. + %d session files were too large to search and were skipped. + + You + Assistant + Pinned + Pin + Unpin + + + Usage: %1$s + %1$s %2$d%% + diff --git a/android/app/src/main/res/values/strings_settings.xml b/android/app/src/main/res/values/strings_settings.xml new file mode 100644 index 000000000..8f2a2a561 --- /dev/null +++ b/android/app/src/main/res/values/strings_settings.xml @@ -0,0 +1,270 @@ + + + + Settings + Search settings + %1$d results for \"%2$s\" + No settings match \"%1$s\" + Section + Native OMP + UI + Workspace + Select a workspace to change these settings. + Open a project or a session to enable this tab. + Retry + Cancel + Copy + Copied + Close + Refresh + Dismiss + Native OMP + + + Interface & Behavior + UI preferences, completion sound, submission mode + Safety & Approvals + Tool safety rules, YOLO mode, terminal permissions + AI Model Defaults + Reasoning budget, verbosity, personality, scratchpad + API Keys & Providers + Connected OAuth accounts, API keys, and model registry + Usage + Tokens, costs, cache analytics, and model breakdown + Agent & Intelligence + Advisor, memory, autolearn, compaction and retry + Agents + Task agents, model settings, and tool policy + Extensions & Tools + MCP servers, managed skills, and OMP plugins + Notifications + Desktop and mobile notifications for your sessions + System & Updates + App updates, runtime version, and active session restart + + + Interface & Behavior + Theme + Choose the app\'s colour theme. + System + Chat Font Size + Adjust text size across conversation messages, code blocks, and markdown output. + Small + Default + Large + Extra large + Interface Scale + Adjust overall UI zoom and display density across dialogs, buttons, and toolbars. + Compact + Standard + Comfortable + Large + Touch Targets + Adjust interactive target sizes for buttons and toolbar controls. + Automatic + Compact + Accessible + Completion sound + Play a tone when the agent completes a run. + Keep tool calls collapsed + Show only compact headers while tools execute. + Expand thinking blocks by default + Open every thinking block as it appears, including while it streams. Collapsing a block by hand still works. + Hide tool activity while working + Fold a running turn\'s tool calls into one Process details row, the same way a finished turn folds them. + Open agent links without asking + Links the agent opens from the session you are viewing open right away. Links from other sessions always ask first. + Auto-read assistant responses + Automatically read aloud new assistant replies when completed. + Speech Voice + Select the installed device voice for text-to-speech reading. + Engine default + No installed voices found. Install a text-to-speech engine in the system settings. + Word completion + Ghost text from omp\'s word prediction. Auto enables it only with a hardware keyboard. + Automatic + On + Off + Message during active run + What the composer does on submit while the agent runs. Steer interrupts; Queue follow-up delivers after finish. + Steer the current run + Queue follow-up + + + Usage + Tokens, costs, cache analytics, and model breakdown + Today + 7 days + 30 days + 90 days + This month + All time + Daily + Monthly + Projects + Total cost + Total tokens + %1$d active days + %1$s per active day + Cache reads + Cache savings + Provider reported %1$s%% · Model priced %2$s%% · Unpriced %3$s%% + Providers + %1$s%% + Breakdown + No usage recorded in this range. + Spend over time + %1$d transcripts · %2$d records · %3$ss + Loading usage… + No usage in this range yet. + + + Server settings + Resume running sessions after a restart + When omp-web restarts while agents are working, restart those sessions and tell each agent: \"Session interrupted and resumed. Continue as you would have done without the interruption.\" Work in progress at the moment of the restart, such as a running command, is lost. + Agent environment variables + Extra environment variables passed to the omp process, one KEY=VALUE per line. Applied to sessions started after saving. + KEY=VALUE, one per line + Save environment + ompweb update + Installed: %1$s + Update available: %1$s + Up to date + Update checks are disabled on this server. + Update command + Check for updates + OMP update + omp version: %1$s + New version: %1$s + Not checked yet + Restart sessions + Restart every live agent session after updating omp. + Mobile app + Bind: %1$s (%2$s), port %3$d + loopback only + network + unknown + No password configured + The password configuration is invalid; every request is rejected. + Addresses + No network address found. Use Tailscale or join the same network as the server. + Setup commands + Windows background service + Running on port %1$d + Stopped + Service and shortcuts installed + Service not installed + Start with Windows + Desktop shortcut + Present + Missing + Install service & shortcuts + Uninstall service + Remove the background service, tray monitor, and shortcuts? Configuration files are kept. + Start + Stop + Restart + About + Android app + Server + API %1$d + Loading server settings… + + + Provider usage + Loading provider usage… + Provider usage is unavailable. + No limits reported + %1$s %2$d%% + resets in %1$s + No provider limits available. + + + Cost + Tokens + Model + Day + Project + Name + Cost + Share + Tokens + %1$s to %2$s + %1$s · %2$s + + + ompweb update available + OMP update available + v%1$s → v%2$s + Details + Dismiss + + + Android app update available + v%1$s → v%2$s + Install update + Try again + Cancel + Open settings + Release page + Dismiss + Downloading update… %1$d%% + Downloading update… + Verifying the update… + Installing the update… + Confirm the installation in the Android dialog. + Confirm install + Allow ompweb to install updates in Android settings. + Android app update + Installed: %1$s + GitHub Releases, or a custom feed that describes the newest APK. + Checks on start, after an app update and when you return after 30 minutes + GitHub Releases + Custom feed URL + https://example.com/ompweb/latest.json + Save + Check for updates + Checking for updates… + Version %1$s is available. + You are on the latest version. + The update source could not be reached. Try again later. + Install update + Try again + Cancel + Confirm install + Open settings + Open release page + Dismiss + The app downloads the APK, verifies its SHA-256 and the signing certificate of the installed app, and then hands it to Android. App data stays. + Update and restart + Update now + ompweb update available + A new version (v%1$s) can be installed automatically. Live ompweb-managed sessions will stop cleanly before files are replaced, and the app will reconnect after the server restarts. + Update omp? + omp %1$s will be installed. Live ompweb-managed sessions will be restarted after the update, and the app will reconnect when the server is back. + Not now + Release notes + What\'s new in v%1$s + No release notes for this version. + Could not load the release notes. + View release on GitHub + Loading release notes… + Preparing the update… + Updating and waiting for the server to return… + Installing the new version… + Finishing the update… + Updated to v%1$s. + The update failed: %1$s + The update failed. + The update did not finish in time. Check the server. + %1$d problem(s) in the environment variables + + Dictation engine + Auto uses the connected server and switches to this phone\'s installed on-device speech model when no server is set up or the server reports no transcription service. On-device dictation never uploads audio; without a model it stops with an error instead of using a network recognizer. + Auto + On this phone + Server + Dictation language + Language for speech recognition on this phone. The server uses its own language setting. + Device language + diff --git a/android/app/src/main/res/values/strings_settings_models.xml b/android/app/src/main/res/values/strings_settings_models.xml new file mode 100644 index 000000000..672775a7f --- /dev/null +++ b/android/app/src/main/res/values/strings_settings_models.xml @@ -0,0 +1,231 @@ + + + + Connected + Custom + System + Loading… + Reload + Save + Saving… + Cancel + Close + Remove + Add + Copy + Copied + Dismiss + Back + Move up + Move down + Refresh + Required + Enter a valid http:// or https:// URL. + + + Connected accounts + Sign in to providers here. Credentials stay in omp on the server; this screen never reads or stores a key. + Search providers… + Add provider + Signed in + Other providers + OAuth + API key + Signed in + Not connected + No provider matches the filter. + Add a provider + Pick a provider to sign in, or define a custom endpoint in the Custom tab. + Custom provider… + + + Sign in + Opening the login stream… + Open sign-in page + Start over + Stop + Signed in. + Sign-in failed. + The login stream ended before omp answered. Try again. + Verifying… + Code or redirect URL + Paste the code or the full callback URL + Submit + Enter this code on the provider page: + The code expires in about %1$d minutes. + Choose the account to continue with: + 1. Open the sign-in page and approve access. + 2. Copy the code or the redirect URL the provider shows and paste it below. + Blocked an unsafe sign-in URL; copy it and open it yourself if you trust it. + Finish this sign-in on the computer running omp + The provider redirects to a callback server on the PC (loopback address %1$s). A phone cannot reach that address, so open the link on the PC, then copy the full callback URL from its browser address bar into the field below. + Callback target: %1$s + + + Stored accounts + omp holds these credentials encrypted on the server. Disconnecting removes one of them. + No stored account for this provider. + This omp version cannot list or disconnect accounts here. Update omp, or run omp in a terminal and use /login. + Disconnect + Disconnect account? + omp will delete the stored credential for %1$s. Other accounts for this provider stay connected. + API key status + Configured — %1$d model(s) available. + No key or model is configured for this provider. + ompweb cannot store or remove API keys. + Stored in models.yml on the server. Leave empty to use the provider environment variable. + Run omp in a terminal and use /login, set the provider environment variable, or configure an apiKey on a custom provider in models.yml. + + + Custom providers + Providers defined in models.yml. Changes apply to omp after saving. + New provider + Provider name + Base URL + API key + sk-… + API protocol + Quick endpoints + Authentication + API key + None + OAuth + %1$d model(s) + No custom provider yet. + Key set + Key missing + Delete provider? + \"%1$s\" and its models are removed from models.yml when you save. + Models + Add model + From catalog + No model defined. omp will not report this provider until you add one. + Model ID + Display name + API override + Inherit from provider + Reasoning / thinking model + Image input + Thinking levels + Reset to auto + Custom effort + Default + Off + Token limits + Context window + Max output tokens + Cost per million tokens (USD) + Input + Output + Cache read + Cache write + Test config + Testing… + Resolved + Failed + Resolves the model through a throwaway omp registry. Credentials are not contacted. + Remove model? + \"%1$s\" is removed from this provider when you save. + Model catalog + Search models.dev for metadata and pricing. Adding one fills id, limits and cost. + Search models, e.g. gpt-5 or claude + No match. Try a shorter query. + Already added + models.yml cannot be parsed + The editor stays read-only so a broken file is never replaced by an empty form. Fix it by hand on the server, or overwrite it explicitly. + Overwrite file… + Overwrite models.yml? + Every provider currently in the file is deleted and replaced with the providers shown here. The previous content is lost. + models.yml changed on the server + The file was changed outside this app since it was loaded. Reload to get its current content; your unsaved edits will be discarded. + Reload file + This file contains entries this editor cannot edit (%1$s). Saving is blocked so they are not lost; fix the file on the server. + + + Registry + Composer picker + Model roles + Retry & fallback + Native model registry + Controls what omp offers as a model. Changes are written to omp settings immediately. + Restrict to selected models + Models without a check are hidden from omp. + Every reported model is available. + omp reports no models right now. + Disabled providers + A disabled provider stays signed in but its models are not offered. + Provider preference + Order in which omp prefers providers for the same model id. + The settings file uses path-scoped entries, which this editor cannot rewrite safely. Edit omp settings on the server to change them here. + Settings are not loaded. Reload before changing the registry or fallbacks. + Composer model picker + Choose which models the chat composer offers. Stored on this device only. + %1$d of %2$d models visible + Filter models… + No reported model matches the filter. + %1$d provider(s) + Show all models of this provider + This provider is disabled in the registry. Enable it to see its models. + This provider is signed in but reports no models. + Enable in omp + Model roles + Assign a model (optionally with an effort) to each of omp\'s roles. Empty means the role uses its default. + No override + Model default + %1$s (not available) + Save roles + Thinking level + Loading roles… + Retry & model fallback + How omp retries a failed turn and which models it falls back to. + Retry transient errors + Allow model fallback + Retry attempts + Return to primary model + After the cooldown expires + Never + Fallback chain for + Select a fallback model + No explicit chain: omp uses its built-in fallback order. + + + Agents + Agent definitions from omp, this device profile and the workspace. Bundled agents are read-only templates. + Search agents… + New agent + Loading agents… + No agent found in this workspace. + No agent matches the search. + Select an agent to edit it. + Unpack omp\'s bundled templates to start from a working definition. + Unpack templates + Bundled + User + Project + Invalid definition + Bundled agents are read-only. Create a %1$s copy to edit it. + The workspace is not available to ompweb, so project agents cannot be read or written. Only user agents are shown. + Name + designer + Description + One sentence the main agent sees when it picks a subagent + Model roles + @designer, @smol + Thinking level + Default + Spawns + task, reviewer or * + Tools + read, edit, bash, task + System prompt + Instructions for this agent… + Scope + Create + Save + Delete agent? + \"%1$s\" is deleted from the %2$s scope. + Copy path + The file is written atomically: either the complete agent is stored or nothing changes. + New agents start with the name, description and prompt below. + Scan warnings + diff --git a/android/app/src/main/res/values/strings_settings_native.xml b/android/app/src/main/res/values/strings_settings_native.xml new file mode 100644 index 000000000..dccf4303a --- /dev/null +++ b/android/app/src/main/res/values/strings_settings_native.xml @@ -0,0 +1,229 @@ + + + + + Settings are unavailable. + Loading settings… + Saving… + Retry + Dismiss + Select a project workspace to view and edit its project MCP configuration. + + + Skill startup notices + Show conflicts and redundant skill copies when an OMP session starts. Manual diagnostics remain available when notices are off. + + + Tool Safety & Approvals + Tool execution safety rules and permission prompts. Persisted in ~/.omp/agent/config.yml. + Approval Mode + Choose when OMP asks before tool calls. + Always ask + Allow writes + Auto approve (YOLO) + Bash Override + Override default approval policy specifically for terminal commands. + Allow + Deny + Extension Tool Requests + Automatically approve extension tool authorization requests. + Auto approve + + + AI Model Defaults + Configure default reasoning effort, response verbosity, personality tone, and thinking display. + Reasoning + Default effort level for thinking-capable models. + Auto Thinking Source + Classifier judges each prompt. Vendor default prefers the publisher\'s effort, then omp\'s per-model default, then normal Auto fallback; the Auto ceiling still applies. + Classifier + Vendor default + Verbosity + Response detail level for supporting providers. + low + medium + high + Personality + Style included in OMP\'s system prompt. + Hide Thinking Blocks + Hide model reasoning from output view. + External Thinking + Private scratchpad reasoning via think tool. + + + Context Compaction + OMP automatically compacts oversized context to prevent hitting context limits. + Automatic Compaction + Compact context before model context limit is hit. + Continue After Compaction + Resume task execution after compaction completes. + Compaction Method Order + Preferred fallback order for automatic context maintenance; unavailable or failed methods advance to the next choice. + No method selected: automatic compaction will not run. + %1$s, position %2$d + Move %1$s up + Move %1$s down + Server compaction + Use provider-native server compaction (OpenAI Responses compact, Anthropic compaction beta) when the active route supports it + Snapcompact + Archive history onto dense bitmap images the active vision model reads back; no LLM call + Handoff + Generate a handoff document and continue from it as the compaction summary + Soft compaction + Summarize in place with a compaction model without using server compaction + Shake + Drop recoverable heavy content in place without an LLM call + Compact Mid-Turn + Check context limits between tool execution steps. + + + Memory & Auto-Learn + Durable project memory storage and automatic lesson capture. + Memory Backend + Where durable knowledge is stored across sessions. + Off + Local summaries + Mnemopi SQLite + Hindsight + Enable Auto-Learn + Capture reusable lessons after completed runs. + Private Capture Turn + Run private lesson-capture turn at completion. + Memory Scope + Scoping for Mnemopi knowledge storage. + Per project + Per project, tagged recall + Global + Recall on Session Start + Load relevant memories into first turn. + Retain Completed Turns + Store completed conversation turns in memory. + + + Automatic Retry + Rules for automatically retrying failed turns. + Automatic Retry + Retry failed turns automatically. + Max Attempts + Retry limit before giving up. + Model Fallback + Fall back to alternative model when retries exhaust. + + + MCP Resource Updates + Inject server resource updates into conversation. + Load Project MCP Servers + Allow project-root MCP configuration to be discovered. + Render MCP Markdown + Render non-JSON MCP results as Markdown in the transcript. + + + Configured MCP Servers + Refresh live MCP status + No MCP servers configured + User level (%1$s) + Live status unavailable: %1$s + No OMP servers + Project MCP Servers + %1$d/%2$d enabled + · %1$d invalid + No servers + Add server + Server name + OMP server configuration (JSON) + Check + Save server + Remove + connected + connecting + disabled + not connected + configured + off + invalid + + + Skills + No skills found + Add custom skill + Add Skill + Search skills + e.g. react, testing, deploy + Search + Searching… + Search skills.sh to discover and install skills for your agent. + Install + ✓ Installed + Installing… + %1$s installs + Source + Version + unknown + This skill lives outside the skill folders omp-web edits (for example a plugin or package install), so it can\'t be toggled here + Visible in model prompt — click to disable + Hidden from model prompt — click to enable + Check + Checking... + Up to date + Automatic checks unavailable + Check failed + Update + Updating... + Update available + Check updates + %1$d updates + project / skills.sh + global / skills.sh + project + global + path + Dormant (%1$d) + Install scope + + + Plugins + No plugins configured + Add plugin + Add plugin + Source + Install + Installing... + Examples + Status + Version + Package + Resources + Installed path + Cwd + Resolved Resources + No resolved resources + Loaded + Installed + Missing + Disabled + disabled + filtered + Package is disabled + Enable package + Disable package + Update + Updating... + Remove + Removing... + Reload current session + Reloading... + Open a session to reload + Remove this plugin? + This will uninstall %1$s and drop its resources. + Refresh + Unknown + %1$d diagnostics + %1$d diagnostic + %1$d ext · %2$d skills · %3$d prompts · %4$d themes + project + global + configured %1$s + Close + Cancel + diff --git a/android/app/src/main/res/values/strings_skill_diagnostics.xml b/android/app/src/main/res/values/strings_skill_diagnostics.xml new file mode 100644 index 000000000..1f98d99c3 --- /dev/null +++ b/android/app/src/main/res/values/strings_skill_diagnostics.xml @@ -0,0 +1,36 @@ + + + + Skill diagnostics + Details + Close skill diagnostics + Dismiss skill notice + Default + Variant + Redundant copy + No bare default is included. Invoke a namespaced variant explicitly. + Shared names do not imply the same skill lineage. These details come from OMP\'s actual resolution. + No conflicting skill variants or redundant installations. + Turn off + Saving… + Could not change skill startup notices. + Startup notices are still enabled for this session. + Skill diagnostics are unavailable. The session must be running on an OMP version that supports diagnostics RPC. + Source: %1$s + Identical to %1$s (%2$s) + %1$d conflicting skill name + %1$d conflicting skill names + %1$d redundant copy deduplicated + %1$d redundant copies deduplicated + Selected by provider priority, then discovery order. + Custom-directory skills override provider skills. + Authored skills override registry-installed skills. + + + System prompt + Close system prompt + Loading system prompt… + System prompt is empty (tools are disabled) + System prompt has not loaded yet + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..b422392f0 --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/android/app/src/main/res/xml/backup_rules.xml b/android/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 000000000..af1a48fd7 --- /dev/null +++ b/android/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/xml/data_extraction_rules.xml b/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..70a17f0dc --- /dev/null +++ b/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..5da716a69 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..5b32ac909 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/a11y/AccessibilitySemanticsTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/a11y/AccessibilitySemanticsTest.kt new file mode 100644 index 000000000..d4c73134c --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/a11y/AccessibilitySemanticsTest.kt @@ -0,0 +1,140 @@ +package io.github.kahme247.ompweb.a11y + +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.performClick +import io.github.kahme247.ompweb.chat.ChatState +import io.github.kahme247.ompweb.screenshot.ChatTranscriptFixture +import io.github.kahme247.ompweb.screenshot.ComposerFixture +import io.github.kahme247.ompweb.screenshot.ComposerPanelsFixture +import io.github.kahme247.ompweb.screenshot.composerScreenshotControllers +import io.github.kahme247.ompweb.ui.chat.ChatFooter +import io.github.kahme247.ompweb.ui.chat.ChatUiState +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionDialogHost +import io.github.kahme247.ompweb.ui.panels.extension.ExtensionUiController +import io.github.kahme247.ompweb.ui.theme.OmpWebTheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import io.github.kahme247.ompweb.net.ExtensionApi +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +/** + * TalkBack contract for the U1 surfaces, asserted on the Compose semantics + * tree (not on pixels): every icon-only control is labeled, tool rows are one + * button, panels expose a heading + button role, and run status changes are + * announced through a polite live region. + */ +@RunWith(RobolectricTestRunner::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(sdk = [36], qualifiers = "w411dp-h890dp-mdpi", application = android.app.Application::class) +class AccessibilitySemanticsTest { + + @get:Rule + val composeRule: ComposeContentTestRule = createComposeRule() + + @Test + fun composerAttachButtonIsLabeledAndTappable() { + val controllers = composerScreenshotControllers(CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)) + composeRule.setContent { + OmpWebTheme { + ComposerFixture( + controllers = controllers, + queue = io.github.kahme247.ompweb.chat.QueueSnapshot(), + autoRetry = null, + ) + } + } + composeRule.onNodeWithContentDescription("Add attachment").assertHasClickAction() + } + + @Test + fun panelHeaderIsAHeadingButton() { + composeRule.setContent { OmpWebTheme { ComposerPanelsFixture() } } + val header = composeRule.onNode( + hasText("Tasks") and hasClickAction() and + SemanticsMatcher.keyIsDefined(SemanticsProperties.Heading), + ) + header.assertExists() + composeRule.onNodeWithText("Add the Robolectric render harness").assertExists() + header.performClick() + composeRule.onNodeWithText("Add the Robolectric render harness").assertDoesNotExist() + } + + @Test + fun toolRowIsOneButtonWithMergedContent() { + composeRule.setContent { OmpWebTheme { ChatTranscriptFixture() } } + composeRule.onAllNodesWithTag("chat-tool-card-header").onFirst() + .assertHasClickAction() + .assert(hasText("edit", substring = true)) + } + + @Test + fun footerRunStatusIsAPoliteLiveRegion() { + composeRule.setContent { + OmpWebTheme { + ChatFooter(ChatUiState(engine = ChatState(agentRunning = true))) + } + } + composeRule.onNode( + hasAnyDescendant(hasText("working")) and SemanticsMatcher.expectValue( + SemanticsProperties.LiveRegion, + LiveRegionMode.Polite, + ), + ).assertExists() + } + + @Test + fun extensionDialogTitleIsAHeading() { + val controller = ExtensionUiController( + api = object : ExtensionApi { + override suspend fun command(sessionId: String, command: JsonObject): JsonElement? = null + }, + scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate), + ) + controller.bind("session-1") + controller.onFrame( + buildJsonObject { + put("type", "extension_ui_request") + put("id", "ask-a11y") + put("method", "ask") + putJsonArray("questions") { + add( + buildJsonObject { + put("id", "q1") + put("question", "Continue?") + putJsonArray("options") { add(buildJsonObject { put("label", "Yes") }) } + }, + ) + } + }, + ) + composeRule.setContent { OmpWebTheme { ExtensionDialogHost(controller) } } + composeRule.onNode( + hasText("Questions from the agent") and + SemanticsMatcher.keyIsDefined(SemanticsProperties.Heading), + ).assertExists() + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/BranchTreeTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/BranchTreeTest.kt new file mode 100644 index 000000000..48d8618c4 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/BranchTreeTest.kt @@ -0,0 +1,86 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class BranchTreeTest { + + private fun node( + id: String, + role: String? = "user", + text: String = id, + children: String = "[]", + compressed: List = emptyList(), + previewRole: String? = null, + previewText: String? = null, + ): JsonObject { + val compressedJson = compressed.joinToString(",") { "\"$it\"" } + return Json.parseToJsonElement( + """ + { + "entry": {"id": "$id", "type": "message", "message": {"role": ${role?.let { "\"$it\"" } ?: "null"}, "content": "$text"}}, + "children": $children, + "compressedEntryIds": [$compressedJson], + "branchPreview": ${if (previewText != null) """{"role": "${previewRole ?: "user"}", "text": "$previewText"}""" else "null"} + } + """.trimIndent(), + ).jsonObject + } + + private fun parse(vararg roots: JsonObject): List = BranchTree.parse(roots.toList()) + + @Test + fun `parses nested tree and previews`() { + val leaf = node("b2", role = "assistant", text = "answer") + val child = node("b1", children = "[$leaf]") + val root = node("root", text = "hello", children = "[$child]") + val parsed = parse(root) + assertEquals(1, parsed.size) + assertEquals("root", parsed[0].id) + assertEquals("hello", parsed[0].text) + assertEquals(1, parsed[0].children.size) + assertEquals("b2", parsed[0].children[0].children[0].id) + } + + @Test + fun `detects a branch anywhere in the tree`() { + val singlePath = node("a", children = "[${node("b")}]") + assertFalse(BranchTree.hasBranch(parse(singlePath))) + val fork = node("a", children = "[${node("b")}, ${node("c")}]") + assertTrue(BranchTree.hasBranch(parse(fork))) + } + + @Test + fun `active path includes compressed ids and their ancestors`() { + val target = node("leaf", compressed = listOf("chain-1", "chain-2")) + val middle = node("middle", children = "[$target]") + val root = node("root", children = "[$middle]") + val tree = parse(root) + assertEquals(setOf("root", "middle", "leaf"), BranchTree.activePathIds(tree, "chain-2")) + assertEquals(setOf("root", "middle"), BranchTree.activePathIds(tree, "middle")) + assertTrue(BranchTree.activePathIds(tree, "missing").isEmpty()) + assertTrue(BranchTree.activePathIds(tree, null).isEmpty()) + } + + @Test + fun `rows mark the active branch and keep siblings`() { + val branchA = node("a1", text = "first") + val branchB = node("b1", text = "second") + val root = node("root", children = "[$branchA, $branchB]") + val rows = BranchTree.rows(parse(root), "b1") + assertEquals(1, rows.size) + assertEquals("root", rows[0].label) + assertTrue(rows[0].active) + assertEquals(2, rows[0].children.size) + assertEquals("first", rows[0].children[0].label) + assertFalse(rows[0].children[0].active) + assertTrue(rows[0].children[1].active) + assertFalse(rows[0].children[0].isLast) + assertTrue(rows[0].children[1].isLast) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatCoalescerTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatCoalescerTest.kt new file mode 100644 index 000000000..a2e3417c2 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatCoalescerTest.kt @@ -0,0 +1,107 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class ChatCoalescerTest { + + /** A scheduler that only runs when the test says so. */ + private class Deferred { + private var scheduled: (() -> Unit)? = null + val scheduler: FlushScheduler = { flush -> + scheduled = flush + { scheduled = null } + } + + fun run() { + val flush = scheduled + assertTrue("expected a scheduled flush", flush != null) + flush!!.invoke() + } + } + + private fun frame(json: String): JsonObject = Json.parseToJsonElement(json).jsonObject + + private fun update(text: String, sequence: Int) = frame( + """{"type":"message_update","message":{"role":"assistant","content":[{"type":"text","text":"$text"}]}, + "web":{"streamId":"s","sequence":$sequence}}""", + ) + + private fun toolUpdate(id: String, text: String) = frame( + """{"type":"tool_execution_update","toolCallId":"$id","toolName":"bash", + "partialResult":{"content":[{"type":"text","text":"$text"}]}}""", + ) + + @Test + fun `only the newest message update is dispatched`() { + val seen = mutableListOf() + val deferred = Deferred() + val coalescer = ChatFrameCoalescer({ seen += it }, deferred.scheduler) + + coalescer.push(update("a", 1)) + coalescer.push(update("b", 2)) + assertTrue(seen.isEmpty()) + deferred.run() + assertEquals(1, seen.size) + assertEquals(2L, seen.single().obj("web")!!.long("sequence")) + } + + @Test + fun `tool updates keep one latest snapshot per tool in first-seen order`() { + val seen = mutableListOf() + val deferred = Deferred() + val coalescer = ChatFrameCoalescer({ frame -> + seen += frame.string("toolCallId") + ":" + frame.string("type") + }, deferred.scheduler) + + coalescer.push(toolUpdate("t1", "one")) + coalescer.push(toolUpdate("t1", "two")) + coalescer.push(toolUpdate("t2", "three")) + assertTrue(seen.isEmpty()) + deferred.run() + + assertEquals(listOf("t1:tool_execution_update", "t2:tool_execution_update"), seen) + } + + @Test + fun `a normal frame flushes pending output first`() { + val seen = mutableListOf() + val deferred = Deferred() + val coalescer = ChatFrameCoalescer({ frame -> seen += frame.string("type")!! }, deferred.scheduler) + + coalescer.push(update("a", 1)) + coalescer.push(frame("""{"type":"agent_end","web":{"streamId":"s","sequence":2}}""")) + + assertEquals(listOf("message_update", "agent_end"), seen) + } + + @Test + fun `message end drops a pending message update but keeps tool output`() { + val seen = mutableListOf() + val deferred = Deferred() + val coalescer = ChatFrameCoalescer({ frame -> seen += frame.string("type")!! }, deferred.scheduler) + + coalescer.push(update("a", 1)) + coalescer.push(toolUpdate("t1", "x")) + coalescer.push(frame("""{"type":"message_end","web":{"streamId":"s","sequence":2}}""")) + assertEquals(listOf("message_end"), seen) + + deferred.run() + assertEquals(listOf("message_end", "tool_execution_update"), seen) + } + + @Test + fun `reset drops everything without dispatching`() { + val seen = mutableListOf() + val deferred = Deferred() + val coalescer = ChatFrameCoalescer({ seen += it }, deferred.scheduler) + coalescer.push(update("a", 1)) + coalescer.reset() + assertFalse(seen.any()) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatContextSyncTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatContextSyncTest.kt new file mode 100644 index 000000000..6602ea3c0 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatContextSyncTest.kt @@ -0,0 +1,174 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class ChatContextSyncTest { + + private val json = Json { ignoreUnknownKeys = true } + + private fun obj(text: String): JsonObject = json.parseToJsonElement(text).jsonObject + + private fun page( + mode: String, + baseEntryId: String?, + entryIds: List, + messages: List, + ): ContextPage { + val ids = entryIds.joinToString(",") { "\"$it\"" } + val payload = """ + {"mode":"$mode","baseEntryId":${baseEntryId?.let { "\"$it\"" } ?: "null"}, + "cursor":{"firstEntryId":"${entryIds.first()}","lastEntryId":"${entryIds.last()}"}, + "hasMore":false,"sessionId":"s1","leafId":"${entryIds.last()}", + "context":{"thinkingLevel":"high","model":{"provider":"commandcode","modelId":"m1"}, + "entryIds":[$ids],"messages":[${messages.joinToString(",")}]}} + """.trimIndent() + return ContextSync.parse(obj(payload)) + } + + @Test + fun `parses history, model and live snapshot`() { + val parsed = ContextSync.parse( + obj( + """ + {"mode":"replace","baseEntryId":null, + "cursor":{"firstEntryId":"e1","lastEntryId":"e2"},"hasMore":false, + "sessionId":"s1","leafId":"e2", + "context":{"thinkingLevel":"high","model":{"provider":"commandcode","modelId":"m1"}, + "entryIds":["e1","e2"], + "messages":[ + {"role":"user","content":[{"type":"text","text":"hello"}],"timestamp":1}, + {"role":"assistant","content":[{"type":"text","text":"hi"}],"timestamp":2}]}, + "live":{"cursor":{"streamId":"w","sequence":5},"isStreaming":true, + "isPromptRunning":true,"isCompacting":false, + "streamingMessage":{"role":"assistant","content":[{"type":"text","text":"partial"}],"timestamp":3}, + "toolEvents":[ + {"type":"tool_execution_start","toolCallId":"t1","toolName":"bash"}, + {"type":"tool_execution_update","toolCallId":"t1","toolName":"bash", + "partialResult":{"content":[{"type":"text","text":"out"}]}}], + "responseObserved":true}} + """.trimIndent(), + ), + ) + assertEquals(listOf("e1", "e2"), parsed.entryIds) + assertEquals(2, parsed.messages.size) + assertEquals("commandcode", parsed.model?.provider) + assertEquals("m1", parsed.model?.id) + assertEquals("e2", parsed.leafId) + assertNotNull(parsed.live) + assertEquals("w", parsed.live?.cursor?.streamId) + assertEquals(5L, parsed.live?.cursor?.sequence) + assertTrue(parsed.live?.isStreaming == true) + assertEquals("partial", parsed.live?.streamingMessage?.blocks?.filterIsInstance()?.single()?.text) + assertEquals("out", parsed.live?.toolEvents?.last()?.result?.text) + } + + @Test + fun `live absent for a wrapper-less idle session`() { + val parsed = page("replace", null, listOf("e1"), listOf("""{"role":"user","content":"hi","timestamp":1}""")) + assertNull(parsed.live) + assertEquals("hi", (parsed.messages.single() as ChatMessage.User).text) + } + + @Test + fun `malformed page with misaligned ids yields no rows`() { + val parsed = ContextSync.parse( + obj( + """ + {"mode":"replace","cursor":{},"hasMore":false,"sessionId":"s1", + "context":{"entryIds":["e1","e2"],"messages":[{"role":"user","content":"hi"}]}} + """.trimIndent(), + ), + ) + assertTrue(parsed.messages.isEmpty()) + assertTrue(parsed.entryIds.isEmpty()) + } + + @Test + fun `engine applies the live snapshot and replays running tools`() { + val engine = ChatEngine(now = { 5L }) + engine.setSession("s1") + val parsed = ContextSync.parse( + obj( + """ + {"mode":"replace","baseEntryId":null,"hasMore":false,"sessionId":"s1","leafId":"e1", + "context":{"entryIds":["e1"],"messages":[{"role":"user","content":"hi","timestamp":1}]}, + "live":{"cursor":{"streamId":"w","sequence":5},"isStreaming":true, + "isPromptRunning":true,"isCompacting":false, + "streamingMessage":{"role":"assistant","content":[{"type":"text","text":"tal"}],"timestamp":2}, + "toolEvents":[{"type":"tool_execution_start","toolCallId":"t1","toolName":"bash"}]}} + """.trimIndent(), + ), + ) + engine.applyContextPage(parsed) + val state = engine.state.value + assertTrue(state.historyLoaded) + assertTrue(state.streamingActive) + assertTrue(state.agentRunning) + assertTrue(state.promptRunning) + assertEquals("tal", state.streaming?.blocks?.filterIsInstance()?.single()?.text) + assertEquals(1, state.liveTools.size) + assertTrue(state.liveTools.single().running) + } + + @Test + fun `frame-committed rows survive a page that raced the file write`() { + val engine = ChatEngine(now = { 10L }) + engine.setSession("s1") + engine.sendStarted("hello", 1) + engine.onFrame( + obj("""{"type":"agent_start","web":{"streamId":"w","sequence":1}}"""), + ) + engine.onFrame( + obj( + """{"type":"message_end","web":{"streamId":"w","sequence":3}, + "message":{"role":"assistant","content":[{"type":"text","text":"answer"}],"timestamp":10}}""", + ), + ) + assertEquals(1, engine.state.value.messages.size) + val userOnly = page("replace", null, listOf("e1"), listOf("""{"role":"user","content":"hello","timestamp":1}""")) + engine.applyContextPage(userOnly) + assertTrue(engine.state.value.messages.any { it is ChatMessage.Assistant }) + assertEquals("the unconfirmed reply stays visible", 2, engine.state.value.messages.size) + + val confirmed = page( + "replace", null, listOf("e1", "e2"), + listOf( + """{"role":"user","content":"hello","timestamp":1}""", + """{"role":"assistant","content":[{"type":"text","text":"answer"}],"timestamp":10}""", + ), + ) + engine.applyContextPage(confirmed) + assertEquals("the confirmed page replaces the frame row", 2, engine.state.value.messages.size) + assertEquals("e2", engine.state.value.messages[1].entryId) + } + + @Test + fun `stale message frames below the sequence floor are dropped`() { + val engine = ChatEngine(now = { 1L }) + engine.setSession("s1") + engine.onFrame(obj("""{"type":"agent_start","web":{"streamId":"w","sequence":1}}""")) + engine.onFrame( + obj( + """{"type":"message_update","web":{"streamId":"w","sequence":10}, + "message":{"role":"assistant","content":[{"type":"text","text":"ten"}]}}""", + ), + ) + engine.onFrame( + obj( + """{"type":"message_update","web":{"streamId":"w","sequence":9}, + "message":{"role":"assistant","content":[{"type":"text","text":"nine"}]}}""", + ), + ) + val text = engine.state.value.streaming?.blocks?.filterIsInstance()?.single()?.text + assertEquals("ten", text) + assertFalse(engine.state.value.messages.any()) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineNoticeTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineNoticeTest.kt new file mode 100644 index 000000000..3b0cf61c7 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineNoticeTest.kt @@ -0,0 +1,63 @@ +package io.github.kahme247.ompweb.chat + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +/** + * Notice-bar lifecycle: duplicate frames of one message collapse while it is + * visible, but once the bar dismissed it (user or its 5 s/30 s timer) the same + * message must be able to appear again — a repeated clipboard/host notice was + * otherwise swallowed for the rest of the session. + */ +class ChatEngineNoticeTest { + + private var clock = 1_000L + + private fun engine(): ChatEngine = ChatEngine(now = { clock }) + + @Test + fun repeatedNoticeIsDedupedWhileVisible() { + val engine = engine() + engine.notice(NoticeKind.Info, "An extension read your clipboard") + clock = 1_400L + engine.notice(NoticeKind.Info, "An extension read your clipboard") + + val notices = engine.state.value.notices + assertEquals(1, notices.size) + assertEquals(1_000L, notices.single().createdAt) + } + + @Test + fun dismissedNoticeCanShowAgain() { + val engine = engine() + engine.notice(NoticeKind.Info, "An extension read your clipboard") + val first = engine.state.value.notices.single() + + engine.dismissNotice(first) + assertEquals(emptyList(), engine.state.value.notices) + + clock = 7_000L + engine.notice(NoticeKind.Info, "An extension read your clipboard") + + val again = engine.state.value.notices.single() + assertEquals(7_000L, again.createdAt) + assertNotEquals(first.createdAt, again.createdAt) + } + + @Test + fun noticesStayBounded() { + val engine = engine() + repeat(8) { index -> + engine.notice(NoticeKind.Info, "Notice $index") + } + + val messages = engine.state.value.notices.map { it.message } + + assertEquals(5, messages.size) + assertEquals( + listOf("Notice 3", "Notice 4", "Notice 5", "Notice 6", "Notice 7"), + messages, + ) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineSkillDiagnosticsTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineSkillDiagnosticsTest.kt new file mode 100644 index 000000000..1f7adccbd --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineSkillDiagnosticsTest.kt @@ -0,0 +1,134 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * The skill-resolution report reaches the UI from two places: the live + * `skill_diagnostics_update` frame and `get_state`. The revision fence keeps a + * slower snapshot from resurrecting a report the live frame already replaced + * (web `skillDiagnosticsSeqRef`), and the Disable reply obeys the same rule. + */ +class ChatEngineSkillDiagnosticsTest { + + private val json = Json { ignoreUnknownKeys = true } + + private fun obj(text: String): JsonObject = json.parseToJsonElement(text).jsonObject + + private fun engine(): ChatEngine = ChatEngine(now = { 1_800_000_000_000L }) + + private val entry = + """{"name":"clean","filePath":"C:/repo/.skills/clean/SKILL.md","source":"project"}""" + + private fun frame(showStartup: Boolean = true, reason: String = "source-order"): JsonObject = obj( + """ + {"type":"skill_diagnostics_update","data":{ + "cwd":"C:/repo","showStartupDiagnostics":$showStartup, + "diagnostics":[{"name":"clean","reason":"$reason","skills":[$entry],"duplicates":[]}]}} + """.trimIndent(), + ) + + private fun state(showStartup: Boolean): JsonObject = obj( + """ + {"running":false,"state":{"skillDiagnostics":{ + "cwd":"C:/repo","showStartupDiagnostics":$showStartup, + "diagnostics":[]}}} + """.trimIndent(), + ) + + @Test + fun aLiveFramePublishesTheReportAndBumpsTheRevision() { + val engine = engine() + + engine.onFrame(frame()) + engine.flush() + + val snapshot = engine.state.value.skillDiagnostics!! + assertEquals("C:/repo", snapshot.cwd) + assertTrue(snapshot.showStartupDiagnostics) + assertEquals(1, engine.state.value.skillDiagnosticsRevision) + } + + @Test + fun aMalformedReportClearsTheBanner() { + val engine = engine() + engine.onFrame(frame()) + engine.flush() + + engine.onFrame(obj("""{"type":"skill_diagnostics_update","data":"nope"}""")) + engine.flush() + + assertNull(engine.state.value.skillDiagnostics) + assertEquals(2, engine.state.value.skillDiagnosticsRevision) + } + + @Test + fun aGetStateSnapshotFillsAMissingReport() { + val engine = engine() + + engine.applyAgentState(state(showStartup = false), expectedRunId = engine.state.value.runId) + engine.flush() + + val snapshot = engine.state.value.skillDiagnostics!! + assertFalse(snapshot.showStartupDiagnostics) + assertEquals(1, engine.state.value.skillDiagnosticsRevision) + } + + @Test + fun aSnapshotRequestedBeforeALiveFrameCannotOverwriteIt() { + val engine = engine() + val revisionWhenRequested = engine.state.value.skillDiagnosticsRevision + + engine.onFrame(frame(showStartup = true)) + engine.flush() + engine.applyAgentState( + state(showStartup = false), + expectedSkillDiagnosticsRevision = revisionWhenRequested, + ) + engine.flush() + + assertTrue(engine.state.value.skillDiagnostics!!.showStartupDiagnostics) + assertEquals(1, engine.state.value.skillDiagnosticsRevision) + } + + @Test + fun aSetterReplyOvertakenByALiveFrameIsIgnored() { + val engine = engine() + engine.onFrame(frame(showStartup = true)) + engine.flush() + val revisionWhenSent = engine.state.value.skillDiagnosticsRevision + val live = engine.state.value.skillDiagnostics!! + + engine.onFrame(frame(showStartup = true)) + engine.flush() + + val applied = engine.applySkillDiagnostics(live, expectedRevision = revisionWhenSent) + + assertFalse(applied) + assertEquals(2, engine.state.value.skillDiagnosticsRevision) + } + + @Test + fun aSetterReplyAppliesWhileNoNewerReportArrived() { + val engine = engine() + engine.onFrame(frame(showStartup = true)) + engine.flush() + val revisionWhenSent = engine.state.value.skillDiagnosticsRevision + val current = engine.state.value.skillDiagnostics!! + + val applied = engine.applySkillDiagnostics( + current.copy(showStartupDiagnostics = false), + expectedRevision = revisionWhenSent, + ) + + assertTrue(applied) + assertFalse(engine.state.value.skillDiagnostics!!.showStartupDiagnostics) + assertEquals(2, engine.state.value.skillDiagnosticsRevision) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineSpeedTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineSpeedTest.kt new file mode 100644 index 000000000..8946a0a21 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineSpeedTest.kt @@ -0,0 +1,180 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +/** + * The live rate frames: one assistant message grows in text, the engine turns + * the deltas into a rate at a fixed publish cadence, and every run boundary + * clears it again. + */ +class ChatEngineSpeedTest { + + private val json = Json { ignoreUnknownKeys = true } + private var clock = 1_000L + + private fun engine(): ChatEngine = ChatEngine(now = { clock }) + + private fun obj(text: String): JsonObject = json.parseToJsonElement(text).jsonObject + + private fun startFrame() = obj( + """{"type":"message_start","message":{"role":"assistant","timestamp":900,"content":[]}}""", + ) + + private fun updateFrame(text: String) = obj( + """{"type":"message_update","message":{"role":"assistant","timestamp":900,"content":[""" + + """{"type":"text","text":"$text"}]}}""", + ) + + private fun endFrame(completedAt: Long) = obj( + """ + {"type":"message_end","message":{"role":"assistant","timestamp":900, + "content":[{"type":"text","text":"done"}], + "usage":{"output":7751,"input":10}, + "duration":197638.7748,"ttft":2707.7157,"completedAt":$completedAt, + "stopReason":"stop"}} + """.trimIndent(), + ) + + /** Drive one assistant message to a published rate of 50 t/s. */ + private fun streamTwoHundredTokens(engine: ChatEngine) { + engine.setSession("s1") + engine.sendStarted("hi", 1L) + engine.onFrame(startFrame()) + engine.flush() + clock = 1_000L + engine.onFrame(updateFrame("a".repeat(400))) + engine.flush() + clock = 3_000L + engine.onFrame(updateFrame("a".repeat(800))) + engine.flush() + } + + @Test + fun `the live rate is published after the 500 ms gate`() { + val engine = engine() + streamTwoHundredTokens(engine) + assertEquals(50.0, engine.state.value.liveTokensPerSecond!!, 0.0001) + } + + @Test + fun `a short window stays empty`() { + val engine = engine() + engine.setSession("s1") + engine.sendStarted("hi", 1L) + engine.onFrame(startFrame()) + engine.flush() + clock = 1_000L + engine.onFrame(updateFrame("a".repeat(400))) + engine.flush() + clock = 1_200L + engine.onFrame(updateFrame("a".repeat(800))) + engine.flush() + assertNull("under 700 ms", engine.state.value.liveTokensPerSecond) + } + + @Test + fun `a new assistant message resets the estimate`() { + val engine = engine() + streamTwoHundredTokens(engine) + clock = 4_000L + engine.onFrame(startFrame()) + engine.flush() + assertNull(engine.state.value.liveTokensPerSecond) + } + + @Test + fun `message_end clears the live rate and commits the fields`() { + val engine = engine() + streamTwoHundredTokens(engine) + clock = 5_000L + engine.onFrame(endFrame(completedAt = 5_000L)) + engine.flush() + assertNull(engine.state.value.liveTokensPerSecond) + val message = engine.state.value.messages.last() as ChatMessage.Assistant + assertEquals(7751, message.usageOutput) + assertEquals(2707.7157, message.ttftMs!!, 0.0001) + assertEquals(5_000L, message.completedAt) + assertEquals("stop", message.stopReason) + val speed = messageSpeed(message)!! + assertEquals(39.76, speed.tokensPerSecond, 0.02) + } + + @Test + fun `a subagent frame never changes the rate`() { + val engine = engine() + streamTwoHundredTokens(engine) + clock = 3_100L + engine.onFrame(obj("""{"type":"subagent_event","payload":{"id":"sa1"}}""")) + engine.flush() + assertEquals(50.0, engine.state.value.liveTokensPerSecond!!, 0.0001) + } + + @Test + fun `an agent state snapshot without streaming clears the rate`() { + val engine = engine() + streamTwoHundredTokens(engine) + clock = 3_100L + engine.applyAgentState( + obj("""{"running":true,"state":{"isStreaming":false,"isPromptRunning":false}}"""), + ) + assertNull(engine.state.value.liveTokensPerSecond) + } + + @Test + fun `a user message start keeps the running rate`() { + val engine = engine() + streamTwoHundredTokens(engine) + clock = 3_100L + engine.onFrame( + obj("""{"type":"message_start","message":{"role":"user","content":"next","timestamp":3000}}"""), + ) + engine.flush() + assertEquals(50.0, engine.state.value.liveTokensPerSecond!!, 0.0001) + } + + @Test + fun `the composer ticker keeps a live rate up to date without frames`() { + val engine = engine() + streamTwoHundredTokens(engine) + clock = 3_500L + engine.refreshLiveRate() + assertEquals(50.0, engine.state.value.liveTokensPerSecond!!, 0.0001) + } + + @Test + fun `the composer ticker hides the rate once the stream stalls`() { + val engine = engine() + streamTwoHundredTokens(engine) + // No frame for 2.6 s: the last delta is stale, so the number must go. + clock = 5_600L + engine.refreshLiveRate() + assertNull(engine.state.value.liveTokensPerSecond) + } + + @Test + fun `a heartbeat frame without new tokens does not keep the rate alive`() { + val engine = engine() + streamTwoHundredTokens(engine) + clock = 5_600L + engine.onFrame(updateFrame("a".repeat(800))) + engine.flush() + assertNull(engine.state.value.liveTokensPerSecond) + } + + @Test + fun `the ticker does not invent a rate after the run ended`() { + val engine = engine() + streamTwoHundredTokens(engine) + clock = 5_000L + engine.onFrame(endFrame(completedAt = 5_000L)) + engine.flush() + clock = 9_000L + engine.refreshLiveRate() + assertNull(engine.state.value.liveTokensPerSecond) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineSyncTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineSyncTest.kt new file mode 100644 index 000000000..820495112 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatEngineSyncTest.kt @@ -0,0 +1,352 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Engine regressions from the Phase 5 review: append-page id alignment, the + * queue watermark and the deferred response check. + */ +class ChatEngineSyncTest { + + private val json = Json { ignoreUnknownKeys = true } + + private fun obj(text: String): JsonObject = json.parseToJsonElement(text).jsonObject + + private fun engine(): ChatEngine = ChatEngine(now = { 1_800_000_000_000L }) + + private fun page( + mode: String, + baseEntryId: String?, + hasMore: Boolean = false, + entryIds: List, + messages: List, + ): ContextPage { + val ids = entryIds.joinToString(",") { "\"$it\"" } + val base = baseEntryId?.let { "\"$it\"" } ?: "null" + return ContextSync.parse( + obj( + """ + {"mode":"$mode","baseEntryId":$base, + "cursor":{"firstEntryId":"${entryIds.first()}","lastEntryId":"${entryIds.last()}"}, + "hasMore":$hasMore,"sessionId":"s1","leafId":"${entryIds.last()}", + "context":{"thinkingLevel":"high","model":{"provider":"commandcode","modelId":"m1"}, + "entryIds":[$ids],"messages":[${messages.joinToString(",")}]}} + """.trimIndent(), + ), + ) + } + + private fun assistant(text: String, timestamp: Long, stopReason: String? = null): String { + val reason = stopReason?.let { ",\"stopReason\":\"$it\"" } ?: "" + return """{"role":"assistant","content":[{"type":"text","text":"$text"}],"timestamp":$timestamp$reason}""" + } + + private fun user(text: String, timestamp: Long): String = + """{"role":"user","content":[{"type":"text","text":"$text"}],"timestamp":$timestamp}""" + + private fun toolResult(toolCallId: String, text: String, timestamp: Long): String = + """{"role":"toolResult","toolCallId":"$toolCallId","toolName":"bash", + "content":[{"type":"text","text":"$text"}],"timestamp":$timestamp}""" + + private fun ChatEngine.feed(vararg frames: JsonObject) { + frames.forEach { + onFrame(it) + flush() + } + } + + @Test + fun `append page keeps entry ids aligned with a frame-committed row`() { + val engine = engine() + engine.setSession("s1") + engine.applyContextPage( + page( + mode = "replace", + baseEntryId = null, + entryIds = listOf("e1", "e2"), + messages = listOf(user("hello", 1), assistant("hi", 2, "stop")), + ), + ) + // The frame stream committed the answer but the page has not named it. + engine.feed(obj("""{"type":"message_end","message":${assistant("streamed", 3, "stop")}}""")) + assertEquals(3, engine.state.value.messages.size) + assertEquals(2, engine.state.value.entryIds.size) + + // The append page reports the lost tool result *before* the assistant + // row the frames already committed. + engine.applyContextPage( + page( + mode = "append", + baseEntryId = "e2", + entryIds = listOf("e4", "e5"), + messages = listOf(toolResult("t1", "done", 4), assistant("streamed", 3, "stop")), + ), + ) + + val state = engine.state.value + assertEquals("ids stay index-aligned", state.messages.size, state.entryIds.size) + assertEquals( + listOf("e1", "e2", "e5", "e4"), + state.entryIds, + ) + assertTrue(state.messages.last() is ChatMessage.ToolResult) + assertEquals( + "the assistant row keeps the id of its in-place replacement", + "e5", + state.entryIds[2], + ) + } + + @Test + fun `a partial queue payload keeps the current queue`() { + val engine = engine() + engine.feed(obj("""{"type":"queue_update","steering":["steer"],"followUp":["follow"]}""")) + val queued = engine.state.value + assertEquals(listOf("steer"), queued.queue.steering) + assertEquals(listOf("follow"), queued.queue.followUp) + + engine.feed(obj("""{"type":"queue_update","queuedMessages":{"steering":[]}}""")) + + assertEquals(queued.queue, engine.state.value.queue) + assertEquals(queued.queueRevision, engine.state.value.queueRevision) + } + + @Test + fun `a stale agent-state snapshot does not override a newer queue`() { + val engine = engine() + engine.feed(obj("""{"type":"queue_update","steering":["new"],"followUp":[]}""")) + val revision = engine.state.value.queueRevision + assertNotEquals(0L, revision) + + engine.applyAgentState( + obj( + """ + {"running":true, + "state":{"isStreaming":false,"isPromptRunning":true, + "queuedMessages":{"steering":["old"],"followUp":[]}}} + """.trimIndent(), + ), + expectedQueueRevision = revision - 1, + ) + + val state = engine.state.value + assertEquals("the newer queue survives", listOf("new"), state.queue.steering) + assertTrue("the rest of the snapshot still applies", state.promptRunning) + } + + @Test + fun `a stale agent-state snapshot for an older run is dropped`() { + val engine = engine() + engine.sendStarted("first", runId = 7) + engine.applyAgentState( + obj("""{"running":false,"state":{"isStreaming":false,"isPromptRunning":false}}"""), + expectedRunId = 6, + ) + + val state = engine.state.value + assertEquals(7L, state.runId) + assertTrue("the newer run is still busy", state.busy) + assertEquals("first run pending", "first", engine.state.value.pendingUser?.text) + } + + @Test + fun `a replayed auto compaction frame cannot clear the compacting flag`() { + val engine = engine() + engine.feed( + obj( + """ + {"type":"auto_compaction_start", + "web":{"streamId":"w1","sequence":10}} + """.trimIndent(), + ), + ) + assertTrue(engine.state.value.compacting) + + engine.feed( + obj( + """ + {"type":"auto_compaction_end","aborted":false,"skipped":false, + "web":{"streamId":"w1","sequence":5}} + """.trimIndent(), + ), + ) + + assertTrue("an older compaction frame is fenced", engine.state.value.compacting) + } + + @Test + fun `an empty run is not judged until the page arrives`() { + val engine = engine() + engine.setSession("s1") + engine.sendStarted("hello", runId = 1) + engine.feed(obj("""{"type":"agent_end"}""")) + + assertTrue("no notice while the reload is pending", engine.state.value.notices.isEmpty()) + + engine.applyContextPage( + page( + mode = "replace", + baseEntryId = null, + entryIds = listOf("e1", "e2"), + messages = listOf(user("hello", 1), assistant("answer", 2, "stop")), + ), + ) + + assertTrue("the loaded answer settles the run", engine.state.value.notices.isEmpty()) + } + + @Test + fun `a connected frame asks for a catch-up without resetting the transcript`() { + val engine = engine() + engine.setSession("s1") + engine.applyContextPage( + page( + mode = "replace", + baseEntryId = null, + entryIds = listOf("e1", "e2"), + messages = listOf(user("hello", 1), assistant("hi", 2, "stop")), + ), + ) + engine.drainEffects() + + engine.feed(obj("""{"type":"connected"}""")) + + assertEquals( + listOf(ChatEffect.StreamOpened, ChatEffect.RequestContext(reset = false)), + engine.drainEffects(), + ) + assertEquals("the existing rows stay until the page replaces them", 2, engine.state.value.messages.size) + } + + @Test + fun `a replayed message_end after reconnect commits the row once`() { + val engine = engine() + engine.setSession("s1") + engine.drainEffects() + val replay = obj("""{"type":"message_end","message":${assistant("answer", 2, "stop")}}""") + + engine.feed(replay) + engine.feed(replay) + + assertEquals(1, engine.state.value.messages.size) + val committed = engine.state.value.messages.single() as ChatMessage.Assistant + assertEquals("answer", (committed.blocks.single() as AssistantBlock.Text).text) + } + + @Test + fun `a page without an answer reports the missing response`() { + val engine = engine() + engine.setSession("s1") + engine.sendStarted("hello", runId = 1) + engine.feed(obj("""{"type":"agent_end"}""")) + engine.applyContextPage( + page( + mode = "replace", + baseEntryId = null, + entryIds = listOf("e1"), + messages = listOf(user("hello", 1)), + ), + ) + + val notice = engine.state.value.notices.single() + assertEquals(NoticeKind.Error, notice.kind) + assertEquals("The model returned no response", notice.message) + } + + // --- cache-first seeding ------------------------------------------------ + + private fun cachePage(vararg texts: String): ContextPage = page( + mode = "replace", + baseEntryId = null, + entryIds = texts.indices.map { "e$it" }, + messages = texts.mapIndexed { index, text -> user(text, index + 1L) }, + ) + + @Test + fun `a cached page seeds an empty transcript and marks it stale`() { + val engine = engine() + engine.setSession("s1") + + val seeded = engine.seedCachedHistory(cachePage("cached prompt", "cached answer")) + + assertTrue("an empty engine accepts the cache", seeded) + val state = engine.state.value + assertEquals(2, state.messages.size) + assertTrue("the view is marked as updating", state.historyStale) + assertEquals("the cached view is not a completed load", false, state.historyLoaded) + } + + @Test + fun `a cached page never replaces content that is already there`() { + val engine = engine() + engine.setSession("s1") + engine.feed(obj("""{"type":"message_end","message":${assistant("live answer", 2, "stop")}}""")) + + val seeded = engine.seedCachedHistory(cachePage("cached prompt")) + + assertEquals("a live frame that arrived first wins", false, seeded) + assertEquals(1, engine.state.value.messages.size) + assertEquals(false, engine.state.value.historyStale) + } + + @Test + fun `a real page replaces the cache and clears the stale marker`() { + val engine = engine() + engine.setSession("s1") + engine.seedCachedHistory(cachePage("cached prompt", "cached answer")) + + engine.applyContextPage(cachePage("fresh prompt")) + + val state = engine.state.value + assertEquals("the server answer wins over the disk copy", 1, state.messages.size) + assertEquals(false, state.historyStale) + assertTrue(state.historyLoaded) + } + + @Test + fun `a failed refresh keeps the cached transcript with a retry error`() { + val engine = engine() + engine.setSession("s1") + engine.seedCachedHistory(cachePage("cached prompt")) + + engine.historyRequestFailed("Network unreachable") + + val state = engine.state.value + assertEquals(1, state.messages.size) + assertEquals(false, state.historyStale) + assertTrue("the cached rows count as loaded", state.historyLoaded) + assertEquals("Network unreachable", state.historyError) + } + + @Test + fun `a failed first load without a cache keeps the full-pane error`() { + val engine = engine() + engine.setSession("s1") + + engine.historyRequestFailed("Network unreachable") + + val state = engine.state.value + assertEquals(false, state.historyLoaded) + assertEquals("Network unreachable", state.historyError) + } + + @Test + fun `an empty cached page is not a seed`() { + val engine = engine() + engine.setSession("s1") + val empty = ContextSync.parse( + obj("""{"mode":"replace","sessionId":"s1","context":{"entryIds":[],"messages":[]}}"""), + ) + + val seeded = engine.seedCachedHistory(empty) + + assertEquals(false, seeded) + assertEquals(false, engine.state.value.historyStale) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatFixtureTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatFixtureTest.kt new file mode 100644 index 000000000..bcf6df185 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatFixtureTest.kt @@ -0,0 +1,167 @@ +package io.github.kahme247.ompweb.chat + +import java.io.File +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject + +/** + * Drives the seven recorded protocol fixtures through the engine. The + * fixtures are the binding transcript shape (Phase 5 spec §15): every frame + * type that occurs in a real run must land in the expected state. + */ +class ChatFixtureTest { + + private val json = Json { ignoreUnknownKeys = true } + + private fun fixture(name: String): List = + File("src/test/resources/protocol/$name.ndjson").readLines() + .filter { it.isNotBlank() } + .map { json.parseToJsonElement(it).jsonObject } + + private class Driver(val engine: ChatEngine) { + var effectLog = mutableListOf() + var btwFrames = 0 + var stateApplied: JsonObject? = null + + fun feed(frames: List) { + engine.setSession("session") + effectLog += engine.drainEffects() + for (frame in frames) { + val rpc = frame["_rpc"] as? JsonObject + if (rpc != null) { + val type = (rpc["body"] as? JsonObject)?.get("type")?.toString()?.trim('"') + if (type == "get_state") { + val data = (frame["_response"] as? JsonObject)?.get("data") as? JsonObject + ?: error("get_state fixture without data") + engine.applyAgentState(data) + stateApplied = data + } + continue + } + if (frame["_scenario"] != null) continue + engine.onFrame(frame) + engine.flush() + effectLog += engine.drainEffects() + } + } + + fun resets(): Int = effectLog.count { it is ChatEffect.RequestContext && it.reset } + } + + private fun driver(name: String): Driver { + var btw = 0 + val engine = ChatEngine( + now = { 1_800_000_000_000L }, + onBtwFrame = { btw += 1 }, + ) + val driver = Driver(engine) + driver.feed(fixture(name)) + driver.btwFrames = btw + return driver + } + + @Test + fun plainText() { + val driver = driver("plain-text") + val state = driver.engine.state.value + assertEquals(2, state.messages.size) + assertTrue(state.messages[0] is ChatMessage.User) + val assistant = state.messages[1] as ChatMessage.Assistant + assertEquals("fixture-ok", assistant.blocks.filterIsInstance().single().text) + assertFalse(state.busy) + assertNull(state.streaming) + assertTrue(state.liveTools.isEmpty()) + assertNull(state.pendingUser) + } + + @Test + fun toolCall() { + val driver = driver("tool-call") + val state = driver.engine.state.value + assertEquals("user, assistant(toolCall), toolResult, assistant text", 4, state.messages.size) + val tools = state.messages[1] as ChatMessage.Assistant + val call = tools.blocks.filterIsInstance().single() + assertEquals("bash", call.toolName) + assertEquals("echo fixture", call.input.string("command")) + val result = state.messages[2] as ChatMessage.ToolResult + assertEquals(call.toolCallId, result.toolCallId) + assertEquals("fixture\n", result.text) + assertTrue("a committed result removes the live row", state.liveTools.isEmpty()) + val plan = TranscriptPlan.build(state.messages) + val group = plan.filterIsInstance().single() + val activity = group.segments.filterIsInstance().single() + assertEquals(1, activity.toolCallCount) + assertEquals(call.toolCallId, TranscriptPlan.collectToolResults(state.messages).keys.single()) + } + + @Test + fun abortMidRun() { + val driver = driver("abort-mid-run") + val state = driver.engine.state.value + assertEquals("agent_end.messages must not be appended", 2, state.messages.size) + val assistant = state.messages[1] as ChatMessage.Assistant + assertEquals("aborted", assistant.stopReason) + assertEquals("Interrupted by user", assistant.errorMessage) + assertFalse(state.busy) + assertTrue(state.notices.any { it.message == "Interrupted by user" }) + } + + @Test + fun queuedFollowUpRemoved() { + var sawQueue = false + val engine = ChatEngine(now = { 1_800_000_000_000L }) + engine.setSession("session") + engine.drainEffects() + for (frame in fixture("queued-follow-up-removed")) { + if (frame["_rpc"] != null) continue + if (frame["_scenario"] != null) continue + engine.onFrame(frame) + engine.flush() + engine.drainEffects() + if (frame.string("type") == "queue_update") { + val queue = engine.state.value.queue + if (queue.followUp == listOf("queued fixture message")) sawQueue = true + } + } + assertTrue("the queued follow-up was visible while running", sawQueue) + val state = engine.state.value + assertTrue(state.queue.followUp.isEmpty()) + assertEquals(2, state.messages.size) + assertFalse(state.busy) + } + + @Test + fun btwSideQuestionNeverTouchesTranscript() { + val driver = driver("btw-side-question") + val state = driver.engine.state.value + assertEquals(3, driver.btwFrames) + assertEquals(2, state.messages.size) + assertEquals("btw frames must not start a new epoch", 1, driver.resets()) + } + + @Test + fun getStateSnapshot() { + val driver = driver("get-state") + val state = driver.engine.state.value + assertEquals("deepseek/deepseek-v4.1-flash", state.model?.id) + assertEquals("commandcode", state.model?.provider) + assertEquals("high", state.thinkingLevel) + assertFalse(state.agentRunning) + assertEquals(9448, state.contextUsage?.tokens) + assertEquals(200000, state.contextUsage?.contextWindow) + } + + @Test + fun reconnectResubscribeDoesNotDuplicate() { + val driver = driver("reconnect-resubscribe") + val state = driver.engine.state.value + assertEquals(2, state.messages.size) + assertEquals("the same stream id must not reset the epoch", 1, driver.resets()) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatForeignRunTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatForeignRunTest.kt new file mode 100644 index 000000000..b0d344cad --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatForeignRunTest.kt @@ -0,0 +1,213 @@ +package io.github.kahme247.ompweb.chat + +import io.github.kahme247.ompweb.net.ElsewhereSession +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * The chat's view of a foreign owner: where the report comes from, when it is + * kept and when a run of this app replaces it. The banner and the send gate + * read exactly this field. + */ +class ChatForeignRunTest { + + private val json = Json { ignoreUnknownKeys = true } + + private fun obj(text: String): JsonObject = json.parseToJsonElement(text).jsonObject + + private fun engine(): ChatEngine = ChatEngine(now = { 1_800_000_000_000L }) + + /** The server's single-session report: no id (the list entry has one). */ + private val owner = ElsewhereSession( + kind = "terminal", + source = "process", + running = true, + active = true, + ) + + @Test + fun `a snapshot reports the foreign owner`() { + val engine = engine() + engine.setSession("s1") + engine.applyAgentState( + obj( + """{"running":false,"state":{"isPromptRunning":false},""" + + """"elsewhere":{"kind":"terminal","source":"process",""" + + """"running":true,"active":true}}""", + ), + ) + assertEquals(owner, engine.state.value.foreignRun) + } + + @Test + fun `the owner report of the server reaches the banner without an id`() { + val engine = engine() + engine.setSession("s1") + // The real `GET /api/agent/{id}` body: the single-session report is the + // server's `SessionElsewhere` and has no `id` (and no `cwd`/`pid`: the + // owner's process details stay on the server). A decode that insisted + // on them dropped every live report — the banner never appeared while + // the session list showed the very same owner. + engine.applyAgentState( + obj( + """{"running":false,"state":{"isStreaming":false,"isPromptRunning":false},""" + + """"elsewhere":{"kind":"terminal","source":"process","running":false,""" + + """"active":true,"lastActivityAt":1800000000000}}""", + ), + ) + val report = engine.state.value.foreignRun + assertNotNull("the banner must not depend on a field the server never sends", report) + assertEquals("terminal", report!!.kind) + assertEquals("process", report.source) + assertEquals(1_800_000_000_000L, report.lastActivityAt) + } + + @Test + fun `a snapshot with an explicit null clears the owner`() { + val engine = engine() + engine.setSession("s1") + engine.setForeignRun(owner) + engine.applyAgentState(obj("""{"running":false,"elsewhere":null}""")) + assertNull("the owner ended", engine.state.value.foreignRun) + } + + @Test + fun `an older server without the field keeps what is known`() { + val engine = engine() + engine.setSession("s1") + engine.setForeignRun(owner) + engine.applyAgentState(obj("""{"running":false,"state":{"isPromptRunning":false}}""")) + assertEquals("no report is not a report of absence", owner, engine.state.value.foreignRun) + } + + @Test + fun `a locally running session drops a stale owner report`() { + val engine = engine() + engine.setSession("s1") + engine.setForeignRun(owner) + engine.applyAgentState( + obj("""{"running":true,"state":{"isPromptRunning":true},"elsewhere":{"source":"process"}}"""), + ) + assertNull("this server drives the run", engine.state.value.foreignRun) + } + + @Test + fun `starting a prompt from here clears the owner`() { + val engine = engine() + engine.setSession("s1") + engine.setForeignRun(owner) + engine.sendStarted("into a copy", runId = 7) + assertNull("the user chose the copy", engine.state.value.foreignRun) + } + + @Test + fun `the refusal body sets the owner without a state snapshot`() { + val engine = engine() + engine.setSession("s1") + engine.setForeignRun(owner) + // The same report is not a state change, so no update is published. + engine.setForeignRun(owner) + assertNotNull(engine.state.value.foreignRun) + engine.setForeignRun(null) + assertNull(engine.state.value.foreignRun) + } + + @Test + fun `the contention fork clears the owner and reports the original`() { + val engine = engine() + engine.setSession("s1") + engine.drainEffects() + engine.setForeignRun(owner) + // A forced send raced the owner: omp moved the conversation to a copy. + // The copy is this app's now; the original stays with the other process. + engine.onFrame(obj("""{"type":"session_contention_fork","fromId":"s1","toId":"s2"}""")) + engine.flush() + assertEquals( + listOf(ChatEffect.ContentionFork("s1", "s2")), + engine.drainEffects(), + ) + assertNull("the copy is not foreign any more", engine.state.value.foreignRun) + } + + @Test + fun `an idle live child owns the session without looking like a run`() { + val engine = engine() + engine.setSession("s1") + engine.setForeignRun(null) + // The fresh-spawn answer from the report: omp is alive, answers + // `get_state` with nothing running. `alive` keeps the ownership, `running` + // stays false, so the screen must not say "working". + engine.applyAgentState( + obj( + """{"running":false,"alive":true,""" + + """"state":{"isStreaming":false,"isPromptRunning":false,"isCompacting":false}}""", + ), + ) + assertTrue("the live state is known", engine.state.value.liveStateKnown) + assertEquals("no run in flight", false, engine.state.value.agentRunning) + assertEquals("nothing is busy", false, engine.state.value.busy) + } + + @Test + fun `an idle live child keeps the owner banner down`() { + val engine = engine() + engine.setSession("s1") + engine.setForeignRun(owner) + // `alive` (not `running`) is what proves the session is ours: an idle + // wrapper still holds it, and a stale owner report must not survive it. + engine.applyAgentState( + obj( + """{"running":false,"alive":true,"state":{"isPromptRunning":false},""" + + """"elsewhere":{"kind":"terminal","source":"process","running":true,"active":true}}""", + ), + ) + assertNull("our idle child owns the session", engine.state.value.foreignRun) + } + + @Test + fun `an answer without a child reports the foreign owner`() { + val engine = engine() + engine.setSession("s1") + engine.applyAgentState( + obj( + """{"running":false,"alive":false,"state":{"isPromptRunning":false},""" + + """"elsewhere":{"kind":"terminal","source":"process","running":true,"active":true}}""", + ), + ) + assertEquals(owner, engine.state.value.foreignRun) + } + + @Test + fun `a slow answer without a run settles the marked turn`() { + val engine = engine() + engine.setSession("s1") + engine.applyAgentState(obj("""{"running":true,"alive":true,"state":{"isPromptRunning":true}}""")) + assertTrue("the turn is marked", engine.state.value.busy) + + // 503 `session_slow` with `running:false`: the child is only blocked, so + // the turn the screen still shows is over. + engine.markLiveStateSlow(false) + + assertEquals("the run is settled", false, engine.state.value.busy) + assertTrue("the answer counts as live state", engine.state.value.liveStateKnown) + } + + @Test + fun `a slow answer that reports or omits the run keeps the turn`() { + val unknown = engine() + unknown.setSession("s1") + unknown.markLiveStateSlow(null) + assertTrue("an older server's answer keeps the turn marked", unknown.state.value.agentRunning) + + val running = engine() + running.setSession("s1") + running.markLiveStateSlow(true) + assertTrue("a reported run keeps the turn marked", running.state.value.agentRunning) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatLiveStateTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatLiveStateTest.kt new file mode 100644 index 000000000..a42b900f4 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ChatLiveStateTest.kt @@ -0,0 +1,100 @@ +package io.github.kahme247.ompweb.chat + +import io.github.kahme247.ompweb.domain.chatShowsConnecting +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * "Do we know anything about the live run yet?" — the flag behind the footer's + * *Connecting…* line. A cache-first screen paints the disk transcript before + * the server says a word, and an empty status line there reads as "idle". + */ +class ChatLiveStateTest { + + private val json = Json { ignoreUnknownKeys = true } + + private fun obj(text: String): JsonObject = json.parseToJsonElement(text).jsonObject + + private fun engine(): ChatEngine = ChatEngine(now = { 1_800_000_000_000L }) + + @Test + fun `a freshly opened session has an unknown live state`() { + val engine = engine() + engine.setSession("s1") + assertFalse("nothing has been heard from the server yet", engine.state.value.liveStateKnown) + assertTrue("cached rows do ask for the hint", chatShowsConnecting(false, hasTranscript = true)) + assertFalse("an empty chat keeps quiet", chatShowsConnecting(false, hasTranscript = false)) + } + + @Test + fun `a session frame proves the stream is live`() { + val engine = engine() + engine.setSession("s1") + engine.onFrame(obj("""{"type":"connected"}""")) + engine.flush() + assertTrue(engine.state.value.liveStateKnown) + assertFalse("the real state is shown instead", chatShowsConnecting(true, hasTranscript = true)) + } + + @Test + fun `any frame counts, even one the engine has no use for`() { + val engine = engine() + engine.setSession("s1") + // `turn_start`/`turn_end` are not handled by the engine (omp's turn + // markers); they still prove the session's stream is delivering. + engine.onFrame(obj("""{"type":"turn_start"}""")) + engine.flush() + assertTrue(engine.state.value.liveStateKnown) + } + + @Test + fun `an agent state snapshot settles the live state even when nothing runs`() { + val engine = engine() + engine.setSession("s1") + engine.applyAgentState(obj("""{"running":false,"state":{"isStreaming":false,"isPromptRunning":false}}""")) + assertTrue("the server answered for this session", engine.state.value.liveStateKnown) + assertFalse(engine.state.value.busy) + } + + @Test + fun `switching sessions forgets the known live state`() { + val engine = engine() + engine.setSession("s1") + engine.applyAgentState(obj("""{"running":false}""")) + assertTrue(engine.state.value.liveStateKnown) + engine.setSession("s2") + assertFalse("the next session starts over", engine.state.value.liveStateKnown) + } + + @Test + fun `a live context page settles the live state`() { + val engine = engine() + engine.setSession("s1") + val page = ContextSync.parse( + obj( + """ + {"mode":"replace","baseEntryId":null,"hasMore":false,"sessionId":"s1","leafId":"e1", + "context":{"entryIds":["e1"],"messages":[{"role":"user","content":"hi","timestamp":1}]}, + "live":{"cursor":{"streamId":"w","sequence":3},"isStreaming":false, + "isPromptRunning":false,"isCompacting":false}} + """.trimIndent(), + ), + ) + engine.applyContextPage(page) + assertTrue("the page proves the server has a live view", engine.state.value.liveStateKnown) + assertFalse("an idle live page is not a run", engine.state.value.busy) + } + + @Test + fun `an accepted prompt marks the live state known`() { + val engine = engine() + engine.setSession("s1") + engine.sendStarted("hello", runId = 1) + assertTrue("the prompt went to a live session", engine.state.value.liveStateKnown) + assertTrue(engine.state.value.busy) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/FileLinksTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/FileLinksTest.kt new file mode 100644 index 000000000..935c7340d --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/FileLinksTest.kt @@ -0,0 +1,45 @@ +package io.github.kahme247.ompweb.chat + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class FileLinksTest { + + @Test + fun `posix paths become catch-all segments`() { + assertEquals(listOf("home", "me", "a.ts"), FileLinks.apiPathSegments("/home/me/a.ts")) + assertEquals(listOf("relative", "a.ts"), FileLinks.apiPathSegments("relative/a.ts")) + } + + @Test + fun `dots never climb out of the workspace`() { + assertTrue(FileLinks.apiPathSegments("../../models").isEmpty()) + assertTrue(FileLinks.apiPathSegments("/home/me/../etc/passwd").isEmpty()) + assertTrue(FileLinks.apiPathSegments("./a.ts").isEmpty()) + } + + @Test + fun `windows drive paths use forward slashes`() { + assertEquals( + listOf("C:", "Users", "me", "a.ts"), + FileLinks.apiPathSegments("""C:\Users\me\a.ts"""), + ) + assertEquals( + listOf("C:", "Users", "me", "a.ts"), + FileLinks.apiPathSegments("C:/Users/me/a.ts"), + ) + } + + @Test + fun `unc paths keep their server prefix`() { + assertEquals( + listOf("//server", "share", "a.ts"), + FileLinks.apiPathSegments("""\\server\share\a.ts"""), + ) + assertEquals( + listOf("//server", "share", "a.ts"), + FileLinks.apiPathSegments("//server/share/a.ts"), + ) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/GenerationSpeedTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/GenerationSpeedTest.kt new file mode 100644 index 000000000..ada6b5c8c --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/GenerationSpeedTest.kt @@ -0,0 +1,184 @@ +package io.github.kahme247.ompweb.chat + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +/** + * The rate metric: omp's committed numbers (`usage.output`, `duration`, + * `ttft`, `completedAt`) and the streamed estimate that feeds the live value. + */ +class GenerationSpeedTest { + + private fun assistant( + output: Int? = null, + duration: Double? = null, + ttft: Double? = null, + completedAt: Long? = null, + timestamp: Long? = null, + stopReason: String? = null, + blocks: List = emptyList(), + ) = ChatMessage.Assistant( + key = "m", + timestamp = timestamp, + entryId = null, + blocks = blocks, + stopReason = stopReason, + durationMs = duration, + usageOutput = output, + ttftMs = ttft, + completedAt = completedAt, + ) + + private fun rate(message: ChatMessage.Assistant): Double? = + messageSpeed(message)?.tokensPerSecond + + @Test + fun `real sample A excludes the time to first token`() { + val speed = messageSpeed( + assistant(output = 7751, duration = 197638.7748, ttft = 2707.7157), + ) + assertEquals(39.76, speed!!.tokensPerSecond, 0.02) + assertEquals(7751, speed.outputTokens) + assertEquals(194931L, speed.spanMs) + } + + @Test + fun `real sample B`() { + assertEquals( + 46.55, + rate(assistant(output = 526, duration = 17799.3082, ttft = 6500.3954))!!, + 0.02, + ) + } + + @Test + fun `real sample C`() { + assertEquals( + 44.88, + rate(assistant(output = 17130, duration = 386090.2889, ttft = 4398.5871))!!, + 0.02, + ) + } + + @Test + fun `without ttft the whole duration is the span`() { + assertEquals(39.22, rate(assistant(output = 7751, duration = 197638.7748))!!, 0.02) + } + + @Test + fun `a sample whose generation lasted under a millisecond has no rate`() { + assertNull(rate(assistant(output = 4, duration = 1155.4055, ttft = 1154.4604))) + } + + @Test + fun `completedAt minus timestamp is the fallback span`() { + val message = assistant( + output = 100, + timestamp = 1_700_000_000_000L, + completedAt = 1_700_000_002_000L, + ) + assertEquals(50.0, rate(message)!!, 0.0001) + } + + @Test + fun `an error answer has no rate`() { + assertNull( + rate(assistant(output = 100, duration = 2000.0, stopReason = "error")), + ) + } + + @Test + fun `missing output and missing timestamps have no rate`() { + assertNull(rate(assistant(duration = 2000.0))) + assertNull(rate(assistant(output = 100))) + assertNull(rate(assistant(output = 0, duration = 2000.0))) + } + + @Test + fun `a ttft beyond the duration falls back to the whole duration`() { + assertEquals( + 50.0, + rate(assistant(output = 100, duration = 2000.0, ttft = 2500.0))!!, + 0.0001, + ) + } + + @Test + fun `estimate counts ascii quarters and every other code point whole`() { + val ascii = AssistantBlock.Text("a".repeat(400)) + val cjk = AssistantBlock.Text("漢".repeat(10)) + assertEquals(100.0, estimateStreamTokens(listOf(ascii)), 0.0001) + assertEquals(10.0, estimateStreamTokens(listOf(cjk)), 0.0001) + assertEquals(110.0, estimateStreamTokens(listOf(ascii, cjk)), 0.0001) + } + + @Test + fun `estimate includes thinking but not tool calls`() { + val blocks = listOf( + AssistantBlock.Thinking("abcd", deferred = false, blockIndex = 0), + AssistantBlock.ToolCall("t1", "bash", kotlinx.serialization.json.JsonObject(emptyMap())), + ) + assertEquals(1.0, estimateStreamTokens(blocks), 0.0001) + } + + @Test + fun `tracker reports the rate over the window`() { + val tracker = StreamRateTracker() + tracker.observe(100.0, at = 0L) + tracker.observe(300.0, at = 2_000L) + assertEquals(100.0, tracker.rate(2_000L)!!, 0.0001) + } + + @Test + fun `tracker hides a short and small window`() { + val tracker = StreamRateTracker() + tracker.observe(100.0, at = 0L) + tracker.observe(105.0, at = 100L) + assertNull(tracker.rate(100L)) + } + + @Test + fun `tracker goes stale when the stream stops`() { + val tracker = StreamRateTracker() + tracker.observe(100.0, at = 0L) + tracker.observe(300.0, at = 2_000L) + assertNull("no delta for six seconds", tracker.rate(8_000L)) + } + + @Test + fun `an unchanged estimate does not keep the window alive`() { + val tracker = StreamRateTracker() + tracker.observe(100.0, at = 0L) + tracker.observe(300.0, at = 2_000L) + // A poll of the same snapshot carries no delta: the newest real sample + // keeps its timestamp, so the reading still ages out. + tracker.observe(300.0, at = 3_500L) + assertEquals(100.0, tracker.rate(3_500L)!!, 0.0001) + assertNull("newest real delta was at 2 s", tracker.rate(4_100L)) + } + + @Test + fun `a shrinking estimate starts over from the new baseline`() { + val tracker = StreamRateTracker() + tracker.observe(300.0, at = 0L) + tracker.observe(100.0, at = 1_000L) + tracker.observe(300.0, at = 2_000L) + assertEquals(200.0, tracker.rate(2_000L)!!, 0.0001) + } + + @Test + fun `reset forgets every sample`() { + val tracker = StreamRateTracker() + tracker.observe(100.0, at = 0L) + tracker.observe(300.0, at = 2_000L) + tracker.reset() + assertNull(tracker.rate(2_000L)) + } + + @Test + fun `rate formatting is one decimal and locale independent`() { + assertEquals("39.8", formatGenerationRate(39.7627)) + assertEquals("12.3", formatGenerationRate(12.34)) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/ScrollLogicTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ScrollLogicTest.kt new file mode 100644 index 000000000..1154983f8 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/ScrollLogicTest.kt @@ -0,0 +1,242 @@ +package io.github.kahme247.ompweb.chat + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class ScrollLogicTest { + + @Test + fun `opening a session at the top never expands before the tail was seen`() { + val decision = ScrollLogic.decideLoadEarlier( + firstVisibleItemIndex = 0, + windowStart = 0, + nearBottom = false, + state = LoadEarlierState(), + ) + assertFalse(decision.expand) + assertFalse("the tail was never reached", decision.state.tailReached) + assertFalse("scrolling to the top alone does not arm the trigger", decision.state.armed) + } + + @Test + fun `reaching the tail arms the trigger for the next scroll up`() { + val atTail = ScrollLogic.decideLoadEarlier( + firstVisibleItemIndex = 4, + windowStart = 0, + nearBottom = true, + state = LoadEarlierState(), + ) + assertFalse(atTail.expand) + assertTrue(atTail.state.tailReached) + assertTrue(atTail.state.armed) + + val scrolledUp = ScrollLogic.decideLoadEarlier( + firstVisibleItemIndex = 0, + windowStart = 40, + nearBottom = false, + state = atTail.state, + ) + assertTrue(scrolledUp.expand) + assertFalse("one expansion consumes the arm", scrolledUp.state.armed) + } + + @Test + fun `the trigger does not cascade while the window is prepended`() { + val afterExpand = LoadEarlierDecision( + expand = true, + state = LoadEarlierState(tailReached = true, armed = false), + ).state + // The prepended window shifts the reader away from item 0, which re-arms. + val shifted = ScrollLogic.decideLoadEarlier( + firstVisibleItemIndex = 1, + windowStart = 20, + nearBottom = false, + state = afterExpand, + ) + assertFalse(shifted.expand) + assertTrue(shifted.state.armed) + + // A repeated item 0 without a re-arm must not expand again. + val repeated = ScrollLogic.decideLoadEarlier( + firstVisibleItemIndex = 0, + windowStart = 20, + nearBottom = false, + state = LoadEarlierState(tailReached = true, armed = false), + ) + assertFalse(repeated.expand) + assertFalse(repeated.state.armed) + } + + @Test + fun `the trigger never expands without history or while at the tail`() { + val noHistory = ScrollLogic.decideLoadEarlier( + firstVisibleItemIndex = 0, + windowStart = 0, + nearBottom = false, + state = LoadEarlierState(tailReached = true, armed = true), + ) + assertFalse(noHistory.expand) + + val atTail = ScrollLogic.decideLoadEarlier( + firstVisibleItemIndex = 0, + windowStart = 40, + nearBottom = true, + state = LoadEarlierState(tailReached = true, armed = true), + ) + assertFalse("a shrinking viewport at the bottom is not a scroll up", atTail.expand) + } + + @Test + fun `row to item mapping keeps the reader's row in place`() { + assertEquals(5, ScrollLogic.itemIndexForRow(rowIndex = 5, windowStart = 0)) + // With a prepended window the header occupies item 0. + assertEquals(2, ScrollLogic.itemIndexForRow(rowIndex = 5, windowStart = 4)) + assertEquals(1, ScrollLogic.itemIndexForRow(rowIndex = 12, windowStart = 12)) + assertEquals(1, ScrollLogic.itemIndexForRow(rowIndex = 3, windowStart = 12)) + } + + @Test + fun `the header reveals the first loaded row`() { + assertEquals(0, ScrollLogic.revealLoadedItem(windowStart = 0)) + assertEquals(1, ScrollLogic.revealLoadedItem(windowStart = 12)) + } + + @Test + fun `leading transcript items shift the row mapping`() { + // Extension band plus the compacted-history notice occupy items 0 and 1; + // the first transcript row is item 2, not item 0. + assertEquals(2, ScrollLogic.itemIndexForRow(rowIndex = 0, windowStart = 0, leadItems = 2)) + assertEquals(7, ScrollLogic.itemIndexForRow(rowIndex = 5, windowStart = 0, leadItems = 2)) + // A prepended window adds its "load earlier" row below the lead items. + assertEquals(3, ScrollLogic.revealLoadedItem(windowStart = 12, leadItems = 2)) + assertEquals(3, ScrollLogic.itemIndexForRow(rowIndex = 12, windowStart = 12, leadItems = 2)) + } + + @Test + fun `a persisted row survives every leading item combination`() { + for (leadItems in 0..2) { + for (windowStart in listOf(0, 4, 40)) { + for (rowIndex in windowStart until windowStart + 60) { + val item = ScrollLogic.itemIndexForRow(rowIndex, windowStart, leadItems) + assertEquals( + "row $rowIndex at window $windowStart with $leadItems lead item(s)", + rowIndex, + ScrollLogic.rowIndexForItem(item, windowStart, leadItems), + ) + } + } + } + // A header item at the top maps to the first row of the window. + assertEquals(40, ScrollLogic.rowIndexForItem(itemIndex = 0, windowStart = 40, leadItems = 1)) + assertEquals(40, ScrollLogic.rowIndexForItem(itemIndex = 1, windowStart = 40, leadItems = 1)) + assertEquals(41, ScrollLogic.rowIndexForItem(itemIndex = 3, windowStart = 40, leadItems = 1)) + } + + @Test + fun `a measured tail at the viewport end is at the bottom`() { + val atBottom = TailMeasure( + totalItems = 12, + lastVisibleIndex = 11, + lastVisibleEnd = 2000, + viewportEnd = 2000, + ) + assertTrue(ScrollLogic.tailAtBottom(atBottom)) + assertFalse("nothing to scroll when the tail is visible", ScrollLogic.needsTailScroll(atBottom)) + } + + @Test + fun `content that grew below the viewport needs a follow scroll`() { + // A new row (or a taller one) was laid out: the tail anchor moved out of + // view and the last visible item is the one above it. + val grew = TailMeasure( + totalItems = 13, + lastVisibleIndex = 10, + lastVisibleEnd = 1900, + viewportEnd = 2000, + ) + assertFalse(ScrollLogic.tailAtBottom(grew)) + assertTrue(ScrollLogic.needsTailScroll(grew)) + + // A tall tail item that is only partially visible is not pinned yet. + val partial = TailMeasure( + totalItems = 13, + lastVisibleIndex = 12, + lastVisibleEnd = 2040, + viewportEnd = 2000, + ) + assertTrue(ScrollLogic.needsTailScroll(partial)) + } + + @Test + fun `a list without a measured layout never scrolls itself`() { + val empty = TailMeasure(totalItems = 0, lastVisibleIndex = -1, lastVisibleEnd = 0, viewportEnd = 0) + assertFalse(ScrollLogic.needsTailScroll(empty)) + assertFalse(ScrollLogic.tailAtBottom(empty)) + assertTrue("an unmeasured list counts as the tail", ScrollLogic.isNearBottom(empty, 96)) + } + + @Test + fun `near bottom uses the tail anchor and a threshold`() { + // Tail anchor visible: at the bottom, whatever the offsets say. + assertTrue( + ScrollLogic.isNearBottom( + TailMeasure(totalItems = 12, lastVisibleIndex = 11, lastVisibleEnd = 1990, viewportEnd = 2000), + thresholdPx = 96, + ), + ) + // One long item crosses the viewport end; 80 px below is still near. + assertTrue( + ScrollLogic.isNearBottom( + TailMeasure(totalItems = 12, lastVisibleIndex = 10, lastVisibleEnd = 2080, viewportEnd = 2000), + thresholdPx = 96, + ), + ) + // The same item ends 400 px past the viewport end: the reader is up. + assertFalse( + ScrollLogic.isNearBottom( + TailMeasure(totalItems = 12, lastVisibleIndex = 10, lastVisibleEnd = 2400, viewportEnd = 2000), + thresholdPx = 96, + ), + ) + // An item two slots above the tail is visible: far from the bottom. + assertFalse( + ScrollLogic.isNearBottom( + TailMeasure(totalItems = 12, lastVisibleIndex = 9, lastVisibleEnd = 2000, viewportEnd = 2000), + thresholdPx = 96, + ), + ) + } + + @Test + fun `following resumes after a restore only at the tail`() { + val atTail = + TailMeasure(totalItems = 12, lastVisibleIndex = 11, lastVisibleEnd = 1990, viewportEnd = 2000) + val above = + TailMeasure(totalItems = 12, lastVisibleIndex = 9, lastVisibleEnd = 2000, viewportEnd = 2000) + // The saved position is the newest row: the stream keeps following. + assertTrue(ScrollLogic.followAfterRestore(499, 500, above, thresholdPx = 96)) + // The saved position sits mid-transcript: it stays where the reader left it. + assertFalse(ScrollLogic.followAfterRestore(100, 500, above, thresholdPx = 96)) + // A position that landed at the bottom anyway keeps following. + assertTrue(ScrollLogic.followAfterRestore(480, 500, atTail, thresholdPx = 96)) + assertFalse(ScrollLogic.followAfterRestore(480, 500, above, thresholdPx = 96)) + } + + @Test + fun `a restored window stays where the reader left it`() { + // 500 rows: the reader scrolled 100 rows up, the window is frozen there. + assertEquals(400, ScrollLogic.clampWindow(windowStart = 400, rowCount = 500)) + assertEquals(440, ScrollLogic.clampWindow(windowStart = 440, rowCount = 500)) + } + + @Test + fun `a restored window never points past the transcript`() { + // The transcript shrank while the surface was gone (branch switch or a + // compaction dropped rows): the saved window would render nothing. + assertEquals(39, ScrollLogic.clampWindow(windowStart = 400, rowCount = 40)) + assertEquals(0, ScrollLogic.clampWindow(windowStart = -5, rowCount = 40)) + assertEquals(0, ScrollLogic.clampWindow(windowStart = 120, rowCount = 0)) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/SubagentAsyncResultsTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/SubagentAsyncResultsTest.kt new file mode 100644 index 000000000..a6e9151b4 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/SubagentAsyncResultsTest.kt @@ -0,0 +1,126 @@ +package io.github.kahme247.ompweb.chat + +import io.github.kahme247.ompweb.domain.SubagentInfo +import io.github.kahme247.ompweb.domain.SubagentSource +import io.github.kahme247.ompweb.domain.SubagentStatus +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Test + +/** + * A detached `task` is recorded as `started` when it is spawned and gets no + * lifecycle frame afterwards; the transcript's `async-result` delivery is the + * only proof that the run ended. Without this the finished chip kept reading + * "Running" and showed no duration. + */ +class SubagentAsyncResultsTest { + + private fun job( + id: String, + type: String, + status: String, + durationMs: Long? = null, + ): JsonObject = buildJsonObject { + put("jobId", id) + put("agentId", id) + put("type", type) + put("status", status) + durationMs?.let { put("durationMs", it) } + } + + private fun asyncResult(vararg jobs: JsonObject): ChatMessage.Custom = ChatMessage.Custom( + key = "async:${jobs.size}", + timestamp = 2_000, + entryId = null, + customType = "async-result", + text = "Background job finished", + details = buildJsonObject { + putJsonArray("jobs") { jobs.forEach { add(it) } } + }, + ) + + private fun historyEntry( + id: String, + status: SubagentStatus = SubagentStatus.STARTED, + ): SubagentInfo = SubagentInfo( + id = id, + agent = "task", + status = status, + index = 0, + source = SubagentSource.HISTORY, + ) + + @Test + fun `a delivered task settles its roster entry and gains its duration`() { + val roster = listOf(historyEntry("ProbeRun")) + val settled = settleSubagentsFromAsyncResults( + roster, + listOf(asyncResult(job("ProbeRun", "task", "completed", 71_047))), + ).single() + + assertEquals(SubagentStatus.COMPLETED, settled.status) + assertEquals(71_047L, settled.progress?.durationMs) + assertEquals(SubagentStatus.COMPLETED, settled.progress?.status) + } + + @Test + fun `a failed delivery marks the entry failed`() { + val settled = settleSubagentsFromAsyncResults( + listOf(historyEntry("ProbeRun")), + listOf(asyncResult(job("ProbeRun", "task", "failed"))), + ).single() + + assertEquals(SubagentStatus.FAILED, settled.status) + } + + @Test + fun `other job types and unknown ids leave the roster untouched`() { + val roster = listOf(historyEntry("ProbeRun")) + val settled = settleSubagentsFromAsyncResults( + roster, + listOf( + asyncResult(job("bg_1", "bash", "completed")), + asyncResult(job("Someone", "task", "completed")), + ), + ) + + assertSame(roster, settled) + } + + @Test + fun `a status without a terminal state changes nothing`() { + val roster = listOf(historyEntry("ProbeRun")) + val settled = settleSubagentsFromAsyncResults( + roster, + listOf(asyncResult(job("ProbeRun", "task", "running"))), + ) + + assertSame(roster, settled) + } + + @Test + fun `a settled entry is never regressed`() { + val settled = settleSubagentsFromAsyncResults( + listOf(historyEntry("ProbeRun", SubagentStatus.ABORTED)), + listOf(asyncResult(job("ProbeRun", "task", "completed"))), + ).single() + + assertEquals(SubagentStatus.ABORTED, settled.status) + } + + @Test + fun `a transcript without async deliveries leaves the roster untouched`() { + val roster = listOf(historyEntry("ProbeRun")) + val settled = settleSubagentsFromAsyncResults( + roster, + listOf(ChatMessage.User("u1", 1_000, null, "go")), + ) + + assertSame(roster, settled) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/TranscriptFormatTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/TranscriptFormatTest.kt new file mode 100644 index 000000000..d9bca7ec2 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/TranscriptFormatTest.kt @@ -0,0 +1,448 @@ +package io.github.kahme247.ompweb.chat + +import java.time.ZoneId +import kotlinx.serialization.json.buildJsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * The transcript's pure formatting rules: which date shape a timestamp needs, + * where day separators go and which blocks share a speaker header. The + * Android-side rendering only asks this helper and formats the result. + */ +class TranscriptFormatTest { + + private val zone = ZoneId.of("Europe/Berlin") + + /** 2026-10-26 12:00 in [zone] (CET, UTC+1). */ + private val now = 1793012400000L + + private fun stamp(year: Int, month: Int, day: Int, hour: Int, minute: Int): Long = + java.time.ZonedDateTime.of(year, month, day, hour, minute, 0, 0, zone) + .toInstant() + .toEpochMilli() + + // ------------------------------------------------------------- time buckets + + @Test + fun `a timestamp later today is today`() { + assertEquals(TimeBucket.Today, TranscriptFormat.timeBucket(stamp(2026, 10, 26, 23, 59), now, zone)) + assertEquals(TimeBucket.Today, TranscriptFormat.timeBucket(stamp(2026, 10, 26, 0, 1), now, zone)) + } + + @Test + fun `the day before is yesterday and two days back is this year`() { + assertEquals(TimeBucket.Yesterday, TranscriptFormat.timeBucket(stamp(2026, 10, 25, 23, 59), now, zone)) + assertEquals(TimeBucket.SameYear, TranscriptFormat.timeBucket(stamp(2026, 10, 24, 23, 59), now, zone)) + } + + @Test + fun `a previous year is the older shape`() { + assertEquals(TimeBucket.OlderYear, TranscriptFormat.timeBucket(stamp(2025, 12, 31, 23, 59), now, zone)) + } + + @Test + fun `a timestamp in the future keeps its bucket`() { + assertEquals(TimeBucket.SameYear, TranscriptFormat.timeBucket(stamp(2026, 11, 2, 9, 0), now, zone)) + } + + @Test + fun `day buckets name today and yesterday and leave the rest to a date`() { + assertEquals(DayBucket.Today, TranscriptFormat.dayBucket(stamp(2026, 10, 26, 8, 0), now, zone)) + assertEquals(DayBucket.Yesterday, TranscriptFormat.dayBucket(stamp(2026, 10, 25, 8, 0), now, zone)) + assertEquals(DayBucket.Date, TranscriptFormat.dayBucket(stamp(2026, 10, 20, 8, 0), now, zone)) + assertEquals(DayBucket.Date, TranscriptFormat.dayBucket(stamp(2025, 10, 20, 8, 0), now, zone)) + } + + // ----------------------------------------------------------- elapsed shapes + + @Test + fun `elapsed time reads as seconds, minutes and hours`() { + assertEquals("0 s", TranscriptFormat.formatElapsedShort(0)) + assertEquals("12 s", TranscriptFormat.formatElapsedShort(12_400)) + assertEquals("59 s", TranscriptFormat.formatElapsedShort(59_999)) + assertEquals("1 min 00 s", TranscriptFormat.formatElapsedShort(60_000)) + assertEquals("3 min 05 s", TranscriptFormat.formatElapsedShort(185_000)) + assertEquals("1 h 00 min", TranscriptFormat.formatElapsedShort(3_600_000)) + assertEquals("1 h 02 min", TranscriptFormat.formatElapsedShort(3_725_000)) + } + + @Test + fun `a clock that went backwards never shows a negative time`() { + assertEquals("0 s", TranscriptFormat.formatElapsedShort(-5_000)) + } + + // ------------------------------------------------------------ header blocks + + @Test + fun `consecutive blocks of the same speaker share one header`() { + val blocks = listOf( + HeaderBlock("u1", Speaker(SpeakerRole.You), now), + HeaderBlock("a1", Speaker(SpeakerRole.Assistant, "deepseek-v4.1-flash"), now), + HeaderBlock("a2", Speaker(SpeakerRole.Assistant, "deepseek-v4.1-flash"), now + 1_000), + HeaderBlock("u2", Speaker(SpeakerRole.You), now + 2_000), + ) + val slots = TranscriptFormat.headerPlan(blocks, zone) + assertEquals(listOf(true, true, false, true), slots.map { it.showHeader }) + } + + @Test + fun `a new day always starts a header`() { + val yesterday = stamp(2026, 10, 25, 23, 50) + val blocks = listOf( + HeaderBlock("u1", Speaker(SpeakerRole.You), yesterday), + HeaderBlock("a1", Speaker(SpeakerRole.Assistant), yesterday + 60_000), + HeaderBlock("a2", Speaker(SpeakerRole.Assistant), stamp(2026, 10, 26, 0, 1)), + ) + val slots = TranscriptFormat.headerPlan(blocks, zone) + assertEquals(listOf(true, true, true), slots.map { it.showHeader }) + } + + @Test + fun `a block without a speaker neither draws a header nor breaks a run`() { + val blocks = listOf( + HeaderBlock("a1", Speaker(SpeakerRole.Assistant), now), + HeaderBlock("custom", null, now + 1_000), + HeaderBlock("a2", Speaker(SpeakerRole.Assistant), now + 2_000), + ) + val slots = TranscriptFormat.headerPlan(blocks, zone) + assertEquals(listOf(true, false, false), slots.map { it.showHeader }) + } + + @Test + fun `the first block always shows its header`() { + val slots = TranscriptFormat.headerPlan( + listOf(HeaderBlock("u1", Speaker(SpeakerRole.You), now)), + zone, + ) + assertTrue(slots.single().showHeader) + } + + @Test + fun `the header plan is addressable by block key`() { + val slots = TranscriptFormat.headerSlots( + listOf( + HeaderBlock("turn:u1", Speaker(SpeakerRole.You), now), + HeaderBlock("turn:u1:assistant", Speaker(SpeakerRole.Assistant, "glm-5"), now + 1_000), + ), + zone, + ) + assertTrue(slots.getValue("turn:u1").showHeader) + assertTrue(slots.getValue("turn:u1:assistant").showHeader) + assertEquals("glm-5", slots.getValue("turn:u1:assistant").speaker?.name) + } + + // ------------------------------------------------------------ header blocks + + @Test + fun `a turn group gets a user header and one assistant header`() { + val user = ChatMessage.User("u1", now, "entry-u1", "run it") + val answer = ChatMessage.Assistant( + key = "a1", + timestamp = now + 2_000, + entryId = "entry-a1", + blocks = listOf(AssistantBlock.Text("done")), + model = "deepseek/deepseek-v4.1-flash:high", + provider = "deepseek", + ) + val rows = listOf( + TranscriptRow.Group( + anchor = user, + anchorIndex = 0, + endIndex = 2, + segments = listOf( + TurnSegment.Text( + message = answer, + messageIndex = 1, + blocks = answer.blocks, + last = true, + key = "text:a1:0", + ), + ), + ), + ) + val blocks = TranscriptFormat.headerBlocks(rows) + assertEquals( + listOf("turn:u1", "turn:u1" + TranscriptFormat.ASSISTANT_HEADER_SUFFIX), + blocks.map { it.key }, + ) + assertEquals(SpeakerRole.You, blocks[0].speaker?.role) + assertEquals(SpeakerRole.Assistant, blocks[1].speaker?.role) + assertEquals("deepseek-v4.1-flash", blocks[1].speaker?.name) + assertEquals(now + 2_000, blocks[1].timestamp) + } + + @Test + fun `a turn group without an assistant message still gets an assistant header`() { + val user = ChatMessage.User("u1", now, "entry-u1", "run it") + val bash = ChatMessage.BashExecution( + key = "b1", + timestamp = now + 1_000, + entryId = null, + command = "ls", + output = "", + exitCode = null, + ) + val rows = listOf( + TranscriptRow.Group( + anchor = user, + anchorIndex = 0, + endIndex = 2, + segments = listOf( + TurnSegment.Activity( + pieces = listOf(ActivityPiece(bash, 1)), + stepCount = 1, + toolCallCount = 0, + key = "activity:u1:0", + ), + ), + ), + ) + val blocks = TranscriptFormat.headerBlocks(rows) + assertEquals(SpeakerRole.Assistant, blocks[1].speaker?.role) + assertNull(blocks[1].speaker?.name) + assertEquals(now, blocks[1].timestamp) + } + + @Test + fun `standalone messages carry their own speaker block`() { + val rows = listOf( + TranscriptRow.Message(ChatMessage.User("u1", now, null, "hi")), + TranscriptRow.Message(ChatMessage.ToolResult("r1", now, null, "t1", "read", "x")), + ) + val blocks = TranscriptFormat.headerBlocks(rows) + assertEquals(2, blocks.size) + assertEquals(SpeakerRole.You, blocks[0].speaker?.role) + assertNull(blocks[1].speaker) + } + + // ---------------------------------------------------------------- speakers + + @Test + fun `the assistant header names the message's model`() { + val message = ChatMessage.Assistant( + key = "a1", + timestamp = now, + entryId = null, + blocks = emptyList(), + model = "anthropic/claude-sonnet-4:high", + provider = "anthropic", + ) + val speaker = TranscriptFormat.speakerOf(message) + assertEquals(SpeakerRole.Assistant, speaker?.role) + assertEquals("claude-sonnet-4", speaker?.name) + } + + @Test + fun `an assistant without a model falls back to the localized default`() { + val message = ChatMessage.Assistant("a1", now, null, emptyList()) + val speaker = TranscriptFormat.speakerOf(message) + assertEquals(SpeakerRole.Assistant, speaker?.role) + assertNull(speaker?.name) + assertEquals('A', speaker?.initial) + } + + @Test + fun `a user header is You and a branch summary has none`() { + assertEquals(SpeakerRole.You, TranscriptFormat.speakerOf(ChatMessage.User("u1", now, null, "x"))?.role) + assertNull(TranscriptFormat.speakerOf(ChatMessage.User("u1", now, null, "x", branchSummary = true))) + assertNull(TranscriptFormat.speakerOf(ChatMessage.ToolResult("r1", now, null, "t1", "bash", "x"))) + } + + @Test + fun `the badge initial comes from the speaker name`() { + assertEquals('Y', Speaker(SpeakerRole.You).initial) + assertEquals('D', Speaker(SpeakerRole.Assistant, "deepseek-v4.1-flash").initial) + assertEquals('E', Speaker(SpeakerRole.Subagent, "explore").initial) + assertEquals('S', Speaker(SpeakerRole.Subagent).initial) + } + + // ------------------------------------------------------------ turn timing + + @Test + fun `a day change inside one turn draws its own separator`() { + val user = ChatMessage.User("u1", stamp(2026, 10, 25, 23, 50), null, "go") + val first = ChatMessage.Assistant( + key = "a1", + timestamp = stamp(2026, 10, 25, 23, 51), + entryId = null, + blocks = listOf(AssistantBlock.Text("before midnight")), + ) + val second = ChatMessage.Assistant( + key = "a2", + timestamp = stamp(2026, 10, 26, 0, 10), + entryId = null, + blocks = listOf(AssistantBlock.Text("after midnight")), + ) + val rows = listOf( + TranscriptRow.Group( + anchor = user, + anchorIndex = 0, + endIndex = 3, + segments = listOf( + TurnSegment.Text(first, 1, first.blocks, last = false, key = "text:a1:0"), + TurnSegment.Text(second, 2, second.blocks, last = true, key = "text:a2:1"), + ), + ), + ) + + val breaks = TranscriptFormat.daySeparators(rows, zone = zone) + + assertEquals(setOf("text:a2:1"), breaks.keys) + assertEquals(stamp(2026, 10, 26, 0, 10), breaks.getValue("text:a2:1")) + } + + @Test + fun `a standalone message on a new day draws a separator even without a speaker`() { + val user = ChatMessage.User("u1", stamp(2026, 10, 25, 23, 50), null, "go") + val notice = ChatMessage.Custom( + key = "notice", + timestamp = stamp(2026, 10, 26, 0, 5), + entryId = null, + customType = "async-result", + text = "done", + ) + val next = ChatMessage.User("u2", stamp(2026, 10, 26, 0, 6), null, "next") + + val breaks = TranscriptFormat.daySeparators( + listOf( + TranscriptRow.Message(user), + TranscriptRow.Message(notice), + TranscriptRow.Message(next), + ), + zone = zone, + ) + + assertEquals(setOf("message:notice"), breaks.keys) + } + + @Test + fun `an invisible assistant message does not carry the day change`() { + val user = ChatMessage.User("u1", stamp(2026, 10, 25, 23, 50), null, "go") + val thinkingOnly = ChatMessage.Assistant( + key = "a1", + timestamp = stamp(2026, 10, 26, 0, 2), + entryId = null, + blocks = listOf(AssistantBlock.Thinking("hmm", deferred = false, blockIndex = 0)), + ) + val answer = ChatMessage.Assistant( + key = "a2", + timestamp = stamp(2026, 10, 26, 0, 10), + entryId = null, + blocks = listOf(AssistantBlock.Text("done")), + ) + val rows = listOf( + TranscriptRow.Message(user), + TranscriptRow.Message(thinkingOnly), + TranscriptRow.Message(answer), + ) + + val hidden = TranscriptFormat.daySeparators(rows, hideThinking = true, zone = zone) + val shown = TranscriptFormat.daySeparators(rows, hideThinking = false, zone = zone) + + assertEquals(setOf("message:a2"), hidden.keys) + assertEquals(setOf("message:a1"), shown.keys) + } + + @Test + fun `the live tail separates visible messages across midnight`() { + val user = ChatMessage.User("u1", stamp(2026, 10, 25, 23, 50), null, "go") + val invisible = ChatMessage.Assistant( + key = "a1", + timestamp = stamp(2026, 10, 26, 0, 1), + entryId = null, + blocks = listOf(AssistantBlock.Thinking("hmm", deferred = false, blockIndex = 0)), + ) + val answer = ChatMessage.Assistant( + key = "a2", + timestamp = stamp(2026, 10, 26, 0, 10), + entryId = null, + blocks = listOf(AssistantBlock.Text("done")), + ) + val messages = listOf(user, invisible, answer) + + val breaks = TranscriptFormat.messageDaySeparators( + messages = messages, + startIndex = 0, + endIndex = messages.size, + hideThinking = true, + zone = zone, + ) + + assertEquals(setOf("a2"), breaks.keys) + } + + @Test + fun `the live tail header hangs on the first visible assistant`() { + val user = ChatMessage.User("u1", now, null, "go") + val thinkingOnly = ChatMessage.Assistant( + key = "a1", + timestamp = now + 1_000, + entryId = null, + blocks = listOf(AssistantBlock.Thinking("hmm", deferred = false, blockIndex = 0)), + ) + val answer = ChatMessage.Assistant( + key = "a2", + timestamp = now + 2_000, + entryId = null, + blocks = listOf(AssistantBlock.Text("done")), + ) + val messages = listOf(user, thinkingOnly, answer) + + assertEquals( + 2, + TranscriptFormat.firstVisibleAssistantIndex(messages, 0, messages.size, hideThinking = true), + ) + assertEquals( + 1, + TranscriptFormat.firstVisibleAssistantIndex(messages, 0, messages.size, hideThinking = false), + ) + } + + @Test + fun `the turn timer starts at the prompt and never resets on a tool or steering`() { + val state = ChatState( + runStartedAt = 1_000, + messages = listOf( + ChatMessage.User("u1", 1_000, null, "go"), + ChatMessage.Assistant("a1", 2_000, null, emptyList()), + ChatMessage.User("u2", 7_000, null, "steer"), + ), + liveTools = listOf(LiveTool("t1", "bash", buildJsonObject { }, null, startedAt = 5_000)), + ) + + assertEquals(1_000L, TranscriptFormat.turnStartedAt(state)) + } + + @Test + fun `a running turn starts with its earliest running tool`() { + val state = ChatState( + messages = listOf(ChatMessage.User("u1", 1_000, null, "go")), + liveTools = listOf( + LiveTool("t1", "bash", buildJsonObject { }, null, startedAt = 5_000), + LiveTool("t2", "read", buildJsonObject { }, null, startedAt = 3_000), + LiveTool("t0", "bash", buildJsonObject { }, null, startedAt = 2_000, endedAt = 4_000), + ), + ) + assertEquals(3_000L, TranscriptFormat.turnStartedAt(state)) + } + + @Test + fun `without a running tool the newest prompt starts the turn`() { + val state = ChatState( + messages = listOf( + ChatMessage.User("u1", 1_000, null, "first"), + ChatMessage.Assistant("a1", 2_000, null, emptyList()), + ChatMessage.User("u2", 7_000, null, "second"), + ), + ) + assertEquals(7_000L, TranscriptFormat.turnStartedAt(state)) + } + + @Test + fun `an idle transcript has no turn start`() { + assertNull(TranscriptFormat.turnStartedAt(ChatState())) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/chat/TranscriptPlanTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/chat/TranscriptPlanTest.kt new file mode 100644 index 000000000..09a8f882c --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/chat/TranscriptPlanTest.kt @@ -0,0 +1,138 @@ +package io.github.kahme247.ompweb.chat + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class TranscriptPlanTest { + + private val json = Json { ignoreUnknownKeys = true } + + private fun obj(text: String): JsonObject = json.parseToJsonElement(text).jsonObject + + private fun user(text: String, timestamp: Long = 1) = parseChatMessage( + obj("""{"role":"user","content":[{"type":"text","text":"$text"}],"timestamp":$timestamp}"""), + entryId = "u$timestamp", + syntheticKey = "u", + )!! + + private fun assistant(timestamp: Long, content: String) = parseChatMessage( + obj("""{"role":"assistant","content":$content,"timestamp":$timestamp}"""), + entryId = "a$timestamp", + syntheticKey = "a", + )!! + + @Test + fun `a turn with tools folds its activity into one segment`() { + val messages = listOf( + user("run it"), + assistant(2, """[{"type":"thinking","thinking":"plan"}]"""), + assistant(3, """[{"type":"toolCall","toolCallId":"c1","toolName":"bash","input":{"command":"echo hi"}}]"""), + parseChatMessage( + obj("""{"role":"toolResult","toolCallId":"c1","toolName":"bash","content":[{"type":"text","text":"hi"}]}"""), + "r1", + "r", + )!!, + assistant(4, """[{"type":"text","text":"done"}]"""), + ) + val rows = TranscriptPlan.build(messages) + val group = rows.filterIsInstance().single() + assertEquals("one activity segment, one text segment", 2, group.segments.size) + val activity = group.segments[0] as TurnSegment.Activity + assertEquals(1, activity.toolCallCount) + assertTrue(activity.pieces.any { it.message is ChatMessage.ToolResult || it.blocks?.any { b -> b is AssistantBlock.ToolCall } == true }) + val text = group.segments[1] as TurnSegment.Text + assertTrue(text.last) + assertEquals("done", (text.blocks.single() as AssistantBlock.Text).text) + } + + @Test + fun `a plain answer renders as standalone messages`() { + val rows = TranscriptPlan.build(listOf(user("hi"), assistant(2, """[{"type":"text","text":"hello"}]"""))) + assertEquals(2, rows.filterIsInstance().size) + assertTrue(rows.filterIsInstance().isEmpty()) + } + + @Test + fun `hide thinking removes thinking from the fold`() { + val messages = listOf( + user("hi"), + assistant(2, """[{"type":"toolCall","toolCallId":"c1","toolName":"bash","input":{}}]"""), + assistant(3, """[{"type":"thinking","thinking":"aha"}]"""), + assistant(4, """[{"type":"text","text":"done"}]"""), + ) + val rows = TranscriptPlan.build(messages, hideThinking = true) + val group = rows.filterIsInstance().single() + val activity = group.segments.filterIsInstance() + assertTrue(activity.all { piece -> piece.pieces.all { it.blocks?.any { b -> b is AssistantBlock.Thinking } != true } }) + } + + @Test + fun `tool results are indexed by call id and passive context attaches to the last call`() { + val messages = listOf( + user("hi"), + assistant(2, """[{"type":"toolCall","toolCallId":"c1","toolName":"bash","input":{}}]"""), + assistant(3, """[{"type":"toolCall","toolCallId":"c2","toolName":"read","input":{}}]"""), + parseChatMessage( + obj("""{"role":"toolResult","toolCallId":"c1","toolName":"bash","content":"one"}"""), + "r1", + "r", + )!!, + parseChatMessage( + obj("""{"role":"toolResult","toolCallId":"c2","toolName":"read","content":"two"}"""), + "r2", + "r", + )!!, + parseChatMessage( + obj("""{"role":"custom","customType":"passive-tool-context","content":"\u001B[31mred\u001B[0m context"}"""), + "p1", + "p", + )!!, + assistant(4, """[{"type":"text","text":"done"}]"""), + ) + val results = TranscriptPlan.collectToolResults(messages) + assertEquals(2, results.size) + assertEquals("red context", results["c2"]?.passiveContext?.trim()) + assertNull(results["c1"]?.passiveContext) + } + + @Test + fun `passive tool context strips ansi and control characters`() { + val sanitized = TranscriptPlan.sanitizePassiveToolContext("\u001B[1;32mok\u001B[0m\u0000\n\ntext") + assertEquals("ok\n\ntext", sanitized) + } + + // ------------------------------------------------ display preferences + + @Test + fun `a running tool call opens unless the reader collapsed tool cards`() { + assertTrue(ChatDisplayPrefs.toolCallExpanded(toolCallsCollapsed = false, running = true)) + assertFalse("collapsed cards stay folded while running", ChatDisplayPrefs.toolCallExpanded(true, true)) + assertFalse("a finished call always starts folded", ChatDisplayPrefs.toolCallExpanded(false, false)) + } + + @Test + fun `expand thinking opens reasoning blocks`() { + assertTrue(ChatDisplayPrefs.thinkingExpanded(expandThinking = true)) + assertFalse("the web default folds reasoning", ChatDisplayPrefs.thinkingExpanded(false)) + } + + @Test + fun `the live tail is flat only while tools are visible`() { + assertTrue(ChatDisplayPrefs.liveTail(hideToolActivity = false, streaming = true, isLastGroup = true)) + assertFalse( + "hiding tool activity keeps the fold in place", + ChatDisplayPrefs.liveTail(hideToolActivity = true, streaming = true, isLastGroup = true), + ) + assertFalse( + "only the last group is the live tail", + ChatDisplayPrefs.liveTail(hideToolActivity = false, streaming = true, isLastGroup = false), + ) + assertFalse(ChatDisplayPrefs.liveTail(hideToolActivity = false, streaming = false, isLastGroup = true)) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/data/AesGcmCipherTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/data/AesGcmCipherTest.kt new file mode 100644 index 000000000..a36cf818a --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/data/AesGcmCipherTest.kt @@ -0,0 +1,106 @@ +package io.github.kahme247.ompweb.data + +import java.io.File +import java.security.GeneralSecurityException +import javax.crypto.spec.SecretKeySpec +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class AesGcmCipherTest { + + @get:Rule + val folder = TemporaryFolder() + + private val key = SecretKeySpec(ByteArray(32) { it.toByte() }, "AES") + private val cipher = AesGcmCipher { key } + + private val cookie = StoredCookie( + name = "omp_web_session", + value = "header.payload.signature", + expiresAtMillis = 4_000_000_000_000L, + domain = "100.64.0.10", + path = "/", + secure = false, + httpOnly = true, + ) + + @Test + fun `round trip returns the plaintext`() { + val plain = "session=secret".toByteArray() + + assertArrayEquals(plain, cipher.decrypt(cipher.encrypt(plain))) + } + + @Test + fun `every encryption uses a fresh iv`() { + val plain = "session=secret".toByteArray() + + val first = cipher.encrypt(plain) + val second = cipher.encrypt(plain) + + assertEquals(AesGcmCipher.IV_BYTES + plain.size + AesGcmCipher.TAG_BITS / 8, first.size) + assertFalse(first.contentEquals(second)) + } + + @Test + fun `a foreign key cannot decrypt`() { + val payload = cipher.encrypt("session=secret".toByteArray()) + val other = AesGcmCipher { SecretKeySpec(ByteArray(32) { (it + 1).toByte() }, "AES") } + + assertThrows(GeneralSecurityException::class.java) { other.decrypt(payload) } + } + + @Test + fun `tampered ciphertext is refused`() { + val payload = cipher.encrypt("session=secret".toByteArray()) + payload[payload.size - 1] = (payload[payload.size - 1].toInt() xor 0x01).toByte() + + assertThrows(GeneralSecurityException::class.java) { cipher.decrypt(payload) } + } + + @Test + fun `records are encrypted on disk and can be read back`() { + val file = File(folder.root, "cookies.bin") + EncryptedCookieRecords(file, cipher).save(listOf(cookie)) + + val raw = file.readBytes() + assertFalse(String(raw, Charsets.UTF_8).contains("omp_web_session")) + assertFalse(String(raw, Charsets.UTF_8).contains("header.payload.signature")) + assertEquals(listOf(cookie), EncryptedCookieRecords(file, cipher).load()) + } + + @Test + fun `a file with a foreign key reads as signed out`() { + val file = File(folder.root, "cookies.bin") + EncryptedCookieRecords(file, cipher).save(listOf(cookie)) + val other = AesGcmCipher { SecretKeySpec(ByteArray(32) { (it + 1).toByte() }, "AES") } + + assertTrue(EncryptedCookieRecords(file, other).load().isEmpty()) + } + + @Test + fun `corrupt bytes read as signed out`() { + val file = File(folder.root, "cookies.bin") + file.writeText("not a cookie document") + + assertTrue(EncryptedCookieRecords(file, cipher).load().isEmpty()) + } + + @Test + fun `an empty list deletes the file`() { + val file = File(folder.root, "cookies.bin") + val records = EncryptedCookieRecords(file, cipher) + records.save(listOf(cookie)) + + records.save(emptyList()) + + assertFalse(file.exists()) + assertTrue(records.load().isEmpty()) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/data/DraftStoreLogicTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/data/DraftStoreLogicTest.kt new file mode 100644 index 000000000..7b4fe2b3c --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/data/DraftStoreLogicTest.kt @@ -0,0 +1,69 @@ +package io.github.kahme247.ompweb.data + +import io.github.kahme247.ompweb.domain.ComposerImage +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** Draft merging, eviction and the recovered-text rules of `lib/draft-store.ts`. */ +class DraftStoreLogicTest { + + @Test + fun `recovery prepends like a message block`() { + assertEquals("new\n\nexisting", mergeRecoveredText("existing", DraftRecovery("new"))) + assertEquals("new", mergeRecoveredText("", DraftRecovery("new"))) + assertEquals("", mergeRecoveredText("", DraftRecovery(""))) + } + + @Test + fun `a later recovery replaces the earlier block while it is still the lead`() { + val first = DraftRecovery("one") + val merged = mergeRecoveredText("", first) + val second = DraftRecovery( + text = "one\n\ntwo", + replace = DraftRecovery.Replace(lead = "one", fallback = "two"), + ) + assertEquals("one\n\ntwo", mergeRecoveredText(merged, second)) + } + + @Test + fun `a changed draft keeps both the fallback and the user's text`() { + val recovery = DraftRecovery( + text = "queued", + replace = DraftRecovery.Replace(lead = "old", fallback = "queued"), + ) + assertEquals("queued\n\ntyped", mergeRecoveredText("typed", recovery)) + } + + @Test + fun `the registry keeps insertion order and evicts the oldest draft`() { + val registry = DraftRegistry(maxDrafts = 2) + registry.set("a", ChatDraft(value = "first")) + registry.set("b", ChatDraft(value = "second")) + registry.set("a", ChatDraft(value = "updated")) + registry.set("c", ChatDraft(value = "third")) + + assertNull("the first-inserted key is evicted", registry.get("a")) + assertEquals("second", registry.get("b")?.value) + assertEquals("third", registry.get("c")?.value) + } + + @Test + fun `only text and metadata survive a registry clone`() { + val registry = DraftRegistry() + registry.set( + "s1", + ChatDraft( + value = "draft", + images = listOf(ComposerImage("AAAA", "image/png")), + ), + ) + val clone = registry.get("s1")!! + assertEquals("draft", clone.value) + assertEquals(1, clone.images.size) + assertTrue(clone.hasAttachments) + assertFalse(registry.isEmpty()) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/data/OriginBoundCookieJarTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/data/OriginBoundCookieJarTest.kt new file mode 100644 index 000000000..9f5cc49f1 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/data/OriginBoundCookieJarTest.kt @@ -0,0 +1,187 @@ +package io.github.kahme247.ompweb.data + +import java.io.IOException +import okhttp3.Cookie +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class OriginBoundCookieJarTest { + + private val origin = "http://100.64.0.10:30177/api/sessions".toHttpUrl() + private val otherPort = "http://100.64.0.10:30176/api/sessions".toHttpUrl() + private val otherHost = "http://100.64.0.11:30177/api/sessions".toHttpUrl() + + private fun jar( + configured: HttpUrl? = origin, + records: CookieRecords = InMemoryCookieRecords(), + now: Long = 1_000L, + ): OriginBoundCookieJar = OriginBoundCookieJar({ configured }, records, { now }) + + private fun cookie(value: String = "token", expiresAt: Long = 10_000L): Cookie = + Cookie.Builder() + .name("omp_web_session") + .value(value) + .domain("100.64.0.10") + .path("/") + .expiresAt(expiresAt) + .build() + + @Test + fun `a cookie of the configured origin is sent back to it`() { + val jar = jar() + jar.saveFromResponse(origin, listOf(cookie("token"))) + + val sent = jar.loadForRequest(origin) + + assertEquals(listOf("token"), sent.map { it.value }) + } + + @Test + fun `a cookie is never sent to another port on the same host`() { + val jar = jar() + jar.saveFromResponse(origin, listOf(cookie())) + + assertTrue(jar.loadForRequest(otherPort).isEmpty()) + } + + @Test + fun `a cookie is never sent to another host`() { + val jar = jar() + jar.saveFromResponse(origin, listOf(cookie())) + + assertTrue(jar.loadForRequest(otherHost).isEmpty()) + } + + @Test + fun `a cookie is never sent over another scheme`() { + val secureOrigin = "https://omp.example:30178/api/sessions".toHttpUrl() + val secureJar = OriginBoundCookieJar( + configuredOrigin = { secureOrigin }, + records = InMemoryCookieRecords(), + ) + val secureCookie = Cookie.Builder() + .name("omp_web_session") + .value("token") + .domain("omp.example") + .path("/") + .expiresAt(10_000L) + .secure() + .build() + + secureJar.saveFromResponse(secureOrigin, listOf(secureCookie)) + + assertTrue(secureJar.loadForRequest("http://omp.example:30178/api/sessions".toHttpUrl()).isEmpty()) + } + + @Test + fun `a Set-Cookie from another origin is ignored`() { + val jar = jar() + jar.saveFromResponse(otherPort, listOf(cookie())) + + assertTrue(jar.loadForRequest(origin).isEmpty()) + } + + @Test + fun `an expired cookie is dropped and removed from storage`() { + val records = InMemoryCookieRecords() + val jar = jar(records = records, now = 2_000L) + jar.saveFromResponse(origin, listOf(cookie(expiresAt = 1_500L))) + + assertTrue(jar.loadForRequest(origin).isEmpty()) + assertTrue(records.load().isEmpty()) + } + + @Test + fun `stored cookies survive a new jar instance`() { + val records = InMemoryCookieRecords() + jar(records = records).saveFromResponse(origin, listOf(cookie("token"))) + + val restarted = jar(records = records) + + assertEquals(listOf("token"), restarted.loadForRequest(origin).map { it.value }) + } + + @Test + fun `clear forgets the session`() { + val records = InMemoryCookieRecords() + val jar = jar(records = records) + jar.saveFromResponse(origin, listOf(cookie())) + + jar.clear() + + assertTrue(jar.loadForRequest(origin).isEmpty()) + assertTrue(records.load().isEmpty()) + } + + @Test + fun `without a configured origin nothing is sent`() { + val jar = jar(configured = null) + jar.saveFromResponse(origin, listOf(cookie())) + + assertTrue(jar.loadForRequest(origin).isEmpty()) + } + + @Test + fun `switching servers drops the old server's cookie`() { + val records = InMemoryCookieRecords() + jar(records = records).saveFromResponse(origin, listOf(cookie("old"))) + + val nextServer = jar(configured = otherHost, records = records) + + assertTrue(nextServer.loadForRequest(otherHost).isEmpty()) + assertTrue(nextServer.loadForRequest(origin).isEmpty()) + assertTrue(records.load().isEmpty()) + } + + @Test + fun `a Set-Cookie for a request started before clear is dropped`() { + val records = InMemoryCookieRecords() + val jar = jar(records = records) + jar.loadForRequest(origin) + + jar.clear() + jar.saveFromResponse(origin, listOf(cookie("late"))) + + assertTrue(jar.loadForRequest(origin).isEmpty()) + assertTrue(records.load().isEmpty()) + } + + @Test + fun `records written without an origin are never loaded`() { + val legacy = StoredCookie( + name = "omp_web_session", + value = "token", + expiresAtMillis = 10_000L, + domain = "100.64.0.10", + path = "/", + secure = false, + httpOnly = true, + ) + val records = InMemoryCookieRecords(listOf(legacy)) + + val jar = jar(records = records) + + assertTrue(jar.loadForRequest(origin).isEmpty()) + assertTrue(records.load().isEmpty()) + } + + @Test + fun `a failing cookie store does not break save or load`() { + val jar = jar(records = FailingCookieRecords()) + + jar.saveFromResponse(origin, listOf(cookie("token"))) + + assertEquals(listOf("token"), jar.loadForRequest(origin).map { it.value }) + } + + private class FailingCookieRecords : CookieRecords { + override fun load(): List = emptyList() + + override fun save(cookies: List) { + throw IOException("disk full") + } + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/data/OriginScopedDraftStoreTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/data/OriginScopedDraftStoreTest.kt new file mode 100644 index 000000000..5b4fdde0c --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/data/OriginScopedDraftStoreTest.kt @@ -0,0 +1,110 @@ +package io.github.kahme247.ompweb.data + +import io.github.kahme247.ompweb.testutil.MemoryDrafts +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Draft keys are bound to the configured server origin, so a second + * installation's session ids can never surface another server's unsent text. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class OriginScopedDraftStoreTest { + + @Test + fun `drafts are stored under the current origin only`() = runTest(UnconfinedTestDispatcher()) { + val raw = MemoryDrafts() + val origin = MutableStateFlow("https://a.example:8443") + val store = OriginScopedDraftStore(raw, backgroundScope, origin) + + store.setDraft("s1", ChatDraft(value = "hello")) + assertEquals( + "the persisted key carries the origin", + ChatDraft(value = "hello"), + raw.getDraft("https://a.example:8443\u0000s1"), + ) + assertEquals(ChatDraft(value = "hello"), store.getDraft("s1")) + + origin.value = "https://b.example" + assertNull("another origin never sees the draft", store.getDraft("s1")) + assertTrue("the published state follows the origin", store.state.value.isEmpty()) + store.setDraft("s1", ChatDraft(value = "other")) + assertEquals( + "a same-named key of the second origin is separate", + ChatDraft(value = "hello"), + raw.getDraft("https://a.example:8443\u0000s1"), + ) + assertEquals("other", store.state.value["s1"]?.value) + + origin.value = "https://a.example:8443" + assertEquals( + "switching back restores the first origin's draft", + "hello", + store.getDraft("s1")?.value, + ) + assertEquals("mine becomes visible again", setOf("s1"), store.state.value.keys) + assertEquals("hello", store.state.value["s1"]?.value) + } + + @Test + fun `the published state is filtered to the current origin`() = runTest(UnconfinedTestDispatcher()) { + val raw = MemoryDrafts() + val origin = MutableStateFlow("https://a.example") + val store = OriginScopedDraftStore(raw, backgroundScope, origin) + + store.setDraft("s1", ChatDraft(value = "mine")) + raw.setDraft("https://b.example\u0000s2", ChatDraft(value = "theirs")) + assertEquals("a foreign key never enters the state", setOf("s1"), store.state.value.keys) + + origin.value = "https://b.example" + assertEquals(setOf("s2"), store.state.value.keys) + assertEquals("theirs", store.state.value["s2"]?.value) + } + + @Test + fun `without an origin nothing is published`() = runTest(UnconfinedTestDispatcher()) { + val raw = MemoryDrafts(mapOf("https://a.example\u0000s1" to ChatDraft(value = "a"))) + val store = OriginScopedDraftStore(raw, backgroundScope, MutableStateFlow(null)) + + assertTrue("no session, no visible drafts", store.state.value.isEmpty()) + } + + @Test + fun `recovery events are mapped back to plain keys and foreign origins are dropped`() = + runTest(UnconfinedTestDispatcher()) { + val raw = MemoryDrafts() + val store = OriginScopedDraftStore(raw, backgroundScope, MutableStateFlow("https://a.example")) + val events = mutableListOf() + backgroundScope.launch { store.recoveries.collect { events += it } } + + raw.recoverDraft("https://a.example\u0000s1", DraftRecovery(text = "queued")) + raw.recoverDraft("https://b.example\u0000s2", DraftRecovery(text = "leaked")) + + assertEquals(listOf("s1"), events.map { it.key }) + assertEquals("queued", events.single().recovery.text) + } + + @Test + fun `clearAll drops every origin`() = runTest(UnconfinedTestDispatcher()) { + val raw = MemoryDrafts( + mapOf( + "https://a.example\u0000s1" to ChatDraft(value = "a"), + "https://b.example\u0000s2" to ChatDraft(value = "b"), + ), + ) + val store = OriginScopedDraftStore(raw, backgroundScope, MutableStateFlow("https://a.example")) + + store.clearAll() + + assertNull(raw.getDraft("https://a.example\u0000s1")) + assertNull(raw.getDraft("https://b.example\u0000s2")) + assertTrue(store.state.value.isEmpty()) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/data/SessionCacheStoreTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/data/SessionCacheStoreTest.kt new file mode 100644 index 000000000..69117e72b --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/data/SessionCacheStoreTest.kt @@ -0,0 +1,200 @@ +package io.github.kahme247.ompweb.data + +import java.io.File +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +/** + * JVM tests for the cache-first store: origin isolation, the transcript LRU + * with its three bounds, and that a broken file is a miss instead of a crash + * (a cache must never be able to break the app that reads it). + */ +class SessionCacheStoreTest { + + @get:Rule + val temp = TemporaryFolder() + + private var now = 1_000L + + private fun store( + maxTranscripts: Int = 3, + maxTotalBytes: Long = 1_000, + maxTranscriptBytes: Long = 400, + ) = SessionCacheStore( + root = File(temp.root, "session-cache"), + clock = { now }, + maxTranscripts = maxTranscripts, + maxTotalBytes = maxTotalBytes, + maxTranscriptBytes = maxTranscriptBytes, + ) + + @Test + fun listAndProjectsRoundTripPerOrigin() { + val cache = store() + cache.writeSessions("http://a:1", """{"sessions":[{"id":"s1"}]}""", "\"etag-1\"") + cache.writeProjects("http://a:1", """{"projects":[{"path":"/w"}]}""") + + val stored = cache.readSessions("http://a:1") + assertEquals("""{"sessions":[{"id":"s1"}]}""", stored?.body) + assertEquals("\"etag-1\"", stored?.etag) + assertEquals("""{"projects":[{"path":"/w"}]}""", cache.readProjects("http://a:1")) + } + + @Test + fun anotherOriginNeverSeesTheList() { + val cache = store() + cache.writeSessions("http://a:1", """{"sessions":[]}""", null) + + assertNull("a server switch must not inherit the list", cache.readSessions("http://b:2")) + assertNull(cache.readProjects("http://b:2")) + assertNull(cache.read("http://b:2", "s1", null, false)) + } + + @Test + fun transcriptRoundTripRequiresTheSameView() { + val cache = store() + cache.write("http://a:1", "s1", "leaf-1", false, """{"context":{}}""") + + assertEquals("""{"context":{}}""", cache.read("http://a:1", "s1", "leaf-1", false)) + assertNull("another branch is not this transcript", cache.read("http://a:1", "s1", "leaf-2", false)) + assertNull(cache.read("http://a:1", "s1", "leaf-1", true)) + assertNull(cache.read("http://a:1", "s2", "leaf-1", false)) + } + + @Test + fun transcriptsAreEvictedLeastRecentlyUsedFirst() { + val cache = store() + cache.write("http://a:1", "s1", null, false, """{"n":1}""") + now = 2_000L + cache.write("http://a:1", "s2", null, false, """{"n":2}""") + now = 3_000L + cache.write("http://a:1", "s3", null, false, """{"n":3}""") + + // Touch s1 so it becomes the most recently used entry. + now = 4_000L + assertNotNull(cache.read("http://a:1", "s1", null, false)) + + now = 5_000L + cache.write("http://a:1", "s4", null, false, """{"n":4}""") + + assertEquals(3, cache.transcriptCount("http://a:1")) + assertNotNull("touched entry survives", cache.read("http://a:1", "s1", null, false)) + assertNull("oldest untouched entry is gone", cache.read("http://a:1", "s2", null, false)) + assertNotNull(cache.read("http://a:1", "s3", null, false)) + assertNotNull(cache.read("http://a:1", "s4", null, false)) + } + + @Test + fun aTranscriptLargerThanThePerResponseCapIsNotCached() { + val cache = store(maxTranscriptBytes = 20) + cache.write("http://a:1", "s1", null, false, """{"payload":"${"x".repeat(64)}"}""") + + assertNull(cache.read("http://a:1", "s1", null, false)) + assertEquals(0, cache.transcriptCount("http://a:1")) + } + + @Test + fun anOversizedRewriteDropsThePreviousCopy() { + val cache = store(maxTranscriptBytes = 20) + cache.write("http://a:1", "s1", null, false, """{"v":1}""") + assertNotNull(cache.read("http://a:1", "s1", null, false)) + + cache.write("http://a:1", "s1", null, false, """{"payload":"${"x".repeat(64)}"}""") + + assertNull("a stale copy must not answer the open", cache.read("http://a:1", "s1", null, false)) + } + + @Test + fun theTotalBudgetEvictsTheOldestEntries() { + // Four 100-byte bodies fit the count (10) but not the 250-byte budget. + val cache = store(maxTranscripts = 10, maxTotalBytes = 250, maxTranscriptBytes = 400) + for (index in 1..3) { + now = 1_000L * index + cache.write("http://a:1", "s$index", null, false, "x".repeat(100)) + } + + assertEquals(2, cache.transcriptCount("http://a:1")) + assertNull(cache.read("http://a:1", "s1", null, false)) + assertNotNull(cache.read("http://a:1", "s2", null, false)) + assertNotNull(cache.read("http://a:1", "s3", null, false)) + } + + @Test + fun aCorruptTranscriptFileIsAMissAndIsForgotten() { + val cache = store() + cache.write("http://a:1", "s1", null, false, """{"ok":true}""") + val originDirs = File(temp.root, "session-cache").listFiles().orEmpty() + val bodyFiles = originDirs.flatMap { dir -> dir.listFiles().orEmpty().filter { it.name.startsWith("t-") } } + assertEquals(1, bodyFiles.size) + bodyFiles.single().writeText("") + + assertNull(cache.read("http://a:1", "s1", null, false)) + assertEquals(0, cache.transcriptCount("http://a:1")) + } + + @Test + fun aCorruptListFileIsAMiss() { + val cache = store() + cache.writeSessions("http://a:1", """{"sessions":[]}""", null) + val originDir = File(temp.root, "session-cache").listFiles().orEmpty().single() + File(originDir, "sessions.json").writeText("{not json") + + assertNull(cache.readSessions("http://a:1")) + } + + @Test + fun clearAllDropsEveryOrigin() { + val cache = store() + cache.writeSessions("http://a:1", """{"sessions":[]}""", null) + cache.write("http://a:1", "s1", null, false, """{"n":1}""") + cache.write("http://b:2", "s1", null, false, """{"n":1}""") + + cache.clearAll() + + assertNull(cache.readSessions("http://a:1")) + assertNull(cache.read("http://a:1", "s1", null, false)) + assertNull(cache.read("http://b:2", "s1", null, false)) + assertTrue(File(temp.root, "session-cache").listFiles().orEmpty().isEmpty()) + } + + @Test + fun aCorruptTranscriptIndexDropsTheOrphanedBodies() { + val cache = store(maxTranscripts = 10) + cache.write("http://a:1", "s1", null, false, "x".repeat(100)) + now = 2_000L + cache.write("http://a:1", "s2", null, false, "y".repeat(100)) + val dir = File(temp.root, "session-cache").listFiles().orEmpty().single() + assertEquals(2, dir.listFiles().orEmpty().count { it.name.startsWith("t-") }) + + // A broken index (filesystem, external tool): the entries are gone, the + // bodies are still there and were unreachable from then on. + File(dir, "transcripts.json").writeText("{not json") + assertNull(cache.read("http://a:1", "s1", null, false)) + + now = 3_000L + cache.write("http://a:1", "s3", null, false, "z".repeat(100)) + + assertEquals( + "only the indexed body survives the rebuild", + 1, + dir.listFiles().orEmpty().count { it.name.startsWith("t-") }, + ) + assertEquals(1, cache.transcriptCount("http://a:1")) + assertNull("the orphaned transcript stays a miss", cache.read("http://a:1", "s1", null, false)) + } + + @Test + fun anEmptyBodyIsNeverStored() { + val cache = store() + cache.write("http://a:1", "s1", null, false, "") + cache.writeSessions("http://a:1", "", "\"etag\"") + + assertNull(cache.read("http://a:1", "s1", null, false)) + assertNull(cache.readSessions("http://a:1")) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/data/SettingsLoadedWaitTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/data/SettingsLoadedWaitTest.kt new file mode 100644 index 000000000..f7844de01 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/data/SettingsLoadedWaitTest.kt @@ -0,0 +1,47 @@ +package io.github.kahme247.ompweb.data + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * X6/X7: a restarted service and a notification action run before DataStore's + * first emission. Both wait via `SettingsStore.awaitLoaded`; these tests pin + * that the wait ignores every not-yet-loaded snapshot instead of returning the + * empty default (which has no origin and would stop the service / drop the + * answer). + */ +@OptIn(ExperimentalCoroutinesApi::class) +class SettingsLoadedWaitTest { + + @Test + fun `an already loaded snapshot is returned immediately`() = runTest { + val snapshot = MutableStateFlow(SettingsSnapshot(loaded = true)) + assertEquals(true, awaitLoadedSnapshot(snapshot).loaded) + } + + @Test + fun `the wait stays suspended until the first loaded snapshot`() = runTest { + val snapshot = MutableStateFlow(SettingsSnapshot()) + var result: SettingsSnapshot? = null + val job = launch { result = awaitLoadedSnapshot(snapshot) } + runCurrent() + assertNull(result) + + // A later not-loaded emission is still not an answer. + snapshot.value = SettingsSnapshot(themeId = "other") + runCurrent() + assertNull(result) + + snapshot.value = SettingsSnapshot(loaded = true, themeId = "other") + runCurrent() + assertTrue(job.isCompleted) + assertEquals("other", result?.themeId) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/AgentEnvTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/AgentEnvTest.kt new file mode 100644 index 000000000..6a79fd5a8 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/AgentEnvTest.kt @@ -0,0 +1,116 @@ +package io.github.kahme247.ompweb.domain + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * The client-side mirror of `lib/omp/agent-env-policy.ts`: the Settings editor + * must reject a line before the server would, and never let a reserved name + * through (A22). + */ +class AgentEnvTest { + + @Test + fun `valid lines parse into values`() { + val result = AgentEnv.parse( + """ + # comment + FOO=bar + export PATHX=/usr/bin + EMPTY= + SPACED = padded value + """.trimIndent(), + ) + + assertEquals(mapOf("FOO" to "bar", "PATHX" to "/usr/bin", "EMPTY" to "", "SPACED" to "padded value"), result.values) + assertTrue(result.valid) + assertEquals(emptyList(), result.errors) + } + + @Test + fun `a later assignment wins like a shell`() { + val result = AgentEnv.parse("FOO=1\nFOO=2") + + assertEquals(mapOf("FOO" to "2"), result.values) + assertTrue(result.valid) + } + + @Test + fun `a line without a separator is reported with its line number`() { + val result = AgentEnv.parse("FOO=bar\nBROKEN\n") + + assertEquals(mapOf("FOO" to "bar"), result.values) + assertEquals(listOf("Line 2: expected KEY=VALUE"), result.errors) + assertFalse(result.valid) + } + + @Test + fun `an empty name is reported`() { + val result = AgentEnv.parse("=value") + + assertTrue(result.values.isEmpty()) + assertEquals(listOf("Line 1: missing variable name"), result.errors) + } + + @Test + fun `an invalid name is reported`() { + val result = AgentEnv.parse("1BAD=x\nALSO-BAD=y\nDOTTED.NAME=z") + + assertTrue(result.values.isEmpty()) + assertEquals( + listOf( + "Line 1: \"1BAD\" is not a valid variable name", + "Line 2: \"ALSO-BAD\" is not a valid variable name", + "Line 3: \"DOTTED.NAME\" is not a valid variable name", + ), + result.errors, + ) + } + + @Test + fun `reserved exact names are denied case-insensitively`() { + for (name in listOf("PATH", "Path", "PORT", "port", "NODE_OPTIONS", "HOME", "PI_CODING_AGENT_DIR", "NODE_ENV")) { + val result = AgentEnv.parse("$name=1") + assertTrue("$name must be denied", result.values.isEmpty()) + assertEquals( + listOf("Line 1: \"$name\" is reserved by omp-web and cannot be set"), + result.errors, + ) + } + } + + @Test + fun `reserved prefixes are denied`() { + for (name in listOf("OMP_WEB_TOKEN", "omp_web_password", "NEXT_PUBLIC_X", "_PRIVATE")) { + val result = AgentEnv.parse("$name=1") + assertTrue("$name must be denied", result.values.isEmpty()) + assertEquals(listOf("Line 1: \"$name\" is reserved by omp-web and cannot be set"), result.errors) + } + assertTrue(AgentEnv.isDeniedName("_")) + } + + @Test + fun `sanitize applies the same rules to an already structured record`() { + val result = AgentEnv.sanitize(mapOf("FOO" to "bar", "PATH" to "/bin")) + + assertEquals(mapOf("FOO" to "bar"), result.values) + assertEquals(1, result.errors.size) + } + + @Test + fun `format sorts by name so saves stay stable`() { + assertEquals("A=1\nB=2\nC=3", AgentEnv.format(mapOf("C" to "3", "A" to "1", "B" to "2"))) + assertEquals("", AgentEnv.format(emptyMap())) + } + + @Test + fun `format and parse round-trip`() { + val values = mapOf("ALPHA" to "one two", "BETA" to "", "GAMMA" to "a=b") + val parsed = AgentEnv.parse(AgentEnv.format(values)) + + assertEquals(values, parsed.values) + assertTrue(parsed.valid) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/AgentsLogicTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/AgentsLogicTest.kt new file mode 100644 index 000000000..777c07214 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/AgentsLogicTest.kt @@ -0,0 +1,195 @@ +package io.github.kahme247.ompweb.domain + +import io.github.kahme247.ompweb.net.AgentInfo +import io.github.kahme247.ompweb.ui.settings.agents.AGENT_THINKING_LEVELS +import io.github.kahme247.ompweb.ui.settings.agents.AgentCounts +import io.github.kahme247.ompweb.ui.settings.agents.AgentDraft +import io.github.kahme247.ompweb.ui.settings.agents.AgentValidation +import io.github.kahme247.ompweb.ui.settings.agents.agentFileStem +import io.github.kahme247.ompweb.ui.settings.agents.agentSaveScope +import io.github.kahme247.ompweb.ui.settings.agents.agentWritableScopes +import io.github.kahme247.ompweb.ui.settings.agents.buildAgentPayload +import io.github.kahme247.ompweb.ui.settings.agents.filterAgents +import io.github.kahme247.ompweb.ui.settings.agents.joinAgentCsv +import io.github.kahme247.ompweb.ui.settings.agents.shortenAgentPath +import io.github.kahme247.ompweb.ui.settings.agents.splitAgentCsv +import io.github.kahme247.ompweb.ui.settings.agents.validateAgentDraft +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.put +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** Mirrors `AgentsConfig.tsx` helpers and `agents-service.ts` payload rules. */ +class AgentsLogicTest { + + @Test + fun `agent names follow the service regex`() { + assertTrue(OMP_RESOURCE_NAME_RE.matches("designer")) + assertTrue(OMP_RESOURCE_NAME_RE.matches("a")) + assertTrue(OMP_RESOURCE_NAME_RE.matches("Review_2.0-beta")) + assertFalse(OMP_RESOURCE_NAME_RE.matches("")) + assertFalse(OMP_RESOURCE_NAME_RE.matches("-leading")) + assertFalse(OMP_RESOURCE_NAME_RE.matches(".hidden")) + assertFalse(OMP_RESOURCE_NAME_RE.matches("a/b")) + assertFalse(OMP_RESOURCE_NAME_RE.matches("a".repeat(129))) + assertTrue(OMP_RESOURCE_NAME_RE.matches("a".repeat(128))) + } + + @Test + fun `csv helpers trim and drop empties but keep order`() { + assertEquals(listOf("a", "b", "c"), splitAgentCsv(" a , b,,c ")) + assertEquals(emptyList(), splitAgentCsv(" ")) + assertEquals("a, b", joinAgentCsv(listOf(" a ", "b", ""))) + assertEquals("", joinAgentCsv(null)) + } + + @Test + fun `file stems work with windows and unix paths`() { + assertEquals("designer", agentFileStem("C:\\Users\\me\\.omp\\agents\\designer.md")) + assertEquals("designer", agentFileStem("/home/me/.omp/agents/designer.md")) + assertEquals("UPPER", agentFileStem("/a/UPPER.MD")) + assertEquals("noext", agentFileStem("/a/noext")) + } + + @Test + fun `search matches name and description case-insensitively`() { + val agents = listOf( + AgentInfo(name = "designer", description = "UI specialist"), + AgentInfo(name = "reviewer", description = "Reads diffs"), + ) + assertEquals(listOf("designer"), filterAgents(agents, "UI").map { it.name }) + assertEquals(listOf("reviewer"), filterAgents(agents, "REVIEW").map { it.name }) + assertEquals(2, filterAgents(agents, " ").size) + assertEquals(0, filterAgents(agents, "nope").size) + } + + @Test + fun `draft validation reports the first problem`() { + assertEquals(AgentValidation.NAME_REQUIRED, validateAgentDraft(" ", "desc")) + assertEquals(AgentValidation.NAME_PATTERN, validateAgentDraft("-bad", "desc")) + assertEquals(AgentValidation.DESCRIPTION_REQUIRED, validateAgentDraft("ok", " ")) + assertEquals(AgentValidation.OK, validateAgentDraft(" ok ", "desc")) + } + + @Test + fun `payload mirrors the web save body`() { + val draft = AgentDraft( + name = "designer", + description = " UI specialist ", + modelCsv = "@designer, @smol", + toolsCsv = "read, edit", + spawnsCsv = "task", + thinkingLevel = "high", + body = "You design UIs.", + ) + val frontmatter = Json.parseToJsonElement("""{"prewalk":true,"enabled":false}""").jsonObject + val payload = buildAgentPayload(draft, existingFrontmatter = frontmatter) + assertEquals("UI specialist", (payload["description"] as JsonPrimitive).content) + assertEquals("You design UIs.", (payload["body"] as JsonPrimitive).content) + assertEquals( + listOf("@designer", "@smol"), + (payload["model"] as JsonArray).map { (it as JsonPrimitive).content }, + ) + assertEquals(listOf("read", "edit"), (payload["tools"] as JsonArray).map { (it as JsonPrimitive).content }) + assertEquals(listOf("task"), (payload["spawns"] as JsonArray).map { (it as JsonPrimitive).content }) + assertEquals("high", (payload["thinkingLevel"] as JsonPrimitive).content) + assertEquals(frontmatter, payload["existingFrontmatter"]) + } + + @Test + fun `payload omits empty fields and keeps the spawns star`() { + val payload = buildAgentPayload(AgentDraft(name = "a", description = "d", spawnsCsv = " * ")) + assertEquals(setOf("description", "spawns"), payload.keys) + assertEquals("*", (payload["spawns"] as JsonPrimitive).content) + assertNull(payload["body"]) + assertNull(payload["existingFrontmatter"]) + assertNull(payload["model"]) + assertNull(payload["tools"]) + assertNull(payload["thinkingLevel"]) + + val built = buildJsonObject { put("description", "d") } + assertEquals(built, buildAgentPayload(AgentDraft(description = "d"))) + } + + @Test + fun `a saved agent fills the form back`() { + val agent = AgentInfo( + name = "designer", + description = "UI", + model = listOf("@designer"), + tools = listOf("read"), + spawnsAll = true, + thinkingLevel = "low", + body = "Body", + ) + val draft = AgentDraft.from(agent) + assertEquals("@designer", draft.modelCsv) + assertEquals("read", draft.toolsCsv) + assertEquals("*", draft.spawnsCsv) + assertEquals("low", draft.thinkingLevel) + assertEquals("Body", draft.body) + } + + @Test + fun `save scope never writes bundled and edits keep their scope`() { + assertEquals(listOf("project", "user"), agentWritableScopes(canEditProject = true)) + assertEquals(listOf("user"), agentWritableScopes(canEditProject = false)) + + assertEquals("project", agentSaveScope(creating = true, active = null, chosen = "project", canEditProject = true)) + assertEquals("user", agentSaveScope(creating = true, active = null, chosen = "project", canEditProject = false)) + assertEquals( + "user", + agentSaveScope(creating = false, active = AgentInfo(name = "u", scope = "user"), chosen = "project", canEditProject = true), + ) + assertEquals( + "project", + agentSaveScope(creating = false, active = AgentInfo(name = "p", scope = "project"), chosen = "user", canEditProject = true), + ) + assertEquals( + "project", + agentSaveScope(creating = false, active = AgentInfo(name = "b", scope = "bundled"), chosen = "user", canEditProject = true), + ) + assertEquals( + "user", + agentSaveScope(creating = false, active = AgentInfo(name = "b", scope = "bundled"), chosen = "project", canEditProject = false), + ) + } + + @Test + fun `counts split bundled, user and project agents`() { + val counts = AgentCounts.of( + listOf( + AgentInfo(name = "a", source = "bundled", scope = "bundled"), + AgentInfo(name = "b", source = "user", scope = "user"), + AgentInfo(name = "c", source = "project", scope = "project"), + AgentInfo(name = "d", source = "project", scope = "project"), + ), + ) + assertEquals(AgentCounts(total = 4, bundled = 1, user = 1, project = 2), counts) + } + + @Test + fun `long paths are shortened from the left`() { + val path = "/home/user/.omp/agents/" + "x".repeat(40) + ".md" + val shortened = shortenAgentPath(path) + assertEquals(48, shortened.length) + assertTrue(shortened.startsWith("…")) + assertTrue(shortened.endsWith(".md")) + assertEquals("short", shortenAgentPath("short")) + } + + @Test + fun `thinking levels list matches the service`() { + assertEquals( + listOf("auto", "off", "minimal", "low", "medium", "high", "xhigh", "max"), + AGENT_THINKING_LEVELS, + ) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/AndroidUpdateInstallTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/AndroidUpdateInstallTest.kt new file mode 100644 index 000000000..56c74031e --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/AndroidUpdateInstallTest.kt @@ -0,0 +1,140 @@ +package io.github.kahme247.ompweb.domain + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * The decisions that run between "download finished" and "PackageInstaller + * session": checksum, package names, version codes and the signing + * certificate of the installed app. + */ +class AndroidUpdateInstallTest { + + private val sha = "a".repeat(64) + + private fun identity( + packageName: String = PACKAGE, + versionCode: Long = 10001, + signer: String = "d73354b52dc7c3f0c74623c439ddb7b52e252811c6e6b5f851c9d04b7015fc87", + ) = AndroidApkIdentity( + packageName = packageName, + versionCode = versionCode, + versionName = "1.0.1", + signerSha256 = setOf(signer), + ) + + private fun decide( + installed: AndroidApkIdentity? = identity(versionCode = 10000), + downloaded: AndroidApkIdentity? = identity(), + announcedVersionCode: Long? = 10001, + expectedSha256: String = sha, + actualSha256: String = sha, + ) = decideAndroidUpdateInstall( + appPackageName = PACKAGE, + installed = installed, + downloaded = downloaded, + announcedVersionCode = announcedVersionCode, + expectedSha256 = expectedSha256, + actualSha256 = actualSha256, + ) + + @Test + fun `a matching newer APK signed with the installed key may install`() { + assertEquals(AndroidInstallDecision.Install, decide()) + } + + @Test + fun `a tampered SHA-256 is refused`() { + val decision = decide(actualSha256 = "b".repeat(64)) as AndroidInstallDecision.Refuse + + assertTrue(decision.message.contains("SHA-256 mismatch")) + } + + @Test + fun `another package name is refused`() { + assertTrue((decide(downloaded = identity(packageName = "com.example.evil")) as AndroidInstallDecision.Refuse) + .message.contains("another package")) + assertTrue((decide(installed = identity(packageName = "com.example.other")) as AndroidInstallDecision.Refuse) + .message.contains("does not match this package")) + } + + @Test + fun `a different signing key is refused`() { + val decision = decide( + downloaded = identity(signer = "06d9d698c6a43f643a27643618fd4f7d431d26d4"), + ) as AndroidInstallDecision.Refuse + + assertTrue(decision.message.contains("different key")) + } + + @Test + fun `a version code that is not higher is refused`() { + assertTrue( + ( + decide( + downloaded = identity(versionCode = 10000), + announcedVersionCode = 10000, + ) as AndroidInstallDecision.Refuse + ).message.contains("not newer"), + ) + } + + @Test + fun `a downloaded version code must match the announcement`() { + val decision = decide(announcedVersionCode = 10002) as AndroidInstallDecision.Refuse + + assertTrue(decision.message.contains("does not match the announced one")) + } + + @Test + fun `an unreadable APK or app is refused with a reason`() { + assertTrue((decide(downloaded = null) as AndroidInstallDecision.Refuse) + .message.contains("not an installable app package")) + assertTrue((decide(installed = null) as AndroidInstallDecision.Refuse) + .message.contains("could not be read")) + assertTrue( + (decide(downloaded = identity().copy(signerSha256 = emptySet())) as AndroidInstallDecision.Refuse) + .message.contains("signing certificate"), + ) + } + + @Test + fun `an invalid expected digest is refused before anything else`() { + val decision = decide(expectedSha256 = "not-a-digest") as AndroidInstallDecision.Refuse + + assertTrue(decision.message.contains("no valid SHA-256")) + } + + @Test + fun `the digest helpers accept only 64 hex characters`() { + assertTrue(isAndroidUpdateSha256(sha)) + assertTrue(isAndroidUpdateSha256("A".repeat(64))) + assertTrue(!isAndroidUpdateSha256("a".repeat(63))) + assertTrue(!isAndroidUpdateSha256("g".repeat(64))) + assertNull(normalizeAndroidUpdateSha256(null)) + assertEquals(sha, normalizeAndroidUpdateSha256("A".repeat(64))) + } + + @Test + fun `a sha256sum file yields its digest`() { + assertEquals(sha, parseAndroidUpdateSha256Asset("$sha ompweb-1.0.1.apk\n")) + assertEquals(sha, parseAndroidUpdateSha256Asset("SHA256 (ompweb-1.0.1.apk) = ${"a".repeat(64)}")) + assertNull(parseAndroidUpdateSha256Asset("no digest here")) + } + + @Test + fun `sha256Hex hashes a stream`() { + val digest = sha256Hex("abc".byteInputStream()) + + assertEquals( + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + digest, + ) + } + + private companion object { + const val PACKAGE = "io.github.kahme247.ompweb" + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/AndroidUpdateTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/AndroidUpdateTest.kt new file mode 100644 index 000000000..f07983040 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/AndroidUpdateTest.kt @@ -0,0 +1,362 @@ +package io.github.kahme247.ompweb.domain + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * The app's own update check: tag filtering, numeric version comparison, + * draft/prerelease exclusion, the custom feed format, per-version dismiss and + * the six-hour cache. + */ +class AndroidUpdateTest { + + private fun release( + version: String, + url: String = "https://github.com/kahme247/ompweb/releases/tag/android-v$version", + prerelease: Boolean = false, + draft: Boolean = false, + ) = AndroidRelease( + version = version, + htmlUrl = url, + prerelease = prerelease, + draft = draft, + ) + + private fun candidate( + version: String, + versionCode: Long? = null, + pageUrl: String? = "https://github.com/kahme247/ompweb/releases/tag/android-v$version", + withApk: Boolean = false, + ) = AndroidUpdateCandidate( + versionName = version, + versionCode = versionCode, + pageUrl = pageUrl, + apkUrl = if (withApk) { + "https://github.com/kahme247/ompweb/releases/download/android-v$version/ompweb-$version.apk" + } else { + null + }, + apkSize = if (withApk) 5_610_321L else null, + sha256 = if (withApk) "a".repeat(64) else null, + ) + + @Test + fun `only android-v tags are app releases`() { + assertEquals("1.2.3", androidVersionFromTag("android-v1.2.3")) + assertNull(androidVersionFromTag("v0.5.2")) + assertNull(androidVersionFromTag("omp-v18.12.6")) + assertNull(androidVersionFromTag("android-v")) + assertNull(androidVersionFromTag("android-1.2.3")) + } + + @Test + fun `versions compare numerically, not as text`() { + assertTrue(compareAndroidVersions("1.0.10", "1.0.9") > 0) + assertTrue(compareAndroidVersions("1.10.0", "1.9.9") > 0) + assertTrue(compareAndroidVersions("2.0.0", "10.0.0") < 0) + assertEquals(0, compareAndroidVersions("1.1", "1.1.0")) + assertEquals(0, compareAndroidVersions("v1.1.0", "1.1.0")) + } + + @Test + fun `a debug suffix is a build label, not a newer version`() { + assertEquals("1.0.0", normalizeInstalledVersion("1.0.0-debug")) + assertEquals("1.0.0", normalizeInstalledVersion("1.0.0+20261023")) + assertEquals(0, compareAndroidVersions("1.0.0-debug", "1.0.0")) + assertTrue(isParsableAndroidVersion("1.2.3")) + assertFalse(isParsableAndroidVersion("nightly")) + } + + @Test + fun `a prerelease sorts below its release`() { + assertTrue(compareAndroidVersions("1.1.0-beta.1", "1.1.0") < 0) + assertTrue(compareAndroidVersions("1.1.0", "1.1.0-beta.1") > 0) + assertTrue(compareAndroidVersions("1.1.0-beta.2", "1.1.0-beta.1") > 0) + } + + @Test + fun `an unparsable version never fakes an update`() { + assertEquals(0, compareAndroidVersions("nightly", "1.0.0")) + assertNull(newestAndroidRelease(listOf(release("1.1.0")), "nightly")) + } + + @Test + fun `the newest published release wins and older ones are ignored`() { + val latest = newestAndroidRelease( + listOf(release("1.0.1"), release("1.2.0"), release("1.1.5")), + "1.0.0", + )!! + + assertEquals("1.2.0", latest.version) + } + + @Test + fun `only this repository's release pages and assets are accepted`() { + assertTrue( + isAndroidReleasePage("https://github.com/kahme247/ompweb/releases/tag/android-v1.1.0"), + ) + assertFalse( + isAndroidReleasePage("https://evil.example/kahme247/ompweb/releases/tag/android-v1.1.0"), + ) + assertFalse( + isAndroidReleasePage("https://github.com/kahme247/ompweb.evil/releases/tag/android-v1.1.0"), + ) + assertFalse( + isAndroidReleasePage("https://github.com/other/ompweb/releases/tag/android-v1.1.0"), + ) + assertFalse( + isAndroidReleasePage("https://github.com@evil.example/kahme247/ompweb/releases/x"), + ) + assertFalse( + isAndroidReleasePage("http://github.com/kahme247/ompweb/releases/tag/android-v1.1.0"), + ) + // `java.net.URI#getHost` is null for authorities it cannot parse as a + // server (underscore here): the guard must answer false, not throw. + assertFalse( + isAndroidReleasePage("https://foo_bar/kahme247/ompweb/releases/tag/android-v1.1.0"), + ) + + assertTrue( + isAndroidReleaseAsset( + "https://github.com/kahme247/ompweb/releases/download/android-v1.1.0/ompweb-1.1.0.apk", + ), + ) + assertFalse( + isAndroidReleaseAsset( + "https://evil.example/kahme247/ompweb/releases/download/android-v1.1.0/ompweb-1.1.0.apk", + ), + ) + assertFalse( + isAndroidReleaseAsset( + "http://github.com/kahme247/ompweb/releases/download/android-v1.1.0/ompweb-1.1.0.apk", + ), + ) + } + + @Test + fun `drafts, prereleases, insecure pages and current versions are not updates`() { + val releases = listOf( + release("2.0.0", draft = true), + release("1.9.0", prerelease = true), + release("1.8.0", url = "http://github.com/kahme247/ompweb/releases/tag/android-v1.8.0"), + release("1.7.0", url = "https://evil.example/kahme247/ompweb/releases/tag/android-v1.7.0"), + release("1.0.0"), + release("0.9.0"), + ) + + assertNull(newestAndroidRelease(releases, "1.0.0")) + } + + @Test + fun `a newer GitHub version becomes the notice`() { + val notice = androidUpdateNotice( + candidate("1.1.0"), + currentVersion = "1.0.0-debug", + currentVersionCode = 10000, + dismissedVersion = null, + )!! + + assertEquals("1.0.0", notice.currentVersion) + assertEquals("1.1.0", notice.availableVersion) + assertTrue(notice.releaseUrl.orEmpty().startsWith("https://github.com/")) + assertFalse(notice.installable) + } + + @Test + fun `a custom feed is judged by its version code`() { + val newer = androidUpdateNotice( + candidate("1.0.1", versionCode = 10001, pageUrl = null, withApk = true), + currentVersion = "1.0.1", + currentVersionCode = 10000, + dismissedVersion = null, + )!! + + assertEquals("1.0.1", newer.availableVersion) + assertNull(newer.releaseUrl) + assertTrue(newer.installable) + + // Same versionName, higher code: still an update. + assertTrue( + isAndroidUpdateCandidateNewer( + candidate("1.0.0", versionCode = 10001), + "1.0.0", + 10000, + ), + ) + // Same code: no update, even with a higher name. + assertNull( + androidUpdateNotice( + candidate("9.9.9", versionCode = 10000, pageUrl = null), + currentVersion = "1.0.0", + currentVersionCode = 10000, + dismissedVersion = null, + ), + ) + } + + @Test + fun `dismissing hides that version only`() { + assertNull( + androidUpdateNotice(candidate("1.1.0"), "1.0.0", 10000, dismissedVersion = "1.1.0"), + ) + + // A newer release is news again. + assertEquals( + "1.2.0", + androidUpdateNotice(candidate("1.2.0"), "1.0.0", 10000, dismissedVersion = "1.1.0")!! + .availableVersion, + ) + + // A stale dismissal of an older version must not hide the current one. + assertEquals( + "1.1.0", + androidUpdateNotice(candidate("1.1.0"), "1.0.0", 10000, dismissedVersion = "1.0.5")!! + .availableVersion, + ) + } + + @Test + fun `no notice for a missing candidate or a foreign page`() { + assertNull(androidUpdateNotice(null, "1.0.0", 10000, null)) + val foreignPage = androidUpdateNotice( + candidate("1.1.0", pageUrl = "https://evil.example/kahme247/ompweb/releases/tag/android-v1.1.0"), + "1.0.0", + 10000, + null, + ) + assertNull(foreignPage?.releaseUrl) + } + + @Test + fun `only a plausible APK is installable`() { + val installable = candidate("1.1.0", versionCode = 10001, pageUrl = null, withApk = true) + assertTrue(installable.canInstall) + + assertFalse(installable.copy(apkUrl = null).canInstall) + assertFalse(installable.copy(apkSize = 0).canInstall) + assertFalse(installable.copy(apkSize = MAX_ANDROID_UPDATE_BYTES + 1).canInstall) + assertFalse(installable.copy(sha256 = null).canInstall) + assertFalse( + installable.copy( + apkUrl = "http://example.com/ompweb.apk", + ).canInstall, + ) + assertTrue( + installable.copy( + apkUrl = "http://192.168.1.5:30180/ompweb-1.0.1.apk", + ).canInstall, + ) + } + + @Test + fun `a custom feed resolves its apk url and normalizes the digest`() { + val body = feedBody(sha = "ABCDEF".repeat(10) + "ABCD", apkUrl = "ompweb-1.0.1.apk") + + val parsed = parseAndroidUpdateFeed(body, FEED_URL) + + assertTrue(parsed is AndroidUpdateFeedResult.Parsed) + val feed = (parsed as AndroidUpdateFeedResult.Parsed).feed + assertEquals("1.0.1", feed.versionName) + assertEquals(10001L, feed.versionCode) + assertEquals("https://updates.example.com/ompweb/ompweb-1.0.1.apk", feed.apkUrl) + assertEquals("abcdef".repeat(10) + "abcd", feed.sha256) + assertEquals(5_610_321L, feed.size) + assertEquals("Fixes chat scrolling.", feed.notes) + } + + @Test + fun `the feed may name an absolute APK url inside a private network`() { + val body = feedBody(apkUrl = "http://192.168.1.5:30180/ompweb-1.0.1.apk") + + val feed = (parseAndroidUpdateFeed(body, FEED_URL) as AndroidUpdateFeedResult.Parsed).feed + + assertEquals("http://192.168.1.5:30180/ompweb-1.0.1.apk", feed.apkUrl) + } + + @Test + fun `a feed with a public plain-http apk url is refused`() { + val body = feedBody(apkUrl = "http://example.com/ompweb-1.0.1.apk") + + assertTrue(parseAndroidUpdateFeed(body, FEED_URL) is AndroidUpdateFeedResult.Invalid) + } + + @Test + fun `a feed with a bad digest, size or version is refused`() { + assertTrue(parseAndroidUpdateFeed(feedBody(sha = "abc"), FEED_URL) is AndroidUpdateFeedResult.Invalid) + assertTrue(parseAndroidUpdateFeed(feedBody(sha = "g".repeat(64)), FEED_URL) is AndroidUpdateFeedResult.Invalid) + assertTrue(parseAndroidUpdateFeed(feedBody(size = 0), FEED_URL) is AndroidUpdateFeedResult.Invalid) + assertTrue( + parseAndroidUpdateFeed( + feedBody(size = MAX_ANDROID_UPDATE_BYTES + 1), + FEED_URL, + ) is AndroidUpdateFeedResult.Invalid, + ) + assertTrue(parseAndroidUpdateFeed(feedBody(version = "nightly"), FEED_URL) is AndroidUpdateFeedResult.Invalid) + assertTrue(parseAndroidUpdateFeed(feedBody(versionCode = 0), FEED_URL) is AndroidUpdateFeedResult.Invalid) + assertTrue(parseAndroidUpdateFeed("not json", FEED_URL) is AndroidUpdateFeedResult.Invalid) + assertTrue( + parseAndroidUpdateFeed( + """{"versionName":"1.0.1","versionCode":10001,"sha256":"${"a".repeat(64)}","size":1}""", + FEED_URL, + ) is AndroidUpdateFeedResult.Invalid, + ) + } + + @Test + fun `feed urls follow the server's plain-http policy`() { + assertTrue(parseAndroidUpdateFeedUrl("https://example.com/latest.json") is AndroidUpdateFeedUrlResult.Valid) + assertTrue( + parseAndroidUpdateFeedUrl("http://100.64.0.10:30180/latest.json") is + AndroidUpdateFeedUrlResult.Valid, + ) + assertTrue( + parseAndroidUpdateFeedUrl("http://my-pc.tailnet.ts.net/latest.json") is + AndroidUpdateFeedUrlResult.Valid, + ) + assertTrue(parseAndroidUpdateFeedUrl("http://example.com/latest.json") is AndroidUpdateFeedUrlResult.Invalid) + assertTrue(parseAndroidUpdateFeedUrl("example.com/latest.json") is AndroidUpdateFeedUrlResult.Invalid) + assertTrue(parseAndroidUpdateFeedUrl("https://user:secret@example.com/latest.json") is AndroidUpdateFeedUrlResult.Invalid) + assertTrue(parseAndroidUpdateFeedUrl("") is AndroidUpdateFeedUrlResult.Invalid) + } + + @Test + fun `the cache lasts six hours and remembers its source`() { + val checkedAt = 1_000_000L + val cache = AndroidUpdateCache( + checkedAtMillis = checkedAt, + sourceKey = "https://updates.example.com/latest.json", + candidate = candidate("1.1.0"), + ) + + assertTrue(isAndroidUpdateCacheFresh(cache, checkedAt)) + assertTrue(isAndroidUpdateCacheFresh(cache, checkedAt + ANDROID_UPDATE_CACHE_MILLIS - 1)) + assertFalse(isAndroidUpdateCacheFresh(cache, checkedAt + ANDROID_UPDATE_CACHE_MILLIS)) + // A clock that jumped backwards must not pin a cache forever. + assertFalse(isAndroidUpdateCacheFresh(cache, checkedAt - 1)) + assertEquals("https://updates.example.com/latest.json", cache.sourceKey) + } + + private fun feedBody( + version: String = "1.0.1", + versionCode: Long = 10001, + apkUrl: String = "https://updates.example.com/ompweb/ompweb-1.0.1.apk", + sha: String = "a".repeat(64), + size: Long = 5_610_321, + ) = """ + { + "versionName": "$version", + "versionCode": $versionCode, + "apkUrl": "$apkUrl", + "sha256": "$sha", + "size": $size, + "notes": "Fixes chat scrolling." + } + """.trimIndent() + + private companion object { + const val FEED_URL = "https://updates.example.com/ompweb/latest.json" + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/BtwTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/BtwTest.kt new file mode 100644 index 000000000..b2c175ffc --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/BtwTest.kt @@ -0,0 +1,248 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Test + +/** + * Port of `lib/btw.test.mjs`: the pure state transitions of `/btw` records fed + * by frames, responses and history snapshots that can arrive in any order. + */ +class BtwTest { + + private fun recordJson( + id: String = "r1", + question: String = "what is 2+2", + answer: String = "", + status: BtwStatus = BtwStatus.RUNNING, + createdAt: Long = 1, + updatedAt: Long = 1, + followUps: List? = null, + ): JsonObject = buildJsonObject { + put("id", id) + put("leafId", JsonPrimitive("leaf-1")) + put("question", question) + put("answer", answer) + put("status", status.wire) + put("createdAt", createdAt) + put("updatedAt", updatedAt) + if (followUps != null) put("followUps", kotlinx.serialization.json.JsonArray(followUps)) + } + + private fun record( + id: String = "r1", + question: String = "what is 2+2", + answer: String = "", + status: BtwStatus = BtwStatus.RUNNING, + createdAt: Long = 1, + updatedAt: Long = 1, + followUps: List = emptyList(), + ): BtwRecord = BtwRecord( + id = id, + leafId = "leaf-1", + question = question, + answer = answer, + status = status, + createdAt = createdAt, + updatedAt = updatedAt, + followUps = followUps, + ) + + private fun delta(text: String, recordId: String = "r1"): JsonObject = buildJsonObject { + put("type", "btw_delta") + put("recordId", recordId) + put("delta", text) + } + + private fun recordFrame(record: JsonObject): JsonObject = buildJsonObject { + put("type", "btw_record") + put("record", record) + } + + @Test + fun `deltas stream into a running record and stop once it settles`() { + var records = applyBtwEvent(emptyList(), recordFrame(recordJson())) + records = applyBtwEvent(records, delta("It is ")) + records = applyBtwEvent(records, delta("4.")) + assertEquals("It is 4.", records[0].answer) + + records = applyBtwEvent( + records, + recordFrame(recordJson(answer = "It is 4.", status = BtwStatus.COMPLETE, updatedAt = 2)), + ) + records = applyBtwEvent(records, delta(" late")) + assertEquals("It is 4.", records[0].answer) + assertEquals(BtwStatus.COMPLETE, records[0].status) + } + + @Test + fun `a late btw response never wipes streamed text or resurrects a finished answer`() { + // SSE can beat the HTTP response: deltas land before the `btw` command's running snapshot. + var records = applyBtwEvent(emptyList(), recordFrame(recordJson())) + records = applyBtwEvent(records, delta("It is 4.")) + records = upsertBtwRecord(records, record()) + assertEquals("It is 4.", records[0].answer) + + records = applyBtwEvent( + records, + recordFrame(recordJson(answer = "It is 4.", status = BtwStatus.COMPLETE)), + ) + records = upsertBtwRecord(records, record(answer = "It is")) + assertEquals(BtwStatus.COMPLETE, records[0].status) + assertEquals("It is 4.", records[0].answer) + } + + @Test + fun `follow-up deltas extend the latest turn, not the original answer`() { + val first = record(answer = "4", status = BtwStatus.COMPLETE) + val followUp = BtwTurn( + question = "and 3+3?", + answer = "", + status = BtwStatus.RUNNING, + createdAt = 3, + updatedAt = 3, + ) + var records = upsertBtwRecord(listOf(first), first.copy(followUps = listOf(followUp))) + records = applyBtwEvent(records, delta("6")) + assertEquals("4", records[0].answer) + assertEquals("6", latestBtwTurn(records[0]).answer) + // A snapshot from before the follow-up started is stale. + assertSame(records, upsertBtwRecord(records, first)) + } + + @Test + fun `a new topic goes first, ahead of older ones`() { + var records = applyBtwEvent( + emptyList(), + recordFrame(recordJson(status = BtwStatus.COMPLETE)), + ) + records = applyBtwEvent( + records, + recordFrame(recordJson(id = "r2", question = "next", createdAt = 5, updatedAt = 5)), + ) + assertEquals(listOf("r2", "r1"), records.map { it.id }) + } + + @Test + fun `an authoritative snapshot heals streamed text that diverged from omps`() { + // Deltas applied twice (e.g. replayed after a snapshot that already held them). + val records = listOf(record(answer = "abcdcd")) + assertEquals("abcde", upsertBtwRecord(records, record(answer = "abcde"))[0].answer) + assertEquals( + "abcdcd!", + upsertBtwRecord(records, record(answer = "abcdcd!", status = BtwStatus.COMPLETE))[0].answer, + ) + } + + @Test + fun `malformed frames leave the records untouched`() { + val records = listOf(record(answer = "4")) + val withoutUpdatedAt = buildJsonObject { + put("id", "r9") + put("question", "q") + put("answer", "") + put("status", "running") + put("createdAt", 1) + } + val frames = listOf( + recordFrame(withoutUpdatedAt), + recordFrame(recordJson(id = "r9", status = BtwStatus.RUNNING).withStatus("finished")), + recordFrame( + buildJsonObject { + put("id", "r9") + put("question", "q") + put("answer", "") + put("status", "running") + put("createdAt", 1) + put("updatedAt", 1) + put("followUps", kotlinx.serialization.json.JsonArray(listOf(buildJsonObject { put("question", "q") }))) + }, + ), + // Finite but outside the Date range: the history dialog would throw formatting it. + recordFrame(recordJson(id = "r9", updatedAt = 10_000_000_000_000_000L)), + recordFrame(recordJson(id = "r9", createdAt = -1)), + buildJsonObject { put("type", "btw_record") }, + buildJsonObject { + put("type", "btw_delta") + put("recordId", "r1") + put("delta", 4) + }, + buildJsonObject { + put("type", "btw_delta") + put("delta", "x") + }, + delta("x", recordId = "unknown"), + ) + for (frame in frames) { + assertSame(frame.toString(), records, applyBtwEvent(records, frame)) + } + } + + @Test + fun `history snapshots keep newer local state and merge newest first`() { + val streaming = record(answer = "It is 4", createdAt = 2) + val local = listOf( + record(id = "r2", question = "new", createdAt = 3, updatedAt = 3), + streaming, + ) + val merged = mergeBtwHistory( + local, + listOf( + record(answer = "It", createdAt = 2), + record(id = "r0", status = BtwStatus.COMPLETE, answer = "old", createdAt = 9), + ), + setOf("r1"), + ) + assertEquals(listOf("r0", "r2", "r1"), merged.map { it.id }) + assertEquals("It is 4", merged.first { it.id == "r1" }.answer) + assertEquals( + "started after the request: the snapshot predates it", + BtwStatus.RUNNING, + merged.first { it.id == "r2" }.status, + ) + } + + @Test + fun `a running record omp no longer knows becomes interrupted, not a spinner forever`() { + val followUp = BtwTurn( + question = "and 3+3?", + answer = "6", + status = BtwStatus.RUNNING, + createdAt = 3, + updatedAt = 3, + ) + val local = listOf( + record(answer = "4", status = BtwStatus.COMPLETE, followUps = listOf(followUp)), + record(id = "r0", status = BtwStatus.COMPLETE, createdAt = 0), + ) + val merged = mergeBtwHistory(local, emptyList(), setOf("r1", "r0")) + assertEquals(BtwStatus.INTERRUPTED, latestBtwTurn(merged[0]).status) + assertEquals("earlier turns keep their status", BtwStatus.COMPLETE, merged[0].status) + assertEquals("the partial answer stays readable", "6", latestBtwTurn(merged[0]).answer) + assertSame("finished records the snapshot lacks are kept as they are", local[1], merged[1]) + } + + private fun JsonObject.withStatus(status: String): JsonObject { + val source = this + return buildJsonObject { + for ((key, value) in source) { + if (key != "status") put(key, value) + } + put("status", status) + } + } +} + +/** The wire spelling of [BtwStatus] (omp's `btw_record.status`). */ +private val BtwStatus.wire: String + get() = when (this) { + BtwStatus.RUNNING -> "running" + BtwStatus.COMPLETE -> "complete" + BtwStatus.CANCELLED -> "cancelled" + BtwStatus.ERROR -> "error" + BtwStatus.INTERRUPTED -> "interrupted" + } diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/ColdStartRestoreTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ColdStartRestoreTest.kt new file mode 100644 index 000000000..f10999eb3 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ColdStartRestoreTest.kt @@ -0,0 +1,129 @@ +package io.github.kahme247.ompweb.domain + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * The cold-start restore decision behind `SessionsScreen`'s effect. The live + * Phase 11 run broke it in two ways: an effect that ran before `GET + * /api/sessions` answered consumed the marker without a hit, so a slow list + * never restored, and a dead deep-link id stayed the marker forever. + */ +class ColdStartRestoreTest { + + @Test + fun `the marker survives an effect that runs before the list is loaded`() { + val step = decide(listLoaded = false, sessionIds = emptyList(), lastSessionId = "s1") + + assertEquals(ColdStartRestoreStep.Wait, step) + assertFalse("the one attempt must not be burned before the list answered", step.consumes) + } + + @Test + fun `an empty list does not decide that the saved session is gone`() { + // This is the race from the live run: `loading` was already false but + // the session list was still empty, so the old effect dropped `pending` + // and never retried. + val step = decide(listLoaded = true, sessionIds = emptyList(), lastSessionId = "s1") + + assertEquals(ColdStartRestoreStep.Wait, step) + assertFalse(step.consumes) + } + + @Test + fun `the saved session opens once the loaded list contains it`() { + val step = decide(listLoaded = true, sessionIds = listOf("a", "s1"), lastSessionId = "s1") + + assertEquals(ColdStartRestoreStep.OpenSession("s1"), step) + assertTrue(step.consumes) + } + + @Test + fun `a saved session the loaded list does not know is forgotten`() { + val step = decide(listLoaded = true, sessionIds = listOf("a", "b"), lastSessionId = "dead") + + assertEquals(ColdStartRestoreStep.ForgetSession("dead"), step) + assertTrue(step.consumes) + } + + @Test + fun `a re-run after the restore opens nothing a second time`() { + val restore = decide(listLoaded = true, sessionIds = listOf("s1"), lastSessionId = "s1") + assertEquals(ColdStartRestoreStep.OpenSession("s1"), restore) + + // Rotation: the effect runs again over the same persisted marker. The + // first run already settled the process-level flag… + val afterRestore = decide( + pending = !restore.consumes, + listLoaded = true, + sessionIds = listOf("s1"), + selectedSessionId = "s1", + lastSessionId = "s1", + ) + assertEquals(ColdStartRestoreStep.Done, afterRestore) + + // …and even a composition that runs before the flag write settles must + // not navigate twice, because something is already on screen. + val whileShown = decide( + pending = true, + listLoaded = true, + sessionIds = listOf("s1"), + selectedSessionId = "s1", + lastSessionId = "s1", + ) + assertEquals(ColdStartRestoreStep.Done, whileShown) + } + + @Test + fun `a composed pane consumes the marker before the list loads`() { + // The new-session pane is restored from its own marker; a re-run while + // it is open must not validate and open it twice. + val step = decide( + listLoaded = false, + sessionIds = emptyList(), + newSessionCwd = "/work/pane", + lastNewSessionCwd = "/work/pane", + ) + + assertEquals(ColdStartRestoreStep.Done, step) + assertTrue(step.consumes) + } + + @Test + fun `the new-session marker reopens its pane`() { + val step = decide( + listLoaded = true, + sessionIds = emptyList(), + lastNewSessionCwd = "/work/pane", + ) + + assertEquals(ColdStartRestoreStep.OpenNewSession("/work/pane"), step) + assertTrue(step.consumes) + } + + @Test + fun `without a marker there is nothing to restore`() { + assertEquals(ColdStartRestoreStep.Done, decide(listLoaded = true, sessionIds = emptyList())) + assertTrue(decide(listLoaded = true, sessionIds = emptyList()).consumes) + } + + private fun decide( + pending: Boolean = true, + listLoaded: Boolean, + sessionIds: List, + selectedSessionId: String? = null, + newSessionCwd: String? = null, + lastSessionId: String? = null, + lastNewSessionCwd: String? = null, + ) = coldStartRestoreStep( + pending = pending, + listLoaded = listLoaded, + sessionIds = sessionIds, + selectedSessionId = selectedSessionId, + newSessionCwd = newSessionCwd, + lastSessionId = lastSessionId, + lastNewSessionCwd = lastNewSessionCwd, + ) +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/ComposerLogicTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ComposerLogicTest.kt new file mode 100644 index 000000000..31269ae8c --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ComposerLogicTest.kt @@ -0,0 +1,196 @@ +package io.github.kahme247.ompweb.domain + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** Pure composer rules shared by the chat and new-session composers. */ +class ComposerLogicTest { + + // ------------------------------------------------ submit during a run + + @Test + fun `the filled button submits with the configured mode while a run is live`() { + assertEquals( + "Steer the current run reaches the button a phone actually has", + SubmitDuringRunBehavior.STEER, + streamingSubmitBehavior(hasContent = true, SubmitDuringRunBehavior.STEER), + ) + assertEquals( + SubmitDuringRunBehavior.QUEUE, + streamingSubmitBehavior(hasContent = true, SubmitDuringRunBehavior.QUEUE), + ) + } + + @Test + fun `an empty composer keeps the primary action on Stop`() { + assertNull(streamingSubmitBehavior(hasContent = false, SubmitDuringRunBehavior.STEER)) + assertNull(streamingSubmitBehavior(hasContent = false, SubmitDuringRunBehavior.QUEUE)) + } + + // ------------------------------------------------------------ bash mode + + @Test + fun `single bang includes the context and double bang excludes it`() { + assertEquals(BashInvocation("ls -la", false), parseBashInvocation("!ls -la", false)) + assertEquals(BashInvocation("ls -la", true), parseBashInvocation("!!ls -la", false)) + } + + @Test + fun `an empty command or any attachment disables shell mode`() { + assertNull(parseBashInvocation("! ", false)) + assertNull(parseBashInvocation("!ls", true)) + assertNull(parseBashInvocation("plain text", false)) + } + + // -------------------------------------------------------- caret editing + + @Test + fun `insertAtCursor clamps the caret and returns the new position`() { + assertEquals(TextInsertion("hello world", 6), insertAtCursor("world", 0, "hello ")) + assertEquals(TextInsertion("abcXYZ", 6), insertAtCursor("abc", 99, "XYZ")) + assertEquals(TextInsertion("aXYb", 3), insertAtCursor("ab", 1, "XY")) + } + + // ------------------------------------------------------------- history + + @Test + fun `input history keeps first occurrences oldest to newest and caps the tail`() { + assertEquals(listOf("a", "c", "b"), inputHistory(listOf("a", "b", "a", "c", "b"))) + assertEquals(listOf("2", "3"), inputHistory(listOf("1", "2", "3"), limit = 2)) + assertTrue(inputHistory(listOf("", " ")).isEmpty()) + } + + // ---------------------------------------------------------------- goals + + @Test + fun `an active goal round-trips through its stored JSON`() { + val goal = createActiveGoal(" ship the export ", 1_700_000_000_000) + assertEquals("ship the export", goal.objective) + assertEquals(goal, parseActiveGoal(encodeActiveGoal(goal))) + assertNull(parseActiveGoal("not json")) + assertNull(parseActiveGoal("")) + } + + @Test + fun `elapsed goal time reads as minutes, hours and days`() { + assertEquals("0m", formatGoalElapsed(30_000)) + assertEquals("5m", formatGoalElapsed(5 * 60_000L)) + assertEquals("2h 3m", formatGoalElapsed((2 * 3_600_000L) + (3 * 60_000L))) + assertEquals("25h 0m", formatGoalElapsed((25 * 3_600_000L))) + } + + // ------------------------------------------------------ slash expansion + + @Test + fun `web commands expand their argument into the prompt`() { + val expansion = expandWebSlashCommand("/goal ship the export") + assertTrue(expansion is WebSlashExpansion.Expand) + assertTrue((expansion as WebSlashExpansion.Expand).prompt.contains("ship the export")) + assertTrue(expansion.prompt.startsWith("Work toward this goal")) + } + + @Test + fun `commands that need arguments report usage instead of sending`() { + val expansion = expandWebSlashCommand("/fix") + assertEquals( + WebSlashExpansion.UsageError("/fix", "composer_cmd_fix_arg"), + expansion, + ) + } + + @Test + fun `optional-argument commands expand without them`() { + val expansion = expandWebSlashCommand("/review") + assertTrue(expansion is WebSlashExpansion.Expand) + assertTrue((expansion as WebSlashExpansion.Expand).prompt.contains("current project state")) + } + + @Test + fun `slashes omp owns are not client-expanded`() { + assertEquals(WebSlashExpansion.NotWeb, expandWebSlashCommand("/compact")) + assertEquals(WebSlashExpansion.NotWeb, expandWebSlashCommand("plain")) + assertTrue(getWebSlashCommand("goal") != null) + assertNull(getWebSlashCommand("compact")) + } + + @Test + fun `slash lookup only fires right after a slash at the start or after whitespace`() { + assertEquals(SlashQuery(SlashMode.LEADING, 0, "go"), extractSlashQuery("/go")) + assertEquals(SlashQuery(SlashMode.MID_PROMPT, 5, "go"), extractSlashQuery("text /go")) + assertNull(extractSlashQuery("/go ")) + assertNull(extractSlashQuery("https://example.com/a")) + } + + // -------------------------------------------------------------- at-menu + + @Test + fun `at lookup captures quoted paths and closes after whitespace`() { + assertEquals(AtQuery(0, "src/ma", false), extractAtQuery("@src/ma")) + assertEquals(AtQuery(3, "my file", true), extractAtQuery("hi @\"my file")) + assertNull(extractAtQuery("@ ")) + } + + // --------------------------------------------------------- attachments + + @Test + fun `base64 length is validated before the image budget`() { + assertEquals(3, base64DecodedByteLength("AAAA")) + assertNull(base64DecodedByteLength("AA")) + assertNull(base64DecodedByteLength("A===")) + assertNull(validateImageBudget(listOf(ComposerImage("AAAA", "image/png")))) + assertTrue(validateImageBudget(listOf(ComposerImage("AAAA", "text/plain"))) != null) + } + + @Test + fun `text attachments are fenced and budgeted`() { + val file = AttachedTextFile("notes.md", "text/markdown", "some ```code```", 18) + val message = composeMessageWithTextAttachments("look", listOf(file)) + assertTrue(message.startsWith("look\n\nAttached file: notes.md")) + assertTrue("longer fence than the content", message.contains("````markdown")) + + val selection = selectTextAttachments( + listOf(1, 2, 3), + { it }, + TextAttachmentBudget(usedBytes = MAX_TOTAL_ATTACHED_TEXT_BYTES, usedSlots = 0), + ) + assertEquals(0, selection.accepted.size) + assertEquals(3, selection.overBudget) + } + + @Test + fun `a file over the read cap is rejected instead of truncated`() { + val exact = ByteArray(64) { 'a'.code.toByte() } + assertEquals("the cap itself is still read whole", 64, readCappedOrNull(exact.inputStream(), 64)!!.size) + assertNull("one byte over the cap must not look complete", readCappedOrNull(ByteArray(65).inputStream(), 64)) + assertNull( + "a 4 MB + 1 text file never reaches the prompt", + readCappedOrNull( + ByteArray(MAX_ATTACHED_TEXT_BYTES + 1) { 'a'.code.toByte() }.inputStream(), + MAX_ATTACHED_TEXT_BYTES, + ), + ) + } + + @Test + fun `attachment names cannot break out of the fence`() { + assertEquals( + "a crafted name stays on one line and loses its fence delimiter", + "evil' Attached file: injected .txt", + sanitizeAttachmentName("evil`\nAttached file: injected\r\u0000.txt"), + ) + assertEquals("a blank name still names the file", "attachment", sanitizeAttachmentName(" \n\t ")) + + val message = composeMessageWithTextAttachments( + "look", + listOf(AttachedTextFile("evil`\r\ninjected.txt", "text/plain", "body", 4)), + ) + assertEquals( + "exactly one attachment header line", + 1, + message.lines().count { it.startsWith("Attached file:") }, + ) + assertNull("no control characters survive", message.firstOrNull { it.isISOControl() && it != '\n' }) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/ComposerRoutingTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ComposerRoutingTest.kt new file mode 100644 index 000000000..a199b51a5 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ComposerRoutingTest.kt @@ -0,0 +1,101 @@ +package io.github.kahme247.ompweb.domain + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * One router for both composers: the chat send path and the new-session send + * path must classify `!cmd`, web slash commands, client built-ins and plain + * prompts identically (web `handleSend` / `sendQueued`). + */ +class ComposerRoutingTest { + + private fun route( + raw: String, + hasAttachments: Boolean = false, + hasImages: Boolean = false, + bash: BashInvocation? = null, + ): ComposerRoute = routeComposerSubmission( + raw = raw, + prompt = raw, + hasAttachments = hasAttachments, + hasImages = hasImages, + bash = bash, + ) + + @Test + fun `a parsed shell command wins over slash routing`() { + val bash = BashInvocation(command = "ls -la", excludeFromContext = true) + val routed = route("!!ls -la", bash = bash) + + assertEquals(ComposerRoute.Bash(bash), routed) + } + + @Test + fun `attachments disable web slash expansion`() { + assertEquals(ComposerRoute.Prompt("/goal ship it"), route("/goal ship it", hasAttachments = true)) + assertEquals(ComposerRoute.Prompt("/goal ship it"), route("/goal ship it", hasImages = true)) + } + + @Test + fun `a web command becomes its expanded prompt`() { + val routed = route("/goal ship the export") + assertTrue("expected a web prompt, got $routed", routed is ComposerRoute.WebPrompt) + routed as ComposerRoute.WebPrompt + assertEquals("goal", routed.name) + assertEquals("ship the export", routed.args) + assertTrue( + "the expansion keeps the argument: ${routed.prompt}", + routed.prompt.contains("ship the export"), + ) + assertTrue( + "the raw slash text never reaches omp: ${routed.prompt}", + routed.prompt.startsWith("Work toward this goal"), + ) + } + + @Test + fun `a missing required argument reports usage instead of sending`() { + val routed = route("/plan") + assertEquals(ComposerRoute.Usage("/plan", "composer_cmd_plan_arg"), routed) + } + + @Test + fun `a client builtin keeps its raw text and arguments`() { + assertEquals(ComposerRoute.Builtin("compact", ""), route("/compact")) + assertEquals(ComposerRoute.Builtin("name", "new title"), route("/name new title")) + } + + @Test + fun `everything else is a plain prompt`() { + assertEquals(ComposerRoute.Prompt("hello"), route("hello")) + assertEquals(ComposerRoute.Prompt("/skill:review"), route("/skill:review")) + assertEquals(ComposerRoute.Prompt("/"), route("/")) + } + + // ------------------------------------------------------------------ /loop + + @Test + fun `loop counts match the web for zero, leading zeros and huge numbers`() { + assertTrue("/loop 0 uses the default", attempts("/loop 0 ship it") == 3) + assertTrue("/loop 007 keeps seven", attempts("/loop 007 ship it") == 7) + assertTrue("/loop 4 keeps four", attempts("/loop 4 ship it") == 4) + assertTrue("a huge count is capped", attempts("/loop 99999999999999999999 ship it") == 10) + assertTrue("10 is the maximum", attempts("/loop 10 ship it") == 10) + assertTrue("no count means the default", attempts("/loop ship it") == 3) + } + + @Test + fun `a loop count never swallows the task line`() { + val prompt = (expandWebSlashCommand("/loop 2 fix the parser") as WebSlashExpansion.Expand).prompt + assertTrue(prompt.contains("up to 2 attempts in total")) + assertTrue("the task stays whole: $prompt", prompt.trimEnd().endsWith("fix the parser")) + } + + private fun attempts(command: String): Int { + val prompt = (expandWebSlashCommand(command) as WebSlashExpansion.Expand).prompt + val match = Regex("up to (\\d+) attempts").find(prompt) ?: error("no attempt count in $prompt") + return match.groupValues[1].toInt() + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/ContextUsageTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ContextUsageTest.kt new file mode 100644 index 000000000..81247891b --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ContextUsageTest.kt @@ -0,0 +1,144 @@ +package io.github.kahme247.ompweb.domain + +import java.time.ZoneId +import java.util.Locale +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNull +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +/** + * Mirrors the formatters `components/ComposerPanels.tsx` and + * `components/ChatInput.tsx` rely on (`lib/format.ts`, `lib/subagent-format.ts`, + * `formatUsageLimitLabel`, `readCompactResult`). + */ +class ContextUsageTest { + + private val utc = ZoneId.of("UTC") + + @Test + fun `percent keeps one decimal and the dot separator`() { + assertEquals("42.0%", formatPercent(42.0)) + assertEquals("7.5%", formatPercent(7.5)) + assertEquals("100.0%", formatPercent(100.0)) + } + + @Test + fun `compact token counts match the web formatter`() { + assertEquals("999", formatTokenCount(999)) + assertEquals("1k", formatTokenCount(1_234)) + assertEquals("2k", formatTokenCount(1_500)) + assertEquals("34k", formatTokenCount(34_000)) + assertEquals("1.0M", formatTokenCount(1_000_000)) + assertEquals("1.2M", formatTokenCount(1_200_000)) + } + + @Test + fun `cache hit rate is cacheRead over input plus cacheRead`() { + assertEquals(75.0, cacheHitRate(input = 100, cacheRead = 300)!!, 0.0001) + assertEquals(0.0, cacheHitRate(input = 500, cacheRead = 0)!!, 0.0001) + assertNull(cacheHitRate(input = 0, cacheRead = 0)) + } + + @Test + fun `cost prints four decimals under a cent and two above it`() { + assertNull(formatCost(null)) + assertNull(formatCost(0.0)) + assertNull(formatCost(-1.0)) + assertNull(formatCost(Double.NaN)) + assertEquals("\$0.0050", formatCost(0.005)) + assertEquals("\$1.20", formatCost(1.2)) + } + + @Test + fun `the ring turns warning past 70 and error past 90`() { + assertEquals(ContextTone.NORMAL, contextTone(null)) + assertEquals(ContextTone.NORMAL, contextTone(70.0)) + assertEquals(ContextTone.WARNING, contextTone(70.1)) + assertEquals(ContextTone.WARNING, contextTone(90.0)) + assertEquals(ContextTone.ERROR, contextTone(90.1)) + } + + @Test + fun `allowance percentages drop the fraction only when it is zero`() { + assertEquals("42", formatAllowancePercent(42.0)) + assertEquals("42.5", formatAllowancePercent(42.5)) + } + + @Test + fun `a reset in the past or a missing one has no clock`() { + val now = 1_000_000L + assertNull(usableSlowModeResetTime(null, now)) + // Exactly now counts as past: the label would show a clock that already happened. + assertNull(usableSlowModeResetTime(1_000L, now)) + assertNull(usableSlowModeResetTime(500L, now)) + assertEquals(2_000_000L, usableSlowModeResetTime(2_000L, now)) + } + + @Test + fun `reset clock is a time today, or weekday plus time when further out`() { + val now = java.time.Instant.parse("2026-01-01T12:00:00Z").toEpochMilli() + val soon = java.time.Instant.parse("2026-01-01T15:30:00Z").toEpochMilli() + val tomorrow = java.time.Instant.parse("2026-01-02T12:00:00Z").toEpochMilli() + assertEquals("15:30", formatSlowModeResetClock(soon, now, utc, Locale.US)) + assertEquals("Fri 12:00", formatSlowModeResetClock(tomorrow, now, utc, Locale.US)) + } + + @Test + fun `usage limit labels carry the reset instant and the low-priority arm's allowance`() { + val now = 1_000_000L + val lowPriority = UsageLimit( + stage = UsageLimit.Stage.LOW_PRIORITY, + resetsAtSec = 2_000L, + allowanceLeftPercent = 42.5, + ) + assertEquals( + UsageLimitLabel.LowPriority(resetAtMillis = 2_000_000L, allowancePercent = 42.5), + usageLimitLabel(lowPriority, now), + ) + // A stale reset is dropped rather than printed. + assertEquals( + UsageLimitLabel.LowPriority(resetAtMillis = null, allowancePercent = null), + usageLimitLabel(UsageLimit(UsageLimit.Stage.LOW_PRIORITY, resetsAtSec = 10L), now), + ) + assertEquals( + UsageLimitLabel.WrapUp(resetAtMillis = null, extraUsage = true), + usageLimitLabel(UsageLimit(UsageLimit.Stage.WRAP_UP, extraUsage = true), now), + ) + } + + @Test + fun `context usage parses tokens, window and percent`() { + val parsed = parseContextUsage( + Json.parseToJsonElement("""{"tokens":1200,"contextWindow":200000,"percent":0.6}"""), + ) + assertEquals(ContextUsageInfo(tokens = 1200, contextWindow = 200_000, percent = 0.6), parsed) + assertNull(parseContextUsage(null)) + assertNull(parseContextUsage(JsonNull)) + // Without a window there is nothing to draw. + assertNull(parseContextUsage(Json.parseToJsonElement("""{"tokens":12}"""))) + assertEquals( + ContextUsageInfo(tokens = null, contextWindow = 1_000, percent = null), + parseContextUsage(Json.parseToJsonElement("""{"contextWindow":1000}""")), + ) + } + + @Test + fun `compact results need tokensBefore and default the estimate`() { + val result = readCompactResult( + Json.parseToJsonElement("""{"tokensBefore":1000,"estimatedTokensAfter":250}"""), + "manual", + ) + assertEquals(CompactResult("manual", 1000, 250), result) + assertEquals(750, result!!.savedTokens) + assertEquals( + CompactResult("auto", 500, 0), + readCompactResult(Json.parseToJsonElement("""{"tokensBefore":500}"""), "auto"), + ) + assertNull(readCompactResult(Json.parseToJsonElement("""{"estimatedTokensAfter":10}"""), "auto")) + assertNull(readCompactResult(null, "auto")) + // A shrinking estimate never reports negative savings. + assertEquals(0, CompactResult("manual", 100, 200).savedTokens) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/DictationTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/DictationTest.kt new file mode 100644 index 000000000..6fb2e5215 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/DictationTest.kt @@ -0,0 +1,582 @@ +package io.github.kahme247.ompweb.domain + +import java.util.Locale +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Dictation ownership/claim/adopt/retry/timeout rules, mirroring + * `hooks/useDictation.ts` and the server contract in `lib/stt-jobs.ts`. + */ +class DictationTest { + + private var now = 1_000L + private val machine = DictationMachine { now } + + private fun transcribing(jobId: String = "job-1", after: SttAfter = SttAfter.Insert) { + machine.start(after) + machine.finishRecording() + machine.uploadAccepted(jobId) + } + + // --- capture ---------------------------------------------------------- + + @Test + fun `start resets a shown job and refuses a second capture`() { + transcribing() + machine.fail("network") + assertTrue(machine.start(SttAfter.Send)) + assertNull("the previous job is replaced", machine.jobId) + assertEquals(SttAfter.Send, machine.after) + assertEquals(DictationPhase.Recording, machine.phase) + assertFalse("a capture cannot start twice", machine.start(SttAfter.Insert)) + } + + @Test + fun `elapsed time excludes paused stretches and stops at five minutes`() { + assertTrue(machine.start(SttAfter.Insert)) + now += 2_000 + assertEquals(2_000L, machine.elapsedMs()) + + assertTrue(machine.pause()) + now += 5_000 + assertEquals("paused time does not count", 2_000L, machine.elapsedMs()) + + assertTrue(machine.resume()) + now += 3_000 + assertEquals(5_000L, machine.elapsedMs()) + + machine.finishRecording() + assertEquals(5_000L, machine.finalElapsedMs) + assertEquals("the finished length stays available", 5_000L, machine.elapsedMs()) + + val capped = DictationMachine { now } + capped.start(SttAfter.Insert) + now += DictationMachine.MAX_RECORDING_MS + 10_000 + assertEquals(DictationMachine.MAX_RECORDING_MS, capped.elapsedMs()) + } + + // --- follow / claim --------------------------------------------------- + + @Test + fun `a finished job is claimed once and then waits for the answer`() { + transcribing() + assertEquals(DictationStep.Claim, machine.observe(SttJob("job-1", SttJobStatus.Done, null))) + assertTrue(machine.awaitingClaim) + assertEquals( + "a repeated poll must not claim twice", + DictationStep.Wait, + machine.observe(SttJob("job-1", SttJobStatus.Done, null)), + ) + } + + @Test + fun `own transcript keeps the send intent`() { + transcribing(after = SttAfter.Steer) + machine.observe(SttJob("job-1", SttJobStatus.Done, null)) + val step = machine.takeClaim(SttClaim("job-1", SttJobStatus.Done, null, " hello ", SttAfter.Steer)) + assertEquals(DictationStep.Delivered("hello", SttAfter.Steer, isOwner = true), step) + assertEquals(DictationPhase.Idle, machine.phase) + assertNull(machine.jobId) + assertFalse(machine.hasLocalAudio) + } + + @Test + fun `a transcript another composer recorded is only inserted`() { + assertTrue(machine.adopt(listOf(SttJob("job-2", SttJobStatus.Done, null)))) + assertFalse(machine.mine) + assertEquals(SttAfter.Insert, machine.after) + machine.observe(SttJob("job-2", SttJobStatus.Done, null)) + val step = machine.takeClaim(SttClaim("job-2", SttJobStatus.Done, null, "from the other tab", null)) + assertEquals(DictationStep.Delivered("from the other tab", SttAfter.Insert, isOwner = false), step) + } + + @Test + fun `the owner grace asks again until the claim carries text`() { + transcribing() + machine.observe(SttJob("job-1", SttJobStatus.Done, null)) + assertEquals( + "done without text means: the recording browser still has the first claim", + DictationStep.Wait, + machine.takeClaim(SttClaim("job-1", SttJobStatus.Done, null, null, SttAfter.Send)), + ) + assertTrue("the claim repeats with the same token", machine.awaitingClaim) + val step = machine.takeClaim(SttClaim("job-1", SttJobStatus.Done, null, "text", SttAfter.Send)) + assertEquals(DictationStep.Delivered("text", SttAfter.Send, isOwner = true), step) + } + + @Test + fun `a claim on a pending job goes back to polling`() { + transcribing() + machine.observe(SttJob("job-1", SttJobStatus.Done, null)) + assertEquals( + DictationStep.Wait, + machine.takeClaim(SttClaim("job-1", SttJobStatus.Pending, null, null, null)), + ) + assertFalse(machine.awaitingClaim) + assertEquals(DictationStep.Wait, machine.observe(SttJob("job-1", SttJobStatus.Pending, null))) + } + + @Test + fun `another composer taking the job stands down without an error`() { + transcribing() + assertEquals(DictationStep.StandDown, machine.observe(SttJob("job-1", SttJobStatus.Gone, null))) + assertEquals(DictationPhase.Gone, machine.phase) + assertNull(machine.jobId) + + val follower = DictationMachine { now } + follower.adopt(listOf(SttJob("job-3", SttJobStatus.Pending, null))) + assertEquals( + DictationStep.StandDown, + follower.takeClaim(SttClaim("job-3", SttJobStatus.Gone, null, null, null)), + ) + } + + @Test + fun `a lost job fails only the composer that recorded it`() { + transcribing() + assertEquals(DictationStep.Failed(DictationMachine.JOB_NOT_FOUND), machine.observe(null)) + assertEquals(DictationPhase.Failed, machine.phase) + + val follower = DictationMachine { now } + follower.adopt(listOf(SttJob("job-4", SttJobStatus.Pending, null))) + assertEquals(DictationStep.StandDown, follower.observe(null)) + assertEquals(DictationPhase.Gone, follower.phase) + } + + @Test + fun `job errors surface their message`() { + transcribing() + assertEquals(DictationStep.Failed("upstream refused"), machine.observe(SttJob("job-1", SttJobStatus.Error, "upstream refused"))) + assertEquals("upstream refused", machine.message) + } + + // --- adoption --------------------------------------------------------- + + @Test + fun `the stop intent overrides the intent chosen at start`() { + assertTrue(machine.start(SttAfter.Insert)) + machine.finishRecording(SttAfter.FollowUp) + assertEquals("the primary button decides at stop time", SttAfter.FollowUp, machine.after) + assertEquals(DictationPhase.Uploading, machine.phase) + + machine.finishRecording(SttAfter.Send) + assertEquals("a finished capture cannot change its intent again", SttAfter.FollowUp, machine.after) + assertEquals(DictationPhase.Uploading, machine.phase) + } + + @Test + fun `a capture cannot start over an upload or a running job`() { + assertTrue(machine.start(SttAfter.Insert)) + machine.finishRecording() + assertFalse("uploading blocks a new capture", machine.start(SttAfter.Send)) + + machine.uploadAccepted("job-1") + assertFalse("transcribing blocks a new capture", machine.start(SttAfter.Send)) + assertEquals(DictationPhase.Transcribing, machine.phase) + } + + @Test + fun `the elapsed time freezes when the recording stops`() { + assertTrue(machine.start(SttAfter.Insert)) + now += 4_200 + machine.finishRecording() + now += 60_000 + assertEquals(4_200L, machine.finalElapsedMs) + assertEquals("the deck keeps showing the recorded length", 4_200L, machine.elapsedMs()) + } + + @Test + fun `adoption picks the newest live job and shows a failed one with its error`() { + assertFalse("nothing to adopt", machine.adopt(emptyList())) + assertFalse("gone jobs are not adopted", machine.adopt(listOf(SttJob("g", SttJobStatus.Gone, null)))) + + assertTrue(machine.adopt(listOf(SttJob("failed", SttJobStatus.Error, "boom"), SttJob("newer", SttJobStatus.Done, null)))) + assertEquals("the newest live job wins", "failed", machine.jobId) + assertEquals(DictationPhase.Failed, machine.phase) + assertEquals("boom", machine.message) + assertTrue("the server kept the audio for a retry", machine.canRetry) + assertEquals("an error is not being followed", DictationPhase.Failed, machine.phase) + } + + @Test + fun `nothing is adopted while this composer is busy`() { + transcribing() + assertFalse(machine.canAdopt()) + assertFalse(machine.adopt(listOf(SttJob("other", SttJobStatus.Done, null)))) + assertEquals("job-1", machine.jobId) + } + + // --- retry ------------------------------------------------------------ + + @Test + fun `retry falls back to the local copy when the server lost the job`() { + machine.setLocalAudio(true) + machine.fail("network") + assertEquals(DictationPhase.Failed, machine.phase) + assertTrue(machine.canRetry) + + assertEquals(DictationStep.Wait, machine.retryJobMissing()) + assertEquals("the local recording re-uploads", DictationPhase.Uploading, machine.phase) + assertNull(machine.jobId) + } + + @Test + fun `without a local copy a lost job is final`() { + machine.fail("network") + assertEquals(DictationStep.Failed(DictationMachine.JOB_NOT_FOUND), machine.retryJobMissing()) + assertEquals(DictationPhase.Failed, machine.phase) + assertFalse("nothing left to retry", machine.canRetry) + } + + @Test + fun `a retried job becomes this composer's own`() { + machine.setLocalAudio(true) + assertTrue(machine.adopt(listOf(SttJob("job-7", SttJobStatus.Error, "boom")))) + assertFalse("adopted jobs belong to another composer", machine.mine) + + assertNull(machine.retryAccepted(SttJob("job-7", SttJobStatus.Pending, null))) + assertTrue("the retrying browser becomes the job's owner", machine.mine) + assertEquals("job-7", machine.jobId) + assertEquals(DictationStep.Failed(DictationMachine.JOB_NOT_FOUND), machine.observe(null)) + } + + @Test + fun `a server retry restarts the follow deadline`() { + transcribing() + machine.observe(SttJob("job-1", SttJobStatus.Error, "boom")) + now += 60_000 + assertNull(machine.retryAccepted(SttJob("job-1", SttJobStatus.Pending, null))) + assertEquals(DictationPhase.Transcribing, machine.phase) + now += DictationMachine.STT_TIMEOUT_MS - 1_000 + assertEquals( + "the retry got a fresh six minutes", + DictationStep.Wait, + machine.observe(SttJob("job-1", SttJobStatus.Pending, null)), + ) + } + + @Test + fun `a retry the server refuses keeps the error visible`() { + transcribing() + machine.observe(SttJob("job-1", SttJobStatus.Error, "boom")) + assertEquals(DictationStep.Failed("busy"), machine.retryRejected("busy")) + assertEquals("busy", machine.message) + assertEquals(DictationPhase.Failed, machine.phase) + } + + // --- timeout ---------------------------------------------------------- + + @Test + fun `following gives up six minutes after it started`() { + transcribing() + now += DictationMachine.STT_TIMEOUT_MS + 1 + assertEquals(DictationStep.Failed(DictationMachine.TIMED_OUT), machine.observe(SttJob("job-1", SttJobStatus.Pending, null))) + assertEquals(DictationPhase.Failed, machine.phase) + + val claimer = DictationMachine { now } + claimer.adopt(listOf(SttJob("job-9", SttJobStatus.Pending, null))) + now += DictationMachine.STT_TIMEOUT_MS + 1 + assertEquals( + DictationStep.Failed(DictationMachine.TIMED_OUT), + claimer.takeClaim(SttClaim("job-9", SttJobStatus.Pending, null, null, null)), + ) + } + + // --- reset ------------------------------------------------------------ + + @Test + fun `discard forgets the job and the local copy`() { + transcribing() + machine.setLocalAudio(true) + machine.discard() + assertEquals(DictationPhase.Idle, machine.phase) + assertNull(machine.jobId) + assertFalse(machine.hasLocalAudio) + assertFalse(machine.canRetry) + assertEquals(SttAfter.Insert, machine.after) + assertTrue(machine.canAdopt()) + } + + @Test + fun `a recording restored from disk offers the re-upload`() { + machine.restoreRecording(SttAfter.Send, DictationMachine.TRANSCRIPTION_FAILED) + assertEquals(DictationPhase.Failed, machine.phase) + assertEquals(SttAfter.Send, machine.after) + assertTrue(machine.hasLocalAudio) + assertTrue(machine.canRetry) + assertEquals(DictationStep.Wait, machine.retryJobMissing()) + assertEquals(DictationPhase.Uploading, machine.phase) + } + + // --- delivery --------------------------------------------------------- + + @Test + fun `a transcript merges after the draft with one separating space`() { + assertEquals("hello world", mergeTranscript("hello", "world")) + assertEquals("hello world", mergeTranscript("hello ", "world")) + assertEquals("world", mergeTranscript("", "world")) + } + + @Test + fun `another composers job and Insert only fill the field`() { + assertEquals( + DictationOutcome.Insert("hello"), + dictationOutcome(DictationResult("hello", SttAfter.Send, isOwner = false), "draft", hasSession = true), + ) + assertEquals( + DictationOutcome.Insert("hello"), + dictationOutcome(DictationResult("hello", SttAfter.Insert, isOwner = true), "draft", hasSession = true), + ) + } + + @Test + fun `a session routes Send Steer and FollowUp and merges the draft`() { + assertEquals( + DictationOutcome.Send("draft hello"), + dictationOutcome(DictationResult("hello", SttAfter.Send, isOwner = true), "draft", hasSession = true), + ) + assertEquals( + DictationOutcome.Steer("draft hello"), + dictationOutcome(DictationResult("hello", SttAfter.Steer, isOwner = true), "draft", hasSession = true), + ) + assertEquals( + DictationOutcome.FollowUp("draft hello"), + dictationOutcome(DictationResult("hello", SttAfter.FollowUp, isOwner = true), "draft", hasSession = true), + ) + } + + @Test + fun `without a session every choice becomes a normal send`() { + for (after in listOf(SttAfter.Send, SttAfter.Steer, SttAfter.FollowUp)) { + assertEquals( + "the web's new chat sends the first prompt for $after", + DictationOutcome.Send("draft hello"), + dictationOutcome(DictationResult("hello", after, isOwner = true), "draft", hasSession = false), + ) + } + } + + // --- dictation engine -------------------------------------------------- + + @Test + fun `auto uses the server when one is configured and the phone otherwise`() { + assertEquals( + DictationEngine.SERVER, + resolveDictationEngine(DictationEnginePreference.AUTO, serverConfigured = true), + ) + assertEquals( + DictationEngine.ON_DEVICE, + resolveDictationEngine(DictationEnginePreference.AUTO, serverConfigured = false), + ) + } + + @Test + fun `auto falls back to the phone after the server reported no STT`() { + assertEquals( + DictationEngine.ON_DEVICE, + resolveDictationEngine( + DictationEnginePreference.AUTO, + serverConfigured = true, + serverSttUnconfigured = true, + ), + ) + assertEquals( + "an explicit server choice still asks the server", + DictationEngine.SERVER, + resolveDictationEngine( + DictationEnginePreference.SERVER, + serverConfigured = true, + serverSttUnconfigured = true, + ), + ) + } + + @Test + fun `auto never picks a phone without an installed model`() { + assertEquals( + "auto without a model uses the configured server path", + DictationEngine.SERVER, + resolveDictationEngine( + DictationEnginePreference.AUTO, + serverConfigured = true, + onDeviceAvailable = false, + ), + ) + assertEquals( + "the explicit on-device choice still surfaces the missing model", + DictationEngine.ON_DEVICE, + resolveDictationEngine( + DictationEnginePreference.ON_DEVICE, + serverConfigured = true, + onDeviceAvailable = false, + ), + ) + } + + @Test + fun `an explicit engine wins over the server state`() { + assertEquals( + DictationEngine.ON_DEVICE, + resolveDictationEngine(DictationEnginePreference.ON_DEVICE, serverConfigured = true), + ) + assertEquals( + DictationEngine.SERVER, + resolveDictationEngine(DictationEnginePreference.SERVER, serverConfigured = false), + ) + } + + @Test + fun `auto never lands on the phone without an on-device model`() { + assertEquals( + "a configured server stays the engine", + DictationEngine.SERVER, + resolveDictationEngine( + DictationEnginePreference.AUTO, + serverConfigured = true, + serverSttUnconfigured = true, + onDeviceAvailable = false, + ), + ) + assertEquals( + "without any server the on-device start reports the missing model", + DictationEngine.ON_DEVICE, + resolveDictationEngine( + DictationEnginePreference.AUTO, + serverConfigured = false, + onDeviceAvailable = false, + ), + ) + assertEquals( + "the explicit choice is kept so its start can explain the missing model", + DictationEngine.ON_DEVICE, + resolveDictationEngine( + DictationEnginePreference.ON_DEVICE, + serverConfigured = true, + onDeviceAvailable = false, + ), + ) + } + + @Test + fun `an unknown stored engine falls back to auto`() { + assertEquals(DictationEnginePreference.AUTO, DictationEnginePreference.fromWire(null)) + assertEquals(DictationEnginePreference.AUTO, DictationEnginePreference.fromWire("")) + assertEquals(DictationEnginePreference.AUTO, DictationEnginePreference.fromWire("nonsense")) + assertEquals(DictationEnginePreference.ON_DEVICE, DictationEnginePreference.fromWire("on_device")) + assertEquals(DictationEnginePreference.SERVER, DictationEnginePreference.fromWire("server")) + } + + @Test + fun `the recognition language uses the pick when valid and the device locale otherwise`() { + assertEquals("de-DE", dictationLanguageTag("de-DE", Locale.US)) + assertEquals("en-GB", dictationLanguageTag("en-GB", Locale.GERMANY)) + assertEquals( + "the device locale wins over a malformed pick", + "de-DE", + dictationLanguageTag("not a tag", Locale.GERMANY), + ) + assertEquals("device locale when nothing is picked", "de-DE", dictationLanguageTag(null, Locale.GERMANY)) + assertEquals("language-only locales keep no dash", "de", dictationLanguageTag(" ", Locale("de"))) + } + + @Test + fun `the offered languages put the device language first`() { + val offered = offeredDictationLanguages(Locale("sv", "SE")) + assertEquals("Swedish is already offered", "en-US", offered.first()) + assertEquals(1, offered.count { it == "sv-SE" }) + + val unusual = offeredDictationLanguages(Locale("is", "IS")) + assertEquals("the device language is added when missing", "is-IS", unusual.first()) + assertTrue(unusual.contains("en-US")) + } + + @Test + fun `language labels name the locale`() { + assertEquals("German (Germany)", dictationLanguageLabel("de-DE", Locale.US)) + } + + // --- live on-device insertion ----------------------------------------- + + @Test + fun `the first partial lands at the caret with one separating space`() { + val buffer = LiveDictationBuffer() + val insertion = buffer.onPartial(current = "Reply with", caret = 10, partial = "OK") + assertEquals("Reply with OK", insertion?.text) + assertEquals(13, insertion?.caret) + assertTrue(buffer.active) + } + + @Test + fun `a caret after whitespace does not add a second space`() { + val buffer = LiveDictationBuffer() + val insertion = buffer.onPartial("Reply with ", 11, "hello") + assertEquals("Reply with hello", insertion?.text) + assertEquals(16, insertion?.caret) + } + + @Test + fun `a later partial replaces the visible live text instead of appending`() { + val buffer = LiveDictationBuffer() + val first = buffer.onPartial("hello", 5, "wor")!! + val second = buffer.onPartial(first.text, first.caret, "world") + assertEquals("hello world", second?.text) + assertEquals(11, second?.caret) + assertEquals( + "the same partial is not applied again", + null, + buffer.onPartial(second!!.text, second.caret, "world"), + ) + } + + @Test + fun `the final transcript replaces the partial`() { + val buffer = LiveDictationBuffer() + val first = buffer.onPartial("", 0, "helo") + assertEquals("helo", first?.text) + val final = buffer.onFinal(first!!.text, first.caret, "Hello.") + assertEquals("Hello.", final?.text) + assertEquals(6, final?.caret) + assertFalse("the capture is finished", buffer.active) + } + + @Test + fun `a final that arrives without a partial is inserted once`() { + val buffer = LiveDictationBuffer() + val insertion = buffer.onFinal("draft", 5, " hello world ") + assertEquals("draft hello world", insertion?.text) + assertEquals(17, insertion?.caret) + + assertEquals("blank results insert nothing", null, buffer.onFinal("draft", 5, " ")) + } + + @Test + fun `a user edit while listening keeps the visible text and re-anchors`() { + val buffer = LiveDictationBuffer() + val first = buffer.onPartial("draft", 5, "spoken")!! + // The user typed in front of the live text; the caret is now after it. + val edited = "NEW ${first.text}" + val next = buffer.onPartial(edited, edited.length, "again") + assertEquals("NEW draft spoken again", next?.text) + assertEquals( + "the new partial follows the last visible text and the new caret", + "NEW draft spoken again".length, + next?.caret, + ) + } + + @Test + fun `cancelling removes the live text again`() { + val buffer = LiveDictationBuffer() + val first = buffer.onPartial("draft", 5, "spoken")!! + val restored = buffer.onCancel(first.text) + assertEquals("draft", restored?.text) + assertEquals(5, restored?.caret) + + assertEquals("nothing to restore after a cancel", null, buffer.onCancel("draft")) + assertFalse(buffer.active) + } +} diff --git a/android/app/src/test/java/io/github/kahme247/ompweb/domain/ExtensionUiTest.kt b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ExtensionUiTest.kt new file mode 100644 index 000000000..7f9faff19 --- /dev/null +++ b/android/app/src/test/java/io/github/kahme247/ompweb/domain/ExtensionUiTest.kt @@ -0,0 +1,584 @@ +package io.github.kahme247.ompweb.domain + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Port of the extension-dialog rules in `components/ExtensionDialog.tsx` and + * `hooks/useAgentSession.ts`: the `ask` drafts/answers, the URL allowlist, the + * frame parser and the replay/expiry bookkeeping. + */ +class ExtensionUiTest { + + private fun question( + id: String = "q1", + multi: Boolean = false, + recommended: Int? = null, + options: List = listOf("A", "B"), + ): AskQuestion = AskQuestion( + id = id, + question = "Pick", + header = null, + options = options.map { AskOption(it, null, null) }, + multi = multi, + recommended = recommended, + ) + + private fun frame(method: String, vararg fields: Pair): JsonObject = + buildJsonObject { + put("type", "extension_ui_request") + put("method", method) + put("id", "req-1") + fields.forEach { (key, value) -> put(key, value) } + } + + // ---------------------------------------------------------------- ask form + + @Test + fun `single-select starts on its recommended option, multi-select starts empty`() { + val single = question(recommended = 1) + val multi = question(id = "q2", multi = true, recommended = 0) + val form = initialAskForm(listOf(single, multi)) + assertEquals(listOf("B"), form.draftAt(0).selected) + assertTrue(form.draftAt(1).selected.isEmpty()) + } + + @Test + fun `single-select needs a choice or a non-empty Other, multi-select may stay empty`() { + val single = question() + val multi = question(id = "q2", multi = true) + val questions = listOf(single, multi) + assertFalse(AskForm(listOf(AskDraft(), AskDraft())).canSubmit(questions)) + + var form = AskForm(listOf(AskDraft(), AskDraft())).toggleOption(questions, 0, "A") + assertTrue(form.canSubmit(questions)) + + form = AskForm(listOf(AskDraft(other = " ", otherChosen = true), AskDraft())) + assertFalse(form.canSubmit(questions)) + + form = AskForm(listOf(AskDraft(other = "something", otherChosen = true), AskDraft())) + assertTrue(form.canSubmit(questions)) + } + + @Test + fun `typing Other clears a single-select choice but keeps multi-select choices`() { + val single = question() + val multi = question(id = "q2", multi = true) + val questions = listOf(single, multi) + + var form = AskForm(listOf(AskDraft(selected = listOf("A")), AskDraft(selected = listOf("A")))) + form = form.setOther(questions, 0, "custom") + assertEquals(emptyList(), form.draftAt(0).selected) + assertTrue(form.draftAt(0).otherChosen) + + form = form.setOther(questions, 1, "custom") + assertEquals(listOf("A"), form.draftAt(1).selected) + assertTrue(form.draftAt(1).otherChosen) + } + + @Test + fun `clearing Other unchooses it so a blank answer never blocks Submit`() { + val questions = listOf(question()) + var form = AskForm(listOf(AskDraft(other = "abc", otherChosen = true))) + form = form.setOther(questions, 0, " ") + assertFalse(form.draftAt(0).otherChosen) + assertFalse(form.canSubmit(questions)) + } + + @Test + fun `choosing an option unchooses Other but keeps its text`() { + val questions = listOf(question()) + var form = AskForm(listOf(AskDraft(other = "kept", otherChosen = true))) + form = form.toggleOption(questions, 0, "B") + assertEquals(listOf("B"), form.draftAt(0).selected) + assertFalse(form.draftAt(0).otherChosen) + assertEquals("kept", form.draftAt(0).other) + } + + @Test + fun `tapping back into kept Other text answers with it again`() { + val questions = listOf(question()) + val form = AskForm(listOf(AskDraft(selected = listOf("A"), other = "kept"))) + val focused = form.focusOther(questions, 0) + assertTrue(focused.draftAt(0).otherChosen) + assertEquals(emptyList(), focused.draftAt(0).selected) + + val blank = AskForm(listOf(AskDraft(other = " "))) + assertTrue(blank.focusOther(questions, 0).draftAt(0).otherChosen.not()) + } + + @Test + fun `answers keep option ids, option order and only send a non-empty custom input`() { + val single = question(recommended = 0) + val multi = question(id = "q2", multi = true) + val questions = listOf(single, multi) + var form = initialAskForm(questions) + form = form.toggleOption(questions, 1, "B") + form = form.toggleOption(questions, 1, "A") + form = form.setOther(questions, 0, " free text ") + + val answers = form.answers(questions) + assertEquals(listOf("q1", "q2"), answers.map { it.id }) + assertEquals(emptyList(), answers[0].selectedOptions) + assertEquals("free text", answers[0].customInput) + assertEquals(listOf("A", "B"), answers[1].selectedOptions) + assertNull(answers[1].customInput) + } + + // ------------------------------------------------------------ URL allowlist + + @Test + fun `only http, https and mailto survive the open-url allowlist`() { + assertTrue(isSafeOpenUrl("https://example.com/path?q=1")) + assertTrue(isSafeOpenUrl("http://example.com")) + assertTrue(isSafeOpenUrl("mailto:user@example.com")) + assertTrue(isSafeOpenUrl(" HTTPS://EXAMPLE.COM ")) + assertTrue(isSafeOpenUrl("HtTp://example.com")) + + assertFalse(isSafeOpenUrl(null)) + assertFalse(isSafeOpenUrl("")) + assertFalse(isSafeOpenUrl(" ")) + assertFalse(isSafeOpenUrl("//example.com")) + assertFalse(isSafeOpenUrl("javascript:alert(1)")) + assertFalse(isSafeOpenUrl("data:text/html,