Files
amethyst/.claude/skills/compose-expert/references/rich-text-parsing.md
T
Claude 60edd473c7 Refreshes .claude/ skill library: fixes stale refs, adds 4 new skills
- Updates CLAUDE.md tech stack to current versions (Compose 1.10.3, Kotlin 2.3.20).
- Reframes kotlin-multiplatform iOS as mature; adds secp256k1-kmp 0.23.0 references.
- Updates desktop-expert Main.kt references (code grew from ~270 to 1341 lines and
  NavigationRail moved to ui/deck/SinglePaneLayout.kt); replaces obsolete
  "hardcoded ctrl = true" anti-pattern note with accurate isMacOS branching.
- Removes compose-desktop.md (superseded by desktop-expert/).
- Adds nostr-expert references: nip19-bech32, event-factory, crypto-and-encryption,
  large-cache. Adds kotlin-expert/common-utilities, compose-expert/rich-text-parsing,
  android-expert/image-loading.
- New skills: account-state (Account + LocalCache), relay-client (subscriptions,
  filter assemblers, preloaders), feed-patterns (FeedFilter + FeedViewModel family),
  auth-signers (NostrSigner across internal / NIP-46 / NIP-55).
2026-04-21 21:00:45 +00:00

4.1 KiB

Rich Text Parsing

Amethyst converts raw event content (plain text with URLs, mentions, hashtags, media links, nostr references, markdown) into structured segments that Compose can render. Everything lives under commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/.

Files

  • RichTextParser.kt — the main entry point. A class RichTextParser that takes a note's content, the URL preview cache, and NIP-92 imeta tags and returns a RichTextViewState.
  • RichTextParserSegments.kt — segment data classes (hashtag, url, mention, invoice, etc.) that the parser emits.
  • Patterns.kt — the regex bank. Single source of truth for URL, hashtag, mention, email, invoice, cashu, nostr-URI patterns. Prefer adding a case here to writing a one-off regex at a call site.
  • UrlParser.kt — URL extraction + validation; used to pull URLs out of free-form text before the parser classifies them.
  • GalleryParser.kt — builds MediaGallery groupings from consecutive media URLs in a note.
  • MediaContentModels.kt — the rendering contracts:
    • MediaUrlImage, MediaUrlVideo — plain HTTP(S) media with optional NIP-92 metadata.
    • EncryptedMediaUrlImage, EncryptedMediaUrlVideo — for encrypted/blossom-gated media.
    • MediaLocalImage, MediaLocalVideo — for drafts / not-yet-uploaded media.
  • Base64Image.kt — inline base64 data URI support.
  • ExpandableTextCutOffCalculator.kt — decides where to truncate long content for "Show more" fold points.

How a Note Becomes Rendered UI

  1. Raw content: String arrives (from an Event).
  2. RichTextParser scans with patterns, extracts URLs, nostr IDs, hashtags, mentions, invoices, cashu tokens.
  3. URLs are classified against imeta tags (NIP-92) so media gets correct dimensions, mime type, blurhash.
  4. GalleryParser groups adjacent media into a single MediaGallery segment.
  5. The composable layer (elsewhere in commons/compose/ and amethyst/desktop UI) walks the segment list and renders each with the appropriate composable (RenderMarkdown, NoteQuoteBody, ClickableUrl, etc.).

Typical Reuse

// inside a composable
val state = remember(note, imetaTags) {
    CachedRichTextParser.parseReturningNullable(content, imetaTags, callbackUri)
}
state?.paragraphs?.forEach { paragraph ->
    paragraph.words.forEach { segment ->
        when (segment) {
            is UrlSegment       -> ClickableUrl(segment)
            is HashtagSegment   -> HashtagChip(segment)
            is NostrRefSegment  -> NoteCompose(segment.entity)
            is ImageSegment     -> ZoomableMedia(segment.media)
            // ...
        }
    }
}

On Android there's amethyst/.../service/CachedRichTextParser.kt which caches parser output per content — re-parsing the same note on every recomposition is expensive, so always parse behind a cache.

NIP-92 imeta Enrichment

imeta tags attached to an event carry structured metadata for each media URL: url, m (mime), dim, blurhash, x (sha256), size. RichTextParser maps these into MediaUrlImage / MediaUrlVideo so the renderer can reserve correct aspect ratio and show a blurhash placeholder before the image loads. Reference: nip-catalog.md.

Gotchas

  • Don't parse on every recomposition. Use CachedRichTextParser (Android) or remember(content, imeta) { … } for commonMain.
  • Regexes live in Patterns.kt. If you're writing a new regex for URLs/mentions/hashtags in a UI file, move it to Patterns.kt instead.
  • Segments are @Immutable data classes — safe to pass to Compose without triggering recomposition spam.
  • Encrypted media is a separate class (EncryptedMediaUrl*). If you handle MediaUrlImage but not its encrypted sibling, blossom/NIP-17 gated media silently falls through.
  • GalleryParser groups across whitespace-only-lines between URLs. Changing its grouping rules breaks layout in many note screens.
  • nostr-expert/references/nip-catalog.md — NIP-92 (imeta) spec location
  • compose-expert/references/shared-composables-catalog.md — which composables consume which segment types