# Отрисовка Markdown: откуда взято и где лежит **Статус: УЖЕ СКОПИРОВАНО В ПРОЕКТ (28.09.2026).** Код вендорен в `src/`, собирается, тесты парсера (11 шт.) зелёные. Своё писать не надо. Источник — приватный репозиторий **`ai/assistent`** (https://git.binom.pw/ai/assistent, ветка `main`, проверено на коммите `2c5b3f9`, файлы добавлены в `1acced4` 2026-08-13). Пакеты переименованы под наш проект; внешняя зависимость — одна (`ru.otpbank.ai:markdown`). --- ## Куда что легло | В assistent | У нас | |---|---| | `client-shared/.../shared/markdown/MarkdownSegment.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/markdown/MarkdownSegment.kt` | | `client-shared/.../shared/markdown/MarkdownParserImpl.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/markdown/MarkdownParserImpl.kt` | | `client-shared/.../shared/ui/MarkdownRendering.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRendering.kt` | | `client-shared/.../shared/ui/ChatImage.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ChatImage.kt` | | `client-shared/.../shared/ClientImageSupport.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ClientImageSupport.kt` | | `client/.../client/ui/MarkdownRenderer.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRenderer.kt` | | `client-shared/src/test/.../markdown/MarkdownParserTest.kt` | `src/jvmTest/kotlin/pw/binom/agentik/desktop/markdown/MarkdownParserTest.kt` | Переименование пакетов: - `pw.binom.client.shared.markdown` → `pw.binom.agentik.desktop.markdown` - `pw.binom.client.shared.ui` → `pw.binom.agentik.desktop.ui` - `pw.binom.client.shared` (`ClientImageSupport`) → `pw.binom.agentik.desktop.ui` - `pw.binom.client.ui` (`MarkdownRenderer`) → `pw.binom.agentik.desktop.ui` ### Почему файлов шесть, а не четыре Изначально в доке значились 4 файла. При сборке выяснилось, что `MarkdownRendering.kt` для сегментов-картинок вызывает `ChatImage(...)`, а тот — `ClientImageSupport`. Поэтому взяты ещё `ChatImage.kt` и `ClientImageSupport.kt`. Оба независимы от `chat-*` и `MessageBubble`. `ClientImageSupport` — это реестр: платформенный клиент при старте регистрирует `decoder` (байты → `ImageBitmap`) и, при желании, `fetcher` и `serverBaseUrl`. Пока `decoder` не зарегистрирован, картинки деградируют в текст (alt/ссылку) — ничего не падает. На десктопе декодер регистрируется так: ```kotlin ClientImageSupport.decoder = { bytes -> org.jetbrains.skia.Image.makeFromEncoded(bytes).toComposeImageBitmap() } ``` ## Точки входа - Парсинг: `parseMarkdown(markdown: String): List` (в `...desktop.markdown`). - Отрисовка: `renderMarkdownSegments(content, textColor, fontSize, isBubbleScrolled, onUrlClick)` (в `...desktop.ui`). - Открытие ссылок/`parseMultiline`: `...desktop.ui.MarkdownRenderer`. ## Зависимости и сборка (уже настроено) `gradle/libs.versions.toml`: ```toml compose-multiplatform = "1.7.3" # версия из assistent; собирается на нашем Kotlin 2.4.20 markdown = "0.51.0" markdown = { module = "ru.otpbank.ai:markdown", version.ref = "markdown" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } ``` `build.gradle.kts` — плагины `kotlin-compose` + `compose`, зависимости `compose.foundation` / `compose.ui` / `compose.material3` / `compose.materialIconsExtended` (иконка копирования) + `libs.markdown`. `compose.desktop.currentOs` пока НЕ подключён — он понадобится, когда появится настоящее окно (`compose.desktop { application { mainClass = ... } }`), для компиляции рендера он не нужен. `settings.gradle.kts` — репозитории (без них `ru.otpbank.ai:markdown` не найдётся): ```kotlin maven { name = "developerspace" url = uri("http://192.168.76.117/repository/developerspace-prod-mvn-hosted/") setAllowInsecureProtocol(true) } maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.google.com") ``` Проверено: `./gradlew compileKotlinJvm` — OK; `./gradlew jvmTest` — `MarkdownParserTest` 11/11, `ConversationMetaRepositoryIT` 14/14. ## Как пересинхронизировать, если в assistent что-то поправят RAW-адрес: `/raw/branch/main/<путь>` (нужен логин, репозиторий приватный). ```bash BASE=https://git.binom.pw/ai/assistent/raw/branch/main AUTH='subochev:ПАРОЛЬ' for f in \ client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownSegment.kt \ client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownParserImpl.kt \ client-shared/src/main/kotlin/pw/binom/client/shared/ui/MarkdownRendering.kt \ client-shared/src/main/kotlin/pw/binom/client/shared/ui/ChatImage.kt \ client-shared/src/main/kotlin/pw/binom/client/shared/ClientImageSupport.kt \ client/src/main/kotlin/pw/binom/client/ui/MarkdownRenderer.kt ; do curl -sS -u "$AUTH" "$BASE/$f" -o "/tmp/$(basename "$f")" done ``` После копирования — тем же `sed`-ом переименовать пакеты (см. таблицу выше), `MarkdownRenderer` дополнительно сослать `renderMarkdownSegments` на `pw.binom.agentik.desktop.ui`. ## Что рендер умеет (по коду) - **Текст** с участками: жирный, курсив, код, зачёркнутый, ссылки. - **Блоки кода** — с названием языка и кнопкой копирования (`LocalClipboardManager`). - **Таблицы** — заголовки/строки, переключаются на горизонтальный скролл по `isBubbleScrolled` (иначе Compose падает на бесконечной maxWidth). - **Картинки** — `data:` и `http(s)`; рисуются через `ChatImage`/`ClientImageSupport`. - **Разделители** (`---`). - **Кликабельные ссылки** — нажатие уходит в `onUrlClick`. ## Известный баг на будущее В assistent в `MessageBubble.kt:153` (НЕ скопирован) `hasTable` вызывает `parseMarkdown` вторым проходом до основной отрисовки — двойной парс на пузырь. У нас `MessageBubble` свой, при его написании в 0-й версии просто звать `renderMarkdownSegments` один раз; кэшировать разбор через `remember(record.id, record.text)`. ## Что НЕ брали - `MessageBubble.kt` — тянет `chat-common` и чужие модели сообщений. - Модули `chat-*`, `ChatScreen`, `ChatViewModel`, `SettingsDialog` и т. п. - Границы заимствования — в `BORROW-FROM-ASSISTENT.md`.