Repository navigation
feat(native): host React block renderers in the segmented markdown views - #1027
Merged
hryhoriiK97 merged 6 commits intoOct 22, 2026
Merged
Conversation
6 tasks done
hryhoriiK97
force-pushed
the
feat/block-renderers-native
branch
3 times, most recently
from
October 17, 2026 13:42
d42f18a to
159137a
Compare
hryhoriiK97
requested review from
eszlamczyk
and
a balanced review from Copilot
October 17, 2026 13:46
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved slot identity, layout synchronization, image refresh, and release-build JNI issues affect correctness.
8 open issues
R8 can strip JNI streaming result class and constructor · New Slot annotation order differs from collection order · New JNI modified UTF-8 breaks slot and image matching · New Measurement cache omits block slot membership · New Layout cache ignores markdown and styling changes · New Slot changes apply asynchronously and cause stale content · New Image source changes fail to invalidate segments · New Explicit sizing reuses stale slot measurements · New
What changed in this PR
Adds native hosting for React block replacements, connecting the core collector from #1026 to iOS and Android segmented markdown views. The public JavaScript API remains deferred to the next PR.
Changes:
- Adds a synchronous parser TurboModule and Fabric slot components.
- Measures and positions replacement children, including nested blocks.
- Threads image-source overrides and shared streaming rules through both hosts.
Paths are relative to packages/react-native-enriched-markdown/.
| File | Description |
|---|---|
| src/NativeEnrichedMarkdownParser.ts | Defines synchronous collector interface. |
| src/EnrichedMarkdownTextNativeComponent.ts | Adds media streaming option. |
| src/EnrichedMarkdownNativeComponent.ts | Adds image overrides and media option. |
| src/EnrichedMarkdownBlockSlotNativeComponent.ts | Defines Fabric slot component. |
| ReactNativeEnrichedMarkdown.podspec | Includes shared C++ module sources. |
| react-native.config.js | Registers slots and parser autolinking. |
| package.json | Configures module and component codegen. |
| ios/utils/StreamingMarkdownFilter.mm | Bridges shared core streaming filter. |
| ios/utils/StreamingMarkdownFilter.m | Removes duplicated filter implementation. |
| ios/utils/StreamingMarkdownFilter.h | Declares media filtering options. |
| ios/utils/ImageRequestHeaderUtils.h | Converts image-source override props. |
| ios/utils/HeightUpdateUtils.h | Preserves placements during height updates. |
| ios/utils/ENRMImageDownloader.m | Merges request headers case-insensitively. |
| ios/utils/ENRMImageDownloader.h | Declares header merge helper. |
| ios/turbomodule/EnrichedMarkdownParserProvider.mm | Instantiates parser TurboModule. |
| ios/turbomodule/EnrichedMarkdownParserProvider.h | Declares module provider. |
| ios/styles/StyleConfig.mm | Stores and copies image overrides. |
| ios/styles/StyleConfig.h | Defines image override configuration. |
| ios/slots/ENRMBlockSlotLayerView.m | Passes background touches through. |
| ios/slots/ENRMBlockSlotLayerView.h | Declares slot overlay layer. |
| ios/slots/EnrichedMarkdownBlockSlot.mm | Implements Fabric slot view. |
| ios/slots/EnrichedMarkdownBlockSlot.h | Declares slot view identity. |
| ios/segments/SegmentRenderer.m | Creates replacement slot segments. |
| ios/segments/RenderedMarkdownSegment.m | Constructs slot segment models. |
| ios/segments/RenderedMarkdownSegment.h | Defines slot segment types. |
| ios/segments/ENRMSegmentHeightMeasurer.mm | Measures slots and records origins. |
| ios/segments/ENRMSegmentHeightMeasurer.h | Extends measurement interface. |
| ios/segments/ENRMContainerNodeView.mm | Applies slot heights and margins. |
| ios/segments/ENRMBlockSlotSegmentView.m | Implements placeholders and child binding. |
| ios/segments/ENRMBlockSlotSegmentView.h | Declares placeholder and margin helpers. |
| ios/segments/ENRMBlockquoteContainerView.mm | Supports slots inside quotes. |
| ios/segments/ENRMBlockquoteContainerView.h | Extends nested measurement interface. |
| ios/renderer/ENRMImageRenderer.m | Resolves per-occurrence image overrides. |
| ios/parser/MarkdownParserBridge.mm | Annotates slots and caches parses. |
| ios/parser/ENRMMarkdownParser.mm | Forwards annotation options. |
| ios/parser/ENRMMarkdownParser.h | Exposes annotation options. |
| ios/internals/MeasurementCache.h | Includes image overrides in cache keys. |
| ios/internals/ENRMViewFreeMeasurement.h | Integrates slots into document measurement. |
| ios/internals/ENRMBlockSlotMeasurer.mm | Records measured slot frames. |
| ios/internals/ENRMBlockSlotMeasurer.h | Defines slot measurement callback. |
| ios/internals/EnrichedMarkdownTextState.h | Supports callback-based height updates. |
| ios/internals/EnrichedMarkdownTextShadowNode.h | Uses shared React header compatibility. |
| ios/internals/EnrichedMarkdownTextComponentDescriptor.h | Uses shared React header compatibility. |
| ios/internals/EnrichedMarkdownState.h | Stores slot placement state. |
| ios/internals/EnrichedMarkdownShadowNode.mm | Measures and positions slot children. |
| ios/internals/EnrichedMarkdownShadowNode.h | Declares slot layout and memoization. |
| ios/internals/EnrichedMarkdownComponentDescriptor.h | Uses shared React header compatibility. |
| ios/input/internals/EnrichedMarkdownTextInputState.h | Supports callback-based height updates. |
| ios/input/internals/EnrichedMarkdownTextInputShadowNode.h | Uses shared React header compatibility. |
| ios/input/internals/EnrichedMarkdownTextInputComponentDescriptor.h | Uses shared React header compatibility. |
| ios/EnrichedMarkdown.mm | Mounts slots and coalesces render requests. |
| ios/attachments/ENRMImageAttachment.m | Separates original and request URLs. |
| ios/attachments/ENRMImageAttachment.h | Exposes image override attachment API. |
| common/ENRMReactHeaders.h | Provides React header compatibility. |
| common/EnrichedMarkdownParser.h | Defines shared parser TurboModule. |
| common/EnrichedMarkdownParser.cpp | Filters markdown and collects blocks. |
| android/src/test/java/com/swmansion/enriched/markdown/segments/BlockSlotReconcilerTest.kt | Tests placeholder reuse and identity. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h | Preserves slot placement state. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp | Serializes placements for Kotlin. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h | Uses shared React header compatibility. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextInputShadowNode.h | Uses shared React header compatibility. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h | Declares slot layout and memoization. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp | Measures and positions slot children. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerMeasurementManager.h | Accepts slot ids during measurement. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerMeasurementManager.cpp | Passes slot ids to Kotlin. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h | Serializes image overrides and flavor. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.h | Registers slot component descriptor. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/BlockSlotMeasureSession.h | Defines native slot measurement session. |
| android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/BlockSlotMeasureSession.cpp | Measures children and records frames. |
| android/src/main/jni/parser-module/EnrichedMarkdownParserAutolink.cpp | Supplies autolinking translation unit. |
| android/src/main/jni/parser-module/CMakeLists.txt | Links parser autolinking target. |
| android/src/main/jni/EnrichedMarkdownTextSpec.h | Removes handwritten module declaration. |
| android/src/main/jni/EnrichedMarkdownTextSpec.cpp | Removes handwritten module provider. |
| android/src/main/jni/CMakeLists.txt | Compiles shared parser module. |
| android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageDownloader.kt | Bypasses HTTP cache for header-bearing requests. |
| android/src/main/java/com/swmansion/enriched/markdown/utils/common/StreamingMarkdownFilter.kt | Delegates filtering to native core. |
| android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt | Parses image-source override props. |
| android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt | Includes image overrides in configuration. |
| android/src/main/java/com/swmansion/enriched/markdown/styles/ImageSourceOverride.kt | Defines image request override model. |
| android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt | Applies overridden requests and cache identities. |
| android/src/main/java/com/swmansion/enriched/markdown/slots/EnrichedMarkdownBlockSlotView.kt | Hosts React replacement children. |
| android/src/main/java/com/swmansion/enriched/markdown/slots/EnrichedMarkdownBlockSlotManager.kt | Exposes slot identity and view props. |
| android/src/main/java/com/swmansion/enriched/markdown/slots/BlockSlotLayer.kt | Separates slots from native segments. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt | Creates and updates slot placeholders. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentSignature.kt | Adds slot signature salt. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentHeightMeasurer.kt | Shares root and nested measurement. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt | Adds rendered slot model. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/MarkdownSegment.kt | Splits annotated blocks into slots. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/ContainerNodeView.kt | Propagates heights and offsets segment indices. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/BlockSlotSegmentView.kt | Reserves replacement height and margins. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/BlockSlotMeasurer.kt | Defines slot measurement callback. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/BlockSlotKind.kt | Maps slot kinds to margins. |
| android/src/main/java/com/swmansion/enriched/markdown/segments/BlockquoteContainerView.kt | Supports nested slot placement. |
| android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt | Passes image overrides to spans. |
| android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt | Forwards image occurrence ids. |
| android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt | Adds annotation options and parse memoization. |
| android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt | Integrates slot measurement and cache inputs. |
| android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextPackage.kt | Registers slot view manager. |
| android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt | Mounts children and forwards slot state. |
| android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt | Hosts slots and synchronizes topology changes. |
| android/src/main/cpp/jni-adapter.cpp | Bridges annotation, filtering, and slot measurement. |
| tests/ios/BlockSlotStackingTests.mm | Tests placement, frames, and margins. |
Review effort: Balanced
hryhoriiK97
force-pushed
the
feat/block-renderers-native
branch
from
October 17, 2026 14:01
159137a to
d6d78a4
Compare
hryhoriiK97
force-pushed
the
feat/block-renderers-native
branch
from
October 17, 2026 19:00
f531612 to
844c3b6
Compare
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
2 open issues
8 resolved since last review
Slot annotation order differs from collection order R8 can strip JNI streaming result class and constructor Explicit sizing reuses stale slot measurements Image source changes fail to invalidate segments Slot changes apply asynchronously and cause stale content Layout cache ignores markdown and styling changes Measurement cache omits block slot membership JNI modified UTF-8 breaks slot and image matching
Review effort: Balanced
hryhoriiK97
force-pushed
the
feat/block-renderers-native
branch
from
October 18, 2026 17:18
e1df478 to
f983fcd
Compare
3 of 6 tasks
eszlamczyk
reviewed
Oct 19, 2026
hryhoriiK97
force-pushed
the
feat/block-renderers-native
branch
from
October 19, 2026 10:54
f983fcd to
66c71f0
Compare
eszlamczyk
reviewed
Oct 20, 2026
eszlamczyk
approved these changes
Oct 22, 2026
The iOS and Android GFM hosts can now reserve space for, and position, React
children that stand in for image paragraphs, video blocks and link-only
paragraphs. Inert until the JS layer sends slot children.
- A pure C++ TurboModule (EnrichedMarkdownParser, codegen type "all") gives JS
the core's collector synchronously, with the streaming filter applied first,
so slot children land in the same commit as the markdown.
- The host shadow nodes measure each slot child with Yoga inside their own
measure, lay the document out with placeholders, position the children in
layout() (the ParagraphShadowNode pattern) and publish {id, height, placed}
through state. The last slot measurement is memoized per node and
invalidated on a children change.
- Placeholders reserve the pixel-rounded child height with the margins of the
block they replace; a pass-through slot layer hosts the Fabric children.
- imageSources (per-occurrence request URL and headers) flows into the image
renderers and the measurement cache key.
- streamingConfig.mediaMode, threaded to both hosts' filters.
- iOS renders once per mount transaction in finalizeUpdates; the parser
bridge memoizes the last parses so a streaming tick parses once.
- Android: one height walk for the root stack and nested quotes, margins
rounded like the views, synchronous render when slot ids change, density
in the measurement cache key, a ReactViewManager for the slot child, and
the slot layer drawn last by drawing order.
- common/ENRMReactHeaders.h reaches React Native through its umbrella
headers where they exist (0.88 strict C++ API) and falls back otherwise.
Tests: BlockSlotReconcilerTest (Android) and BlockSlotStackingTests (iOS).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
…ment Review follow-ups on the block slot hosts: - Keep StreamingFilterResult for R8 and clear the pending JNI exception when the class cannot be built, so a minified build gets null, not a throw. - Annotate slots before text-link recognition on both platforms; the JS collector never runs recognition, so both now count the same slots. - The Android split-measure cache key carries the slot ids: a size measured with React children never serves the slot-free document. - Both layout() overrides re-measure when the props or height counter changed under an explicitly sized host where Yoga skipped measureContent. - iOS renders synchronously when the slot ids change, in the mount transaction that places the React child, as the Android host does. - Image source overrides join the text and blockquote segment signatures, so a reused segment never keeps the previous source's attachment. Co-Authored-By: Claude Fable 5.1 <[email protected]>
The streaming config travels with the measurement props (conversions.h) instead of a per-view map the manager filled after the first measurement, so the first layout and a layout after a mode change withhold exactly what the view withholds, for tables, code blocks and media alike. On iOS the table segment signature carries the image source overrides too, as the text and blockquote signatures do. Co-Authored-By: Claude Fable 5.1 <[email protected]>
A clone with new children, which is every frame of an animation running inside a slot, dropped the slot measurement and re-measured the whole document on the UI thread. The clone now only asks the next pass to re-measure the slot children through Yoga, and the measurement is kept when their heights match. In the example app's streaming journal the Android emulator went from 98% janky frames to 4.7%. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…and rounding The hosts filter and parse through the core DocumentStore, so the view and the shadow node reuse the document JS collect already parsed. Slot ids, image ids and recognized links go on during conversion (AstOverlay) instead of being written into the shared tree. JNI strings cross as UTF-16 so ids hash the same bytes as on the JS side. Android: the slot layer is now the last child instead of being drawn last, since TouchTargetHelper ignores drawing order and a Pressable in a slot never got its press. TalkBack reads each slot child where its placeholder is. Slot heights and frames round instead of ceil: they are already whole pixels, and ceil turned float error into an extra pixel at 2.625 or 3.5. Only placed slot children replace their block, so a failed split measurement keeps the native blocks instead of dropping them. iOS: the shadow node reuses its slot measurement across widths within a pixel, a recycled view forgets a pending synchronous render, and segments go beside the slot layer when the view wraps its subviews in a container. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A blockquote is long-clickable for its context menu, which makes it important for accessibility, so the host added it as itself and the framework listed its children the default way: the placeholder is hidden and the slot view lives in the host's slot layer, so the renderer was never reached. Every container now substitutes slot views when it lists its own children, finding them through the host. Co-Authored-By: Claude Opus 5.5 <[email protected]>
eszlamczyk
force-pushed
the
feat/block-renderers-native
branch
from
October 22, 2026 10:56
a7003c4 to
2b1f082
Compare
hryhoriiK97
added a commit
that referenced
this pull request
Oct 22, 2026
…#1026) * feat(core): collect replaceable blocks and share the streaming filter Adds the C++ side of custom block renderers for EnrichedMarkdownText: - BlockKinds: one table per replaceable kind (image paragraph, video block, link-only paragraph) with its node matcher, naming and streaming line rule. The collector, the annotator and the streaming filter iterate it, so a new kind is one entry here plus a margin entry per host. - BlockSlots: collects slots and image occurrences in document order with stable ids (`<kind>:<fnv1a64 of url>:<n>`, slots and occurrences counted separately) and stamps the active ones on the AST in one walk. Nothing inside a list item is a slot, since the hosts lay lists out as text. - StreamingFilter: the math, table, code fence and media rules iOS, Android and the JS collector all shared by copy now live once. Trailing image, link and video lines are withheld while their syntax is open (hidden mode waits for the next block), a bare URL stays withheld until the next line, and an inline image or link still being typed at the tail of the last paragraph is cut so it never flashes as text. - The parser keeps link titles, so a link block can carry metadata. Behaviour changes for existing streaming users: a trailing partial `` links now carry their title in the AST. Tests: block_slots_test (eligibility, id stability, annotation, the hash fixture shared with the web collector) and streaming_filter_test. Co-Authored-By: Claude Fable 5.1 <[email protected]> * chore(web): rebuild the wasm parser bundle The parser now keeps link titles, so the committed bundle has to be rebuilt for scripts/test-web-bundle.mjs to agree with the golden AST dump. Co-Authored-By: Claude Fable 5.1 <[email protected]> * ci: run the pull request checks for PRs stacked on feat/block-renderers-* The CI workflow only triggers for pull requests into main, so the upper PRs of the block renderers stack (#1027 onto #1026's branch, #1028 onto #1027's) got no checks. Temporary; remove once the stack has merged. Co-Authored-By: Claude Fable 5.1 <[email protected]> * fix(core): read inline link syntax the way CommonMark does while streaming Review findings on the streaming classifier, all with regression cases: - a closed bracket followed by prose (`[INFO] done`) is prose, not an open link, so the line is no longer withheld; - a link destination closes at its balanced `)`, so nested parentheses and titles with parentheses classify correctly in both the line classifier and the inline-tail scanner, which now share one reader; - a closed autolink followed by text is prose, not a block; - four or more spaces of indentation make the line indented code, which no classifier matches and the tail scanner leaves alone; - an escaped bracket or bang is literal text; - code spans are matched by backtick-run length, so a double-backtick span keeps its brackets. Co-Authored-By: Claude Fable 5.1 <[email protected]> * fix(core): read nested labels and link titles while streaming The inline syntax helpers now follow CommonMark further: - A label closes at its balanced `]`, skipping escapes and code spans, so `[](v)` and `[a [b]](u)` classify as link paragraphs and an outer link still typing around a closed image is cut, not shown as text. - A destination may be `<pointy>` and may carry a quoted or parenthesised title; parentheses inside the title no longer count as delimiters, so `[a](u "t(")` is complete and `[a](u "t )` stays open. - The tail scanner walks left over the `[` candidates until one is open. Co-Authored-By: Claude Fable 5.1 <[email protected]> * fix(core): withhold a block line cut off inside its opening syntax A stream that stopped at `<`, `<vid`, `<htt` or a lone `!` showed those characters as text for a tick before the line became a video, link or image block. A strict prefix of `<video`, `<http` or `![` on the trailing line is now open syntax, like a half-typed URL. Co-Authored-By: Claude Opus 5.5 <[email protected]> * perf(core): filter the stream in one pass and share one parse per document The streaming filter split the document into copied lines up to three times per tick, and the open-inline-syntax check rescanned the last line for every `[`, which made a long single-line paragraph cost links x line length. It now scans blocks once over string_view lines and makes one inline pass over the trailing paragraph, so a label that wraps onto a second line is cut too. A fuzz test checks every streamed prefix against md4c and a bench fails if a pathological tail gets much slower than prose. DocumentStore keeps the last filtered and parsed documents (16 entries, 8 MB) so JS collect, the shadow node and the view share one filter and one parse per tick. The parsed tree is never written to after the parse. Block ids now hash kind, url, text and title, and image occurrences use their own `img:` prefix, so a block id can no longer equal an occurrence id. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(core): start the trailing paragraph after blocks that end it The inline-syntax pass walked back to the last blank line, so a stray `[` in a heading, a setext heading or a quote above the trailing paragraph withheld everything after it. The walk now stops at headings, underlines, thematic breaks, deeper quotes and HTML blocks. A `](` inside a destination already found to be prose no longer rescans it, which kept `[x](` repeated linear. Both shapes are in the bench. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Fable 5.1 <[email protected]> Co-authored-by: Ernest <[email protected]>
This was referenced Oct 24, 2026
This was referenced Oct 25, 2026
4 of 12 tasks
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.


What/Why?
Second of three stacked PRs for custom block renderers. The iOS and Android GFM hosts can now reserve space for, and position, React children that stand in for image paragraphs,
<video>blocks and link-only paragraphs. Still inert for apps: no JS sends slot children until the next PR.Stack: #1026 (core) → this PR → JS API, docs and example.
EnrichedMarkdownParser(codegentype: "all",ios.modulesProvider, AndroidcxxModule*autolinking), gives JS the core's collector synchronously, with the streaming filter applied first, so slot children land in the same commit as the markdown they belong to.layout()(theParagraphShadowNodepattern:cloneTree,layoutTree,setLayoutMetrics,DisplayType::Nonefor unplaced slots) and publish{id, height, placed}through state. The view-initiated height update uses the callback form ofupdateState, so the two writers never clobber each other. The last slot measurement is memoized per node and invalidated on a children change, so Yoga's repeated callbacks and state-only commits do not re-parse.imageSources(per-occurrence request URL and headers fromresolveImageSource) reaches the image renderers, the downloader cache identity and the measurement cache key.streamingConfig.mediaModethreaded to both hosts.finalizeUpdates(props and slot ids coalesce). The parser bridge memoizes the last three parses, so a streaming tick parses once instead of three times.ReactViewManagerfor the slot child (all view props), the slot layer drawn last by drawing order instead of being reordered every render, and the same parse memo.common/ENRMReactHeaders.hreaches React Native through<React/View.h>,<React/RendererCore.h>and<React/Debug.h>where they exist (the 0.88 strict C++ API) and falls back to the fine-grained headers otherwise.android/src/main/jni/EnrichedMarkdownTextSpec.{cpp,h}is removed; codegen generates it now.Known limits, listed in the follow-ups: the arriving block lands at full height in one frame (height animation is a separate PR), the iOS slot layer bypasses
RCTViewComponentView's clipping bookkeeping.Testing
BlockSlotReconcilerTest(Android, Robolectric): a reused placeholder takes the new slot id.BlockSlotStackingTests(iOS XCTest through the example app's test target): placement, recorded frames, per-kind margins.qa-renderers-streamingflow passed twice on each platform.yarn lint-clang,yarn lint-kotlin, Android unit tests (35).PR Checklist
🤖 Generated with Claude Code