Skip to content

Render block-level images with AsyncImage (part 2)#141

Merged
junyan72 merged 10 commits into
mainfrom
image-block-view
Jul 13, 2026
Merged

Render block-level images with AsyncImage (part 2)#141
junyan72 merged 10 commits into
mainfrom
image-block-view

Conversation

@junyan72

Copy link
Copy Markdown
Contributor

Summary

Part two of experimental Markdown image support (part one added the ImageBlockRewriter and the parse-side flag). This PR makes image-only paragraphs actually render as block-level images, loaded asynchronously with SwiftUI's AsyncImage.

  • Add an ImageData payload (resolved URL + alt text) and a new MarkdownRenderable.image case.
  • Promote image-only paragraphs to .image during pre-render (Paragraph.convert), gated on a new experimental MarkdownRenderConfig.imageSupport flag (defaults to false). This covers both standalone ![alt](url) lines and the image-only paragraphs produced by ImageBlockRewriter.
  • Add BlockImageView, rendered from SingleBlockView, which loads the image via AsyncImage and shows a placeholder while loading or on failure. Accessibility label uses the alt text.
  • Thread imageSupport through parse(text:config:) so that images embedded alongside text are split into their own blocks end-to-end.
  • Add ImageBlockRenderingTests covering: 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

  • AsyncImage was 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-in URLSession+NSCache loader.
  • No sizing/styling knobs yet (max width, corner radius, aspect ratio) — kept intentionally minimal.

Validation

  • swiftlint --strict on 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.
  • Full suite: the only failures are pre-existing snapshot reference mismatches (confirmed they also fail on this branch's base with my changes stashed); they are cross-machine reference-image differences unrelated to this change, which is gated off by default.

OSS readiness

  • No secrets, internal URLs, private identifiers, or product-only service names were added.
  • Public docs, fixtures, or notices were updated if behavior or dependencies changed. (new imageSupport render flag documented as experimental; defaults to false so no behavior change by default)
  • Third-party dependency changes (adds, removes, version bumps) are intentional and reviewed. (no dependency changes — uses native AsyncImage)
  • Streaming/incomplete markdown behavior remains covered by fixtures or tests.

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
junyan72 requested review from a team and Copilot July 10, 2026 23:48
Comment thread Sources/MarkdownText/Block/Paragraph+.swift Outdated
Comment thread Sources/MarkdownText/Models/MarkdownRenderConfig.swift Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 ImageData and a new MarkdownRenderable.image case, and wires it into SingleBlockView.
  • Promotes image-only paragraphs to .image during pre-render when imageSupport is enabled, and threads imageSupport through MarkdownParser.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.

Comment thread Sources/MarkdownText/UI/BlockImageView.swift Outdated
Comment thread Sources/MarkdownText/UI/BlockImageView.swift Outdated
Jun Yan and others added 9 commits July 13, 2026 11:16
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
@junyan72
junyan72 merged commit 40994aa into main Jul 13, 2026
6 checks passed
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.

2 participants