mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-30 07:37:38 +00:00
The 33 descriptions (~16k chars, ~19k with paths) overran Codex's skill-list budget (2% of the context window, or 8,000 chars when unknown), and a review found most bodies stale: paths from before the commons/commonsUI moves, the deleted legacy desktop app, a callback signer API that no longer exists, and example APIs that were never in the tree. Kept, rewritten and checked against the code (every path, class and command): account-state (+feeds), amy-expert, android-expert, auth-signers, compose-expert (+API style, Navigation 3 back stacks), desktop-expert (+run/package), event-store-semantics (+NIP-50 search), find-missing- translations, kotlin-coroutines (+structured concurrency, flow modeling), kotlin-multiplatform (+Gradle, composable extraction), ngit-pr, nip85-trusted-assertions, nostr-expert (+NIP lookup, generated NIP catalog), relay-client. Dropped: textbook Compose/Kotlin technique skills, kotlin-expert, kotlin-types-value-class, gradle-expert, quartz-integration (external-facing; see the Quartz docs commit), find-non-lambda-logs (audit finished; the rule is now in AGENTS.md Kotlin Style), and the /nip, /desktop-run, /extract commands. The skill list is now ~4k chars. Also drops .claude/core-skills-plan.md (stale history) and repoints SearchableEvent's KDoc to event-store-semantics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60 lines
3.6 KiB
Markdown
60 lines
3.6 KiB
Markdown
# Rich Text Parsing
|
|
|
|
Event content (text with URLs, mentions, hashtags, media links, nostr references, invoices,
|
|
markdown) is parsed headlessly into segments, then rendered by shared composables.
|
|
|
|
## Parser (headless, `commons`)
|
|
|
|
All under `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/`:
|
|
|
|
- **`RichTextParser.kt`**: `class RichTextParser`, `parseText(content, tags, callbackUri, authorPubKey)`
|
|
returns a `RichTextViewerState`.
|
|
- **`CachedRichTextParser.kt`**: `object CachedRichTextParser`, an LRU in front of the parser:
|
|
`parseText(...)` (parse or hit), `cachedText(...)` (hit only, may return null),
|
|
`isMarkdown(content)`, `trimToSize(maxItems)`. **Always parse through this.**
|
|
- **`RichTextParserSegments.kt`**: `RichTextViewerState` (`urlSet`, `mediaForPager`, `mediaList`,
|
|
`customEmoji`, `paragraphs`, `tags`, all `Immutable*`), `ParagraphState` /
|
|
`ImageGalleryParagraph`, and the `Segment` subclasses (`ImageSegment`, `VideoSegment`,
|
|
`LinkSegment`, `HashTagSegment`, `BechSegment`, `HashIndexUserSegment`, `InvoiceSegment`,
|
|
`CashuSegment`, `RelayUrlSegment`, `MathSegment`, `RegularTextSegment`, ...).
|
|
- **`Patterns.kt`**: the regex bank (URL, hashtag, mention, email, invoice, cashu, nostr URI).
|
|
Add a case here instead of writing a regex at a call site.
|
|
- **`UrlParser.kt`**: `UrlParser` / `Urls`, URL extraction before classification.
|
|
- **`GalleryParser.kt`**: groups consecutive media into an `ImageGalleryParagraph`.
|
|
- **`MediaContentModels.kt`**: `MediaUrlImage`, `MediaUrlVideo`, `MediaUrlPdf` and their
|
|
encrypted siblings `EncryptedMediaUrlImage` / `EncryptedMediaUrlVideo`.
|
|
**`MediaLocalContent.kt`**: `MediaLocalImage` / `MediaLocalVideo` for not-yet-uploaded drafts.
|
|
- **`ExpandableTextCutOffCalculator.kt`**: where to fold long content for "Show more".
|
|
|
|
## Renderer (`commonsUI`)
|
|
|
|
- `commonsUI/.../commons/ui/components/RichTextViewer.kt`: `RichTextViewer(content, ...)`, the
|
|
entry point screens call. It checks `CachedRichTextParser.isMarkdown`, parses through
|
|
`CachedRichTextParser.parseText` inside `remember(content, tags)`, and hands the state to the
|
|
core below.
|
|
- `commonsUI/.../commons/ui/richtext/RichTextViewer.kt`: the shared core that takes a parsed
|
|
`RichTextViewerState`: paragraphs, RTL, word flow, plain text, custom emoji, hashtags.
|
|
- `commonsUI/.../commons/ui/richtext/RichTextSegmentRenderer.kt`: the seam for segments whose
|
|
look and action differ per platform (media, gallery, equation, nostr entity, quoted event,
|
|
user mention, payment, ...), installed through `LocalRichTextSegmentRenderer`;
|
|
`DefaultRichTextSegmentRenderer.kt` is the default. See the `README.md` in that folder.
|
|
|
|
To render a new segment type: add the `Segment` subclass and its detection in the parser, then
|
|
either draw it in the shared core (if it looks the same everywhere) or add a method to
|
|
`RichTextSegmentRenderer`.
|
|
|
|
## NIP-92 imeta enrichment
|
|
|
|
`imeta` tags carry per-URL metadata (`url`, `m`, `dim`, `blurhash`, `x`, `size`). The parser
|
|
maps them onto `MediaUrlImage` / `MediaUrlVideo` so the renderer can reserve the aspect ratio
|
|
and show a blurhash before the image loads. The data comes from the event's tags, not the URL.
|
|
|
|
## Gotchas
|
|
|
|
- **Never parse per recomposition.** Go through `CachedRichTextParser`; it is keyed by content,
|
|
tags, callback URI and author.
|
|
- **Encrypted media is a subclass.** `EncryptedMediaUrlImage` extends `MediaUrlImage` (and the
|
|
video likewise), so an `is MediaUrlImage` branch also catches encrypted NIP-17 / Blossom media.
|
|
Test for the encrypted class first wherever it needs decryption.
|
|
- **Changing `GalleryParser` grouping** changes the layout of every note with several images.
|