Skip to content

iOS EnrichedMarkdownTextInput: getMarkdown()/setValue() round-trip loses content (unmatched "[" drops blocks, escapes and leading block syntax lost) #1127

Open

Description

@waltermvp

Summary

On iOS, markdown returned by EnrichedMarkdownTextInput's getMarkdown() doesn't survive setValue() → getMarkdown(). In the worst case, an unmatched [ causes list and heading blocks to be dropped, and in a list the loss repeats on every round-trip. Reproduces on 1.1.1 and current main (5cd1c6a). Android not checked; it uses a separate parser (InputParser.kt).

1. An unmatched [ drops list and heading blocks (cascades in lists)

setValue("## Title [wip")      → getMarkdown() = "Title [wip"
setValue("- a\n- b\n- c [")    → "- a\n- b\nc ["   → "- a\nb\nc ["   → "a\nb\nc ["

Remend appends a closing ] (ENRMInputRemend.mm:14). md4c then treats […] as literal, so the last block's content range runs past the original string. That block is skipped at ENRMInputParser.mm:627 (contentEnd > rawLength → continue), and the now-unprefixed line merges into the item above. Possibly related: #799, #1014.

2. Backslash escapes are lost one way

ENRMInputParser.mm:321-323, :552 strips escapes (since #798), and ENRMMarkdownSerializer.mm never writes any. So a \\( b → a \( b → a ( b, and 2 \*x\* 6 stays the same text but turns italic. Related: #792, #798.

3. Block syntax at the start of item content is lost or reinterpreted

The block-marker strip (ENRMInputParser.mm:534-550) removes everything up to the first text byte, and the serializer doesn't escape literal block syntax in content:

"- \> x"            → "- > x"  → "- x"
"- \+ x"            → "- + x"  → "- - x"     (becomes a nested list)
"## `code` here"    → "## code` here"

Related: #896.

4. Leading whitespace in list or heading content collapses (minor)

Text x typed at the start of a list item serializes as 1. x, which re-parses as 1. x. Same for ## x.

Expected

getMarkdown(setValue(getMarkdown())) equals getMarkdown(): no blocks dropped, escapes preserved, and literal block syntax in content escaped on output.

Environment

  • react-native-enriched-markdown 1.1.1
  • React Native 0.85.3, New Architecture (Fabric), Expo SDK 56
  • iOS 26.4.1 simulator (iPhone 17 Pro)

Confirmed by running the 1.1.1 iOS parser/serializer sources directly, and by a seeded fuzz of edit sequences on device (25/25 cases reach a fixed point; 2/25 plain-text and 11/25 markdown-heavy cases change on the first round-trip).

Activity

  1. wikil commented on Nov 3, 2026

    @wikil

    Confirming items 2 and 3 on Android as well, plus a note on where a fix could come from.

    Android reproduction

    react-native-enriched-markdown@1.1.1, RN 0.86.3 / Expo 57 / React 19.2.3, HONOR VER-AN00 (Android 16, arm64 device), and the same runs on an iOS 26.3 simulator (iPhone 17e). Driven through the native API: setValue → getMarkdown → setValue the result again → getMarkdown + style query at the affected text.

    setValue first getMarkdown() second round
    字面 \*不是斜体\* 结束 字面 *不是斜体* 结束 same text, italic.isActive = true on both platforms
    \# 不是标题 # 不是标题 same text, heading.isActive = true on both platforms
    ## 版本 \# ## 版本 # Android: ## 版本 (closing sequence stripped); iOS keeps the # as text
    字面 \[资料](https://example.org) 结束 字面 [资料](https://example.org) 结束 escape lost the same way (not probed further)

    The first two were repeated twice per platform. Disabling markdownShortcuts and hiding the formatting menu make no difference, which is expected: the loss is on the serialize side. Both platforms import through md4c (ENRMInputParser.mm; InputParser.kt → Parser.shared.parseMarkdown), so the same escapes are needed on both.

    The escaping already exists in the web input

    src/web/input/formatting/markdownEscape.ts (markdownEscapePositions, from #965) computes the backslash positions — inline \ * _ ~ | [ ] < and entity-starting &, line-leading # - + = > and 1. / 1) markers, trailing # runs, ! ahead of a link — and writeMarkdown in the web MarkdownSerializer.ts applies them while copying text between delimiters. ENRMMarkdownSerializer.mm and MarkdownSerializer.kt copy the text segments verbatim instead, so the three serializers disagree on the same input model.

    Offer

    I'd be happy to open a PR that ports markdownEscapePositions to Objective-C++ and Kotlin and applies it at the text-copy point of both native serializers, with the web round-trip cases mirrored as XCTest (__tests__/ios) and Robolectric (android/src/test) tests. Scope would be items 2 and 3 of this issue (output escaping). Item 1 (unmatched [ dropping blocks) and item 4 are parser-side and I'd leave them out.

    One question before starting: #1109 changes the same two serializer functions (per-line range splitting). Would you prefer the port based on that branch, or should it wait for #1109 to land and rebase on top?

  2. added
    domain: inputEnrichedMarkdownTextInput component
    library: react-nativeTasks that are specifically for react-native-enriched-markdown
    bugSomething isn't working
    on Nov 4, 2026
  3. JaydeepAgravat commented on Nov 8, 2026

    @JaydeepAgravat

    Hi! Since @wikil is taking the output escaping (items 2 and 3), I'd like to take item 1, the unmatched [ that drops list and heading blocks on iOS.

    From the write-up, the closing ] that Remend appends makes md4c's content range run past the raw string, and ENRMInputParser.mm then skips the block (contentEnd > rawLength). I'd look at clamping or remapping that range to the original string instead of skipping the block, and add the cases from the issue (## Title [wip, - a\n- b\n- c [) as round-trip tests. I'll check whether Android's InputParser.kt has the same problem too.

    Does that sound OK, or would you rather handle the parser side yourselves?

  4. self-assigned this
    on Nov 10, 2026
  5. jnowakow commented on Nov 10, 2026

    @jnowakow
    Collaborator

    Hello all! Thanks for the bug report. I think that "getMarkdown(setValue(getMarkdown())) equals getMarkdown()" should be true on all platform. I think that the correct solution is to implement one shared input parser in C++. It would match the library philosophy and would be close to what was proposed in #1014.
    @wikil, @JaydeepAgravat thanks for readiness to help but I'll handle this one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain: inputEnrichedMarkdownTextInput componentlibrary: react-nativeTasks that are specifically for react-native-enriched-markdownpriority: medium

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions