Skip to content

Proposal: image support in EnrichedMarkdownTextInput #635

Description

@szt217

Following up from #403, where @hryhoriiK97 suggested contributing this. The contributing guide asks for a discussion before implementation PRs, so here's the design I have in mind. I'd like to agree on the approach before writing code.

Why

The renderer handles ![alt](url) on both platforms, and the core parser already emits image nodes. The input drops them. On iOS, ENRMInputParser doesn't list MD_SPAN_IMG. On Android, InputParser maps NodeType.Image to nothing. The README lists inline images in the input under future plans, so this seems like the right next gap to close.

Our use case is a chat composer where custom emoji render as small inline images while you type. Inline images are the priority for us, and I'd propose keeping block images out of the first pass to keep the PRs small.

Proposed design

The input models text as formatting ranges (characters) and block ranges (lines). An image is neither. It's one atomic character (U+FFFC) that carries a url and alt text. Instead of a parallel store like #403's ENRMImageStore, I'd put that payload on the range itself, so images ride the same adjust-for-edit path as everything else and there's no second store to keep ordered.

Per area:

  • Parser. iOS: add MD_SPAN_IMG and teach the raw-to-plain mapping about substituted characters (plain text stops being a strict subsequence of the source once U+FFFC replaces the image syntax). Android: handle NodeType.Image in InputParser.walkNode, append U+FFFC, record the range.
  • Atomicity. Caret and selection behavior copied from links: the caret can't land inside an image, backspace deletes it whole, selection snaps around it.
  • Rendering. Reuse the renderer's pipeline. ENRMImageAttachment on iOS, which needs a small refactor since it takes the renderer's StyleConfig today. ImageSpan on Android, which needs the MarkdownSpan marker plus URL-keyed instance reuse so a restamp doesn't re-download. Async height resolution hooks the existing layout observer into the input's height update path.
  • Serializer. Images fit neither the paired-delimiter path nor the line-prefix path, so both serializers get a third mode: replace the U+FFFC at that position with ![alt](url).
  • Style props. markdownStyle.image and markdownStyle.inlineImage on the input, same keys as the renderer.
  • Commands. insertImage(url, alt?) following the insertLink shape. Once the parser knows images, insertText('![a](url)') works too.
  • Tests and docs. Maestro flows under .maestro/enrichedMarkdownInput with screenshot baselines for both platforms, plus INPUT.md and API_REFERENCE.md updates.

Questions before I start

  1. feat(ios): add editable markdown input with editor controller #628 adds an editable input with image attachments to the standalone iOS package. Is the RN input going to move onto that editor? If so I'd rather build against it than add this to the current stack twice.
  2. Range model: payload on the formatting range, or a separate store like feat(ios): add image support to input #403? I lean toward the former but happy to go either way.
  3. PR split: I'd do the TS spec plus iOS first, with Android right behind it. If you'd rather have platform parity in every PR, I can structure it that way instead.
  4. Is an inline-only first pass fine, with block images as a follow-up?

Plan

I'd start with the round trip and skip rendering entirely. Once the parser and serializer both understand images, setValue() then getMarkdown() should hand back ![alt](url) untouched, with a placeholder glyph standing in for the image. That's the riskiest code on both platforms, so I'd want it reviewed before building any UI on top of it.

After that, roughly in order: attachment rendering and the style props (async height included), the insertImage command with the example app and Storybook coverage, the editing behavior (atomic caret, backspace, copy and paste), and the Maestro flows, screenshots, and docs last.

Happy to split these into separate PRs so no single review gets big.

@hryhoriiK97, (and anyone else) let me know if you have any feedback. Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions