feat: add markdownLinkRenderer hook#145
Open
ajram23 wants to merge 1 commit into
Open
Conversation
Mirrors the existing markdownImageRenderer pattern so consumers can
inject a custom SwiftUI view for inline markdown links — e.g. to add a
favicon, a hover tooltip, or scheme-specific link UI.
## API
```swift
struct MyLinkRenderer: MarkdownLinkRenderer {
func makeBody(configuration: Configuration) -> some View {
HStack { faviconFor(configuration.url); configuration.label }
}
}
MarkdownView(content)
.markdownLinkRenderer(MyLinkRenderer(), forURLScheme: "https")
// or for all schemes:
.markdownLinkRenderer(MyLinkRenderer())
```
`Configuration` exposes `url: URL` and `label: AnyView`. Dispatch is by
URL scheme with `"*"` as a wildcard fallback (extends the
image-renderer's exact+lowercased-scheme match to also support a
catch-all renderer).
## Design
**Env-scoped, not singleton.** Renderers live on
`MarkdownRendererConfiguration.linkRenderers: [String: AnyMarkdownLinkRenderer]`
and participate in `Equatable` so `CmarkFirstMarkdownViewRenderer`'s
view cache invalidates when a renderer instance changes. The modifier
writes via `transformEnvironment`. No global state. Two callsites
registering different renderers for the same scheme stay isolated to
their respective subtrees — deterministic and testable.
The same global-singleton anti-pattern exists upstream in
`MarkdownImageRenders`; only fixing the link side here to keep
divergence minimal.
**Sendable required on the protocol.** Mirrors SwiftUI's `AnyShape`
pattern (`AnyShape` requires `S: Sendable` on init).
`AnyMarkdownLinkRenderer`'s init captures the generic
`renderer: D` into three stored closures (wrapped + makeBody +
isEqualTo). Without `D: Sendable`, Swift 6 region-based isolation
rejects those captures because the wrapper struct is `Sendable`
(via `@unchecked` to allow `@MainActor` function-typed properties).
Hoisting the requirement onto the protocol makes `D` automatically
`Sendable` and removes the need for a `sending` parameter modifier
that wouldn't have worked across all three capture sites anyway.
**Breaking change:** every type conforming to `MarkdownLinkRenderer`
must also conform to `Sendable`. Free for types with no stored
properties — the synthesis is implicit.
`Configuration` itself is intentionally NOT `Sendable` because
`AnyView`'s `Sendable` conformance is `@available(*, unavailable)`
per Apple. The protocol is `@preconcurrency @MainActor`, so the
config never crosses an actor boundary in practice.
## Files
- `Modifiers/MarkdownLinkRendererModifier.swift` (new) — public
`View.markdownLinkRenderer(_:forURLScheme:)` extension
- `Renderers/Node Representations/Links/Protocol/MarkdownLinkRenderer.swift`
(new) — public protocol + `Configuration` + `AnyMarkdownLinkRenderer`
type erasure
- `Configurations/MarkdownRendererConfiguration.swift` — adds
`linkRenderers: [String: AnyMarkdownLinkRenderer]` field + Equatable
contribution
- `Renderers/Cmark/CmarkNodeVisitor.swift` — `visitLink` dispatches to
custom renderer if registered for the URL scheme (or `"*"`);
otherwise unchanged. Default branch adds `.help(url)` for the
native macOS hover tooltip.
## Verification
- `swift build` — Build complete!
- Used in production by an external macOS app (`@MainActor` SwiftUI
consumer) — favicon + URL hover tooltip on inline markdown links
in an AI chat surface. Renders thousands of links per session, no
observed cache thrash or render glitches.
170 insertions, 1 deletion across 4 files.
Author
|
@LiYanan2004 Codex app does this and make for better looking links. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
markdownLinkRenderer, mirroring the existingmarkdownImageRendererpattern so consumers can inject a custom SwiftUI view for inline markdown links — e.g. favicon-prefixed links, hover tooltips, scheme-specific link UI.Motivation
MarkdownImageRendereralready lets consumers customize image rendering per URL scheme. Links have no equivalent — every consumer that wants custom link UI (favicons, tooltips, internal scheme handling) has to wrap or fork. This PR fills that symmetric gap with the same API shape.API
Configurationexposesurl: URLandlabel: AnyView. Dispatch is by URL scheme with"*"as a wildcard — extends the image-renderer's exact+lowercased-scheme match to also support a catch-all.Design notes
Env-scoped storage, not a singleton. Renderers live on
MarkdownRendererConfiguration.linkRenderers: [String: AnyMarkdownLinkRenderer]and participate inEquatablesoCmarkFirstMarkdownViewRenderer's view cache invalidates correctly when a renderer instance changes. The modifier writes viatransformEnvironment. No global state. Two callsites registering different renderers for the same scheme stay isolated to their respective subtrees.(Note: the same global-singleton anti-pattern exists in
MarkdownImageRendersupstream. Intentionally only fixing the link side here to minimize divergence — happy to follow up with a matching image-side fix in a separate PR if you'd like.)Sendable requirement on the protocol. Mirrors SwiftUI's
AnyShapepattern (AnyShaperequiresS: Sendableon init). Required for Swift 6 region-based isolation becauseAnyMarkdownLinkRenderer's init captures the genericrenderer: Dinto three stored closures.Configurationitself is intentionally NOTSendablebecauseAnyView'sSendableconformance is@available(*, unavailable)per Apple. The protocol is@preconcurrency @MainActor, so the config never crosses an actor boundary in practice.Files
Modifiers/MarkdownLinkRendererModifier.swiftView.markdownLinkRenderer(_:forURLScheme:)Renderers/Node Representations/Links/Protocol/MarkdownLinkRenderer.swiftConfiguration+AnyMarkdownLinkRendererConfigurations/MarkdownRendererConfiguration.swiftlinkRenderersfield, contributes toEquatableRenderers/Cmark/CmarkNodeVisitor.swiftvisitLinkdispatches to custom renderer if registered for the URL scheme (or"*"); default branch adds.help(url)for the native macOS hover tooltip170 insertions, 1 deletion. No changes to existing public API surface.
Verification
swift build— Build complete!@MainActorSwiftUI consumer) — favicon + URL hover tooltip on inline markdown links in an AI chat surface. Renders thousands of links per session, no observed cache thrash or render glitches.MarkdownView 3 / #132
I see #132 ("Introducing MarkdownView 3") is in draft. If v3 plans to ship a link-renderer hook with a different architecture, happy to defer this — just let me know. Otherwise this should be useful for everyone on the current 2.x line.