You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
iOS EnrichedMarkdownTextInput: getMarkdown()/setValue() round-trip loses content (unmatched "[" drops blocks, escapes and leading block syntax lost) #1127
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)
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:
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).
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?
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?
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
Activity
wikil commentedon Nov 3, 2026
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→setValuethe result again →getMarkdown+ style query at the affected text.setValuegetMarkdown()字面 \*不是斜体\* 结束字面 *不是斜体* 结束italic.isActive = trueon both platforms\# 不是标题# 不是标题heading.isActive = trueon both platforms## 版本 \### 版本 ### 版本(closing sequence stripped); iOS keeps the#as text字面 \[资料](https://example.org) 结束字面 [资料](https://example.org) 结束The first two were repeated twice per platform. Disabling
markdownShortcutsand 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# - + = >and1./1)markers, trailing#runs,!ahead of a link — andwriteMarkdownin the webMarkdownSerializer.tsapplies them while copying text between delimiters.ENRMMarkdownSerializer.mmandMarkdownSerializer.ktcopy 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
markdownEscapePositionsto 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?
JaydeepAgravat commentedon Nov 8, 2026
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, andENRMInputParser.mmthen 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'sInputParser.kthas the same problem too.Does that sound OK, or would you rather handle the parser side yourselves?
jnowakow commentedon Nov 10, 2026
Hello all! Thanks for the bug report. I think that "
getMarkdown(setValue(getMarkdown()))equalsgetMarkdown()" 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