Skip to content

feat(markdown): native SVG decoder + theme-aware images + browser UA#618

Merged
rainxchzed merged 1 commit into
mainfrom
feat/markdown-image-fixes
May 16, 2026
Merged

feat(markdown): native SVG decoder + theme-aware images + browser UA#618
rainxchzed merged 1 commit into
mainfrom
feat/markdown-image-fixes

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 16, 2026

  • Coil3 SVG decoder registered globally via SingletonImageLoaderFactory in App() — README diagrams + screenshots that are SVG render natively. Preprocessor's blanket-SVG skip narrowed to badge-only.
  • MarkdownImageTransformer ImageRequest now sends browser-like User-Agent + standard Accept headers — unblocks hotlink-protected CDN badges.
  • New `applyThemeAwareImages(content, isDark)` in core/domain/util strips `#gh-dark-mode-only` / `#gh-light-mode-only` images on the mismatched theme, matching GitHub's own behavior. Wired into About + WhatsNew via `remember(raw, isDark)`.

Test plan

  • Compile clean (composeApp + feature/details)
  • Device: open repo with SVG diagrams (any draw.io / mermaid-exported README) — verify they render. Switch theme, theme-only images flip correctly.

Summary by CodeRabbit

  • New Features

    • Native SVG rendering for README and release notes (including diagrams/hero images)
    • Theme-aware image handling: light/dark-only images now respect app theme
  • Improved

    • CDN badge rendering fixed by using a browser-like User-Agent
    • Markdown image handling refined so badges are preserved while SVGs render natively

Review Change Stack

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rainxchzed has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0c02a801-1dbd-4325-9544-9c668e24c0cb

📥 Commits

Reviewing files that changed from the base of the PR and between c7b9259 and f6b9e0e.

📒 Files selected for processing (21)
  • composeApp/build.gradle.kts
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/applyThemeAwareImages.kt
  • core/presentation/src/commonMain/composeResources/files/whatsnew/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ar/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/bn/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/es/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/fr/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/hi/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/it/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ja/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ko/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/pl/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ru/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/tr/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/18.json
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/utils/preprocessMarkdown.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt
  • gradle/libs.versions.toml

Walkthrough

This PR adds native SVG rendering and theme-aware image handling to the GitHub Store app. It integrates Coil 3 for image loading with SVG support, implements a utility to process GitHub's #gh-dark-mode-only and #gh-light-mode-only image URL fragments, updates the markdown rendering pipeline to use Coil with custom headers while removing SVG blocking, and wires the theme-aware processing into the About and WhatsNew screens across all supported localizations.

Changes

SVG and Theme-Aware Image Rendering

Layer / File(s) Summary
Gradle dependencies and Coil SVG decoder registration
gradle/libs.versions.toml, composeApp/build.gradle.kts, composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt
Coil 3.3.0 is added to the version catalog with new libraries (coil3-compose, coil3-network-ktor3, coil3-svg) and included in the landscapist bundle; composeApp adds Coil 3 deps and App registers the Coil SVG decoder via the singleton ImageLoader factory.
Theme-aware image processing utility
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/applyThemeAwareImages.kt
New applyThemeAwareImages(content: String, isDark: Boolean): String rewrites/removes Markdown image and HTML <img> entries containing #gh-dark-mode-only / #gh-light-mode-only to match the current theme.
Image transformer with Coil request and header configuration
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt
MarkdownImageTransformer now builds Coil ImageRequest with custom User-Agent and Accept headers and passes the request to rememberAsyncImagePainter; it rejects non-HTTP(S)/non-data: links and no longer early-returns for SVG URLs.
Markdown preprocessing image skip rules
feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/utils/preprocessMarkdown.kt
shouldSkipImage now skips only badge-like images (isBadgeUrl) instead of broadly skipping SVGs, allowing SVGs to pass through preprocessing.
Theme-aware image processing in UI screens
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt, feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt
About and WhatsNew now compute raw markdown, detect system dark mode, and memoize displayContent via remember(raw,isDark) after applying applyThemeAwareImages(raw,isDark) before rendering.
Localized release notes documenting SVG and theme-aware features
core/presentation/src/commonMain/composeResources/files/whatsnew/{18,ar,bn,es,fr,hi,it,ja,ko,pl,ru,tr,zh-CN}/18.json
Release-notes JSON across locales are updated to mention native SVG rendering in README/release notes, theme-aware image handling, and a browser User-Agent fix for CDN badge hotlink blocking.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hop through markdown, nibble on a tag,
SVGs shine bright where once they’d lag.
Themes now choose which picture to show,
Coil brings the magic, headers in tow,
README art blooms in dark and glow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely summarizes the three main changes: native SVG decoder support, theme-aware image handling, and browser User-Agent updates for markdown image rendering.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/markdown-image-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt (1)

35-49: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle protocol-relative URLs before scheme validation.

Lines 44-47 currently reject //... image links, which are still encountered in markdown content. Normalize them to https:// first so they can render.

Suggested patch
-        val normalizedLink =
+        val normalizedLink =
             if (link.contains("github.com") && link.contains("/blob/")) {
                 link
                     .replace("github.com", "raw.githubusercontent.com")
                     .replace("/blob/", "/")
             } else {
                 link
             }
+        val requestLink =
+            if (normalizedLink.startsWith("//")) {
+                "https:$normalizedLink"
+            } else {
+                normalizedLink
+            }
 
-        if (!normalizedLink.startsWith("http://") &&
-            !normalizedLink.startsWith("https://") &&
-            !normalizedLink.startsWith("data:")
+        if (!requestLink.startsWith("http://") &&
+            !requestLink.startsWith("https://") &&
+            !requestLink.startsWith("data:")
         ) {
             return null
         }
@@
-                .data(normalizedLink)
+                .data(requestLink)

Also applies to: 53-55

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt`
around lines 35 - 49, The scheme validation currently rejects protocol-relative
URLs like "//host/path"; update the normalization flow in
MarkdownImageTransformer (where normalizedLink is computed) to detect links
starting with "//" and prepend "https:" before performing the github->raw
conversion and the subsequent startsWith checks, and apply the same adjustment
to the second validation block around lines 53-55 so both checks accept
protocol-relative URLs after normalization.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/applyThemeAwareImages.kt`:
- Around line 14-27: The current replacement returns "" for mismatched themed
images which can leave a leftover linking wrapper like [![](...)](...); update
the replacement logic in applyThemeAwareImages (the processed.replace call and
its regex/replacement) to detect and remove an optional surrounding markdown
link when dropping an inner image: either extend the Regex to optionally capture
a surrounding "[ ... ](...)" wrapper and return an empty string for the whole
wrapper, or in the lambda inspect characters/substring around the matched span
(using processed and match.range) to remove the enclosing "[...](...)" if
present; ensure you still strip the fragment and rebuild the image URL for kept
matches (using match.groupValues[1..4] / the same alt/urlBase/trailing
variables).

---

Outside diff comments:
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt`:
- Around line 35-49: The scheme validation currently rejects protocol-relative
URLs like "//host/path"; update the normalization flow in
MarkdownImageTransformer (where normalizedLink is computed) to detect links
starting with "//" and prepend "https:" before performing the github->raw
conversion and the subsequent startsWith checks, and apply the same adjustment
to the second validation block around lines 53-55 so both checks accept
protocol-relative URLs after normalization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 82d53420-8dfc-4ee8-8a40-4276897016d3

📥 Commits

Reviewing files that changed from the base of the PR and between 01226dc and c7b9259.

📒 Files selected for processing (21)
  • composeApp/build.gradle.kts
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/applyThemeAwareImages.kt
  • core/presentation/src/commonMain/composeResources/files/whatsnew/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ar/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/bn/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/es/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/fr/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/hi/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/it/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ja/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ko/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/pl/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ru/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/tr/18.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/18.json
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/utils/preprocessMarkdown.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt
  • gradle/libs.versions.toml

Comment on lines +14 to +27
processed = processed.replace(
Regex("""!\[([^\]]*)\]\(([^)]*?)#gh-(dark|light)-mode-only([^)]*)\)"""),
) { match ->
val mode = match.groupValues[3]
if ((isDark && mode == "light") || (!isDark && mode == "dark")) {
"" // drop entirely; alt text would just be noise here
} else {
// Strip the fragment so the URL is clean for Coil's cache key.
val alt = match.groupValues[1]
val urlBase = match.groupValues[2]
val trailing = match.groupValues[4]
"![$alt]($urlBase$trailing)"
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Removing mismatched themed images can leave empty markdown links.

At Line 19, returning "" for mismatched variants can turn linked images into [](...) artifacts when the image is wrapped in a link.

Suggested patch
     processed = processed.replace(
         Regex("""!\[([^\]]*)\]\(([^)]*?)`#gh-`(dark|light)-mode-only([^)]*)\)"""),
     ) { match ->
@@
         }
     }
+
+    // Cleanup empty markdown links left after dropping mismatched linked images.
+    processed = processed.replace(Regex("""\[\s*\]\(([^)]+)\)"""), "")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
processed = processed.replace(
Regex("""!\[([^\]]*)\]\(([^)]*?)#gh-(dark|light)-mode-only([^)]*)\)"""),
) { match ->
val mode = match.groupValues[3]
if ((isDark && mode == "light") || (!isDark && mode == "dark")) {
"" // drop entirely; alt text would just be noise here
} else {
// Strip the fragment so the URL is clean for Coil's cache key.
val alt = match.groupValues[1]
val urlBase = match.groupValues[2]
val trailing = match.groupValues[4]
"![$alt]($urlBase$trailing)"
}
}
processed = processed.replace(
Regex("""!\[([^\]]*)\]\(([^)]*?)`#gh-`(dark|light)-mode-only([^)]*)\)"""),
) { match ->
val mode = match.groupValues[3]
if ((isDark && mode == "light") || (!isDark && mode == "dark")) {
"" // drop entirely; alt text would just be noise here
} else {
// Strip the fragment so the URL is clean for Coil's cache key.
val alt = match.groupValues[1]
val urlBase = match.groupValues[2]
val trailing = match.groupValues[4]
"![$alt]($urlBase$trailing)"
}
}
// Cleanup empty markdown links left after dropping mismatched linked images.
processed = processed.replace(Regex("""\[\s*\]\(([^)]+)\)"""), "")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/applyThemeAwareImages.kt`
around lines 14 - 27, The current replacement returns "" for mismatched themed
images which can leave a leftover linking wrapper like [![](...)](...); update
the replacement logic in applyThemeAwareImages (the processed.replace call and
its regex/replacement) to detect and remove an optional surrounding markdown
link when dropping an inner image: either extend the Regex to optionally capture
a surrounding "[ ... ](...)" wrapper and return an empty string for the whole
wrapper, or in the lambda inspect characters/substring around the matched span
(using processed and match.range) to remove the enclosing "[...](...)" if
present; ensure you still strip the fragment and rebuild the image URL for kept
matches (using match.groupValues[1..4] / the same alt/urlBase/trailing
variables).

@rainxchzed rainxchzed force-pushed the feat/markdown-image-fixes branch from c7b9259 to f6b9e0e Compare May 16, 2026 15:37
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rainxchzed has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@rainxchzed rainxchzed merged commit d0b40c2 into main May 16, 2026
1 check was pending
@rainxchzed rainxchzed deleted the feat/markdown-image-fixes branch May 16, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant