Render block-level images with AsyncImage (part 2)#141
Merged
Conversation
Part two of Markdown image support (#89): render image-only paragraphs as block-level images. - Add an ImageData payload and a new MarkdownRenderable.image case. - Promote image-only paragraphs to .image during pre-render, gated on a new experimental MarkdownRenderConfig.imageSupport flag (default false). - Add BlockImageView, which loads the image asynchronously via AsyncImage and shows a placeholder while loading or on failure. - Thread imageSupport through parse(text:config:) so images embedded in text are split into their own blocks by ImageBlockRewriter end-to-end. - Add ImageBlockRenderingTests covering disabled, standalone-enabled, and mixed-with-text cases. Refs #89 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
junyan72
commented
Jul 10, 2026
junyan72
commented
Jul 10, 2026
There was a problem hiding this comment.
Pull request overview
This PR continues the experimental Markdown image work by promoting image-only paragraphs into a new block-level renderable and rendering them via SwiftUI AsyncImage, gated behind a new MarkdownRenderConfig.imageSupport flag (default false) to avoid behavior changes by default.
Changes:
- Introduces
ImageDataand a newMarkdownRenderable.imagecase, and wires it intoSingleBlockView. - Promotes image-only paragraphs to
.imageduring pre-render whenimageSupportis enabled, and threadsimageSupportthroughMarkdownParser.parse(text:config:). - Adds
BlockImageView(AsyncImage + placeholder + a11y label) and new unit tests covering enabled/disabled/mixed-text behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| Tests/MarkdownTextTests/ImageBlockRenderingTests.swift | Adds unit tests for block-level image rendering behavior under the feature flag. |
| Sources/MarkdownText/UI/BlockView.swift | Renders the new .image block via BlockImageView. |
| Sources/MarkdownText/UI/BlockImageView.swift | Implements block-level image rendering using AsyncImage with placeholder + accessibility label. |
| Sources/MarkdownText/Parser/MarkdownParser.swift | Threads config.imageSupport into the parse option for end-to-end behavior. |
| Sources/MarkdownText/Models/RenderableDocument.swift | Includes .image blocks in plainText extraction (alt text when present). |
| Sources/MarkdownText/Models/MarkdownRenderConfig.swift | Adds imageSupport render flag (default false) and documents its experimental nature. |
| Sources/MarkdownText/Models/MarkdownRenderable.swift | Adds the .image renderable case and integrates it into id handling. |
| Sources/MarkdownText/Models/ImageData.swift | Adds the ImageData payload (URL + alt), using mixed-encoding URL parsing. |
| Sources/MarkdownText/Block/Paragraph+.swift | Promotes image-only paragraphs to .image when config.imageSupport is enabled. |
junyan72
commented
Jul 10, 2026
Size block-level images relative to their container using containerRelativeFrame (iOS 17 / macOS 14+), via a new containerRelativeFrameCompat helper that no-ops on older OSes. Also add a withImageSupport(enabled:) render-config builder and enable image support in the sample demo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17e2748f-66c1-4deb-b557-df65a17c84f5
Drop the auto-extracted "Image" string stub; leave the catalog untouched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17e2748f-66c1-4deb-b557-df65a17c84f5
Address PR review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17e2748f-66c1-4deb-b557-df65a17c84f5
Address PR review feedback to make image support a struct for future extensibility. `MarkdownRenderConfig.imageSupport: Bool` becomes `imageConfig: ImageConfig`, which carries an `allowedImageTypes` list (`.remote(allowedDomains:)`). BlockImageView now renders the placeholder for images whose URL is not permitted (disabled, missing, or a remote host outside the allowlist). Note: used "allowedDomains" rather than "whitelist" to satisfy the repo's SwiftLint inclusive_language rule. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17e2748f-66c1-4deb-b557-df65a17c84f5
Move the allowlist check out of BlockImageView's body: ImageData now computes isDomainAllowed once during pre-rendering (in Paragraph.convert, where the config is available) instead of reading the config from the environment and evaluating eligibility on every view update. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17e2748f-66c1-4deb-b557-df65a17c84f5
Address PR review feedback: use a shimmer block for the image loading (.empty) state instead of the static photo icon. Adds the markiv/SwiftUI-Shimmer dependency (pinned to 1.5.1) and applies .shimmering() to a rounded placeholder. Failure and disallowed-domain states keep the photo icon. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17e2748f-66c1-4deb-b557-df65a17c84f5
containerRelativeFrame(.horizontal) sizes relative to the nearest container (scroll view / window), not the text column, so 2/3 of the full macOS window rendered nearly full-width. Measure the actual available width via GeometryReader (onWidthChange) and constrain the image to 2/3 of it, giving a true two-thirds block on every platform. Removes the now-unused containerRelativeFrameCompat helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17e2748f-66c1-4deb-b557-df65a17c84f5
Drop the 2/3 width measurement and just use maxWidth: .infinity so the image fills the text column the app has already constrained via its own content max-width / padding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17e2748f-66c1-4deb-b557-df65a17c84f5
Replace the hard-coded "Image" VoiceOver label with a localized String.imageLabel constant (key a11y_image), following the existing String+.swift / Localizable.xcstrings convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17e2748f-66c1-4deb-b557-df65a17c84f5
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
Part two of experimental Markdown image support (part one added the
ImageBlockRewriterand the parse-side flag). This PR makes image-only paragraphs actually render as block-level images, loaded asynchronously with SwiftUI'sAsyncImage.ImageDatapayload (resolvedURL+ alt text) and a newMarkdownRenderable.imagecase..imageduring pre-render (Paragraph.convert), gated on a new experimentalMarkdownRenderConfig.imageSupportflag (defaults tofalse). This covers both standalonelines and the image-only paragraphs produced byImageBlockRewriter.BlockImageView, rendered fromSingleBlockView, which loads the image viaAsyncImageand shows a placeholder while loading or on failure. Accessibility label uses the alt text.imageSupportthroughparse(text:config:)so that images embedded alongside text are split into their own blocks end-to-end.ImageBlockRenderingTestscovering: disabled (stays a paragraph), standalone-enabled (renders as an image block), and mixed-with-text (split into paragraph/image/paragraph).Refs #89
Notes / follow-ups
AsyncImagewas chosen for a zero-dependency default. Its cache scope is the view, so identity churn during streaming (paragraph splitting, speculative rewrites) can cause a placeholder flash; a future part may add an injectable image-provider abstraction so consumers can plug in a caching loader (SDWebImage/Kingfisher/Nuke) or a built-inURLSession+NSCacheloader.Validation
swiftlint --stricton all changed files — 0 violations.xcodebuild build -scheme SwiftStreamingMarkdown -destination "platform=macOS" -skipMacroValidation— succeeds.xcodebuild test ... -only-testing:SwiftStreamingMarkdownTests/ImageBlockRenderingTests -only-testing:SwiftStreamingMarkdownTests/ImageBlockRewriterTests -only-testing:SwiftStreamingMarkdownTests/MarkdownParserTests— 11/11 pass.OSS readiness
imageSupportrender flag documented as experimental; defaults tofalseso no behavior change by default)AsyncImage)