feat: gfm blockquotes - #674
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Implements GitHub-flavor “recursive container” rendering for blockquotes (including nested blockquotes and quoted block components like code blocks/tables), aligning behavior across iOS/Android and updating docs/examples accordingly.
Changes:
- Add blockquote as its own segmented block kind and render it via recursive container views (iOS + Android).
- Refactor render entry points to render sibling node lists directly (avoids synthetic document roots) and add a “blockquote text” decorator for quote content.
- Add view-free height measurement helpers for nested container measurement consistency and update docs/storybook/maestro coverage.
Reviewed changes
Copilot reviewed 46 out of 61 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/react-native-enriched-markdown/src/web/styles.ts | Adjust blockquote left padding to include gap + padding. |
| packages/react-native-enriched-markdown/ios/views/TableContainerView.m | Render table cell content via renderNodes: instead of synthetic root. |
| packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.m | Add blockquote segmentation + new blockquoteContent rendering mode. |
| packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.h | Remove old header (moved to ios/segments). |
| packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.m | Add ENRMBlockquoteSegment and rendered segment constructor. |
| packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.h | Add blockquote segment kind + storage on rendered segment. |
| packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m | Add blockquote content rendering path using a decorator block. |
| packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.h | Export ENRMRenderBlockquoteContentNodes API + docs. |
| packages/react-native-enriched-markdown/ios/segments/SegmentRenderer.h | New public header for segment renderer with blockquoteContent flag. |
| packages/react-native-enriched-markdown/ios/segments/ENRMSegmentHeightMeasurer.mm | New view-free segment height summation (incl nested blockquotes). |
| packages/react-native-enriched-markdown/ios/segments/ENRMSegmentHeightMeasurer.h | Public API + documentation for view-free segment height summation. |
| packages/react-native-enriched-markdown/ios/segments/ENRMContainerNodeView.mm | New reusable container host for vertically stacked segment views. |
| packages/react-native-enriched-markdown/ios/segments/ENRMContainerNodeView.h | Public interface for container host view. |
| packages/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.mm | New recursive blockquote container view (draw + layout + recursion). |
| packages/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.h | Public blockquote container API + measurement helpers. |
| packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m | New decorator to render quote prose with quote baseline + line height. |
| packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.h | Public interface/docs for blockquote text decorator. |
| packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m | Replace renderRoot: with renderNodes: + optional decorator. |
| packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h | Public API change: renderNodes:context:block:. |
| packages/react-native-enriched-markdown/ios/internals/ENRMViewFreeMeasurement.h | Add blockquote segment measurement in view-free measurement pass. |
| packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm | Register blockquote segment view + layout margins + render call updates. |
| packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt | Move math container into segments package and adjust imports. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt | Move into segments package and switch renderer to renderContent. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt | Move into segments package and fix context-menu imports. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockSegmentView.kt | Move into segments package. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt | Move into segments package + add blockquote kind salt. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentReconciler.kt | Move into segments package. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt | Move into segments + add blockquote rendered segment + blockquote text rendering. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt | Move into segments + add blockquote segment type and splitting. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewFactory.kt | New factory interface enabling shared container reconciliation. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt | New shared view constructors for segment kinds (root + nested reuse). |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentHeightMeasurer.kt | New view-free segment height summation utility. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/ContainerNodeView.kt | New shared container layout/reconcile implementation. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/BlockquoteContainerView.kt | New recursive blockquote container implementation + view-free height. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt | Add renderContent envelope + optional decorator; simplify renderDocument. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt | Remove Document renderer mapping; add renderNodes helper. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt | Delete no-longer-needed Document renderer. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteTextRenderer.kt | New decorator to render quote prose with quote baseline + line height. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt | Add blockquote segment measurement and update imports after package move. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt | Update BlockSegmentView import path. |
| packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt | Refactor root view to extend ContainerNodeView and support blockquote segments. |
| packages/react-native-enriched-markdown/android/src/main/baseline-prof.txt | Update baseline profile entries for moved/removed classes. |
| docs/ELEMENTS_STRUCTURE.md | Document GitHub-flavor recursive-container blockquote behavior. |
| docs/API_REFERENCE.md | Update flavor="github" docs to include block-style blockquotes. |
| apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Blockquote.stories.tsx | Add flavor control + example with quoted fenced code block. |
| .maestro/enrichedMarkdownText/flows/advanced/block_elements/blockquote_nested_code_block_combo_test.yaml | Add UI flow to capture nested quote + nested code block behavior. |
Suppressed comments (3)
packages/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.mm:1
allowFontScalingandmaxFontSizeMultiplierare hard-coded / sourced fromconfigfor blockquote children, so blockquote content can ignore the component’s props (e.g.,allowFontScaling={false}) even though the root render path respects them. Thread these settings intoENRMBlockquoteContainerView(store on the instance when created fromEnrichedMarkdown.mm) and pass them through toENRMRenderSegmentsFromAST/ view-free measurement so quoted content matches the root’s font-scaling behavior.
packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m:1- The implementation signature marks
blockas non-nullable, but the header declares it as nullable and call sites passnil. Please align nullability between.hand.m(and keep the doc “optional block” consistent) to avoid compiler warnings and make API expectations clear.
packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m:1 pushOnContext:mutatescontext.blockquoteDepthbut there is no corresponding “pop/restore” step in the decorator protocol, so callers must rely on allocating a freshRenderContexteach time to avoid state leakage. Consider adding a symmetricpopFromContext:(or capturing/restoring the previousblockquoteDepth) and invoking it fromAttributedRendererin a@try/@finallypattern (similar to the Android implementation) to make reuse ofRenderContextsafe.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
d1fe127 to
ecd4dfa
Compare
| // gates the last child's bottom margin). | ||
| static NSArray<ENRMRenderedSegment *> *ENRMRenderBlockquoteChildren(MarkdownASTNode *node, StyleConfig *config) | ||
| { | ||
| return ENRMRenderSegmentsFromAST(node, config, /*allowTrailingMargin*/ NO, /*allowFontScaling*/ YES, |
There was a problem hiding this comment.
allowFontScaling is hardcoded to YES and lineBreakStrategy to NSLineBreakStrategyNone -- blockquote content will ignore these props if the consumer overrides them. Should thread the actual values through like the root path does. Same allowFontScaling hardcoding in ENRMSegmentHeightMeasurer.mm.
| * after the margins (e.g. in BlockquoteTextRenderer.postProcess) would erase them | ||
| * and clip the first line's ascent. | ||
| */ | ||
| const val SPAN_FLAGS_LINE_HEIGHT_PRIORITY = |
There was a problem hiding this comment.
Same value as SPAN_FLAGS_CONTAINER_BACKGROUND above. Intentional semantic alias or should they have different priorities?
There was a problem hiding this comment.
Yes this is intentional, they're the same packed value but govern two independent span domains. CONTAINER_BACKGROUND orders BlockquoteSpan against other background/drawing spans, while LINE_HEIGHT_PRIORITY orders the block-wide LineHeightSpan against the per-block MarginBottomSpans. Each need max priority within its own domain, and its already made this way. Keeping the two separate flags documents distinct usage of them.
| } | ||
| private fun applyRenderedSegments( | ||
| renderedSegments: List<RenderedSegment>, | ||
| @Suppress("UNUSED_PARAMETER") style: StyleConfig, |
There was a problem hiding this comment.
Nit: style is passed from every call site but never read. Either drop it or add a comment on why it's kept.
| * same function via SegmentHeightMeasurer. Math child heights use the shared | ||
| * estimate to avoid a main-thread measure from the layout pass. | ||
| */ | ||
| fun measureBlockquoteNodeHeight( |
There was a problem hiding this comment.
Minor: splitASTIntoSegments + render runs here for measurement and again in applyBlockquoteNode for display, so each nesting level renders content twice. Not urgent but could cache the rendered segments so the view path reuses the measurement result.
There was a problem hiding this comment.
I've added a comment that can be picked uppon while we work on performance improvements
Reworks GFM blockquotes from inline spans into a recursive container view (ContainerNodeView / BlockquoteContainerView) on Android, iOS and web. Squashed for rebase onto main. Created with usage of AI tools
ecd4dfa to
060879c
Compare
GFM alerts/admonitions built on the blockquote container, parameterized by node type; shared octicon paths. Includes the #674 review fixes (iOS font-scaling/ line-break threading, dropped unused style param, measurement caching note) and the corrected NodeType count (33). Squashed for rebase onto main. Created with usage of AI tools
What/Why?
Closes #608; Closes #621.
Reworks GFM blockquotes (
flavor="github") from inline spans into a recursive container view, attempting to mirror how code blocks already work (#607).Previously a top-level blockquote was folded into a single attributed-string segment and drawn with spans so it couldn't carry per-instance state or be extended with refs/richer styling. Now each blockquote is a first-class object:
ContainerNodeView(AndroidViewGroup/ iOSENRMContainerNodeView) that reconciles a list of rendered segments into child views, stacks them vertically with per-segment margins, and reports height. Both the document root and every blockquote use it. This is done mainly to have abstraction on top ofnon-leaf AST nodesBlockquoteContainerViewsubclasses it and draws the box itself - accent bar,backgroundColor, andpadding, withborderRadius(the accent bar is clipped to the rounded box so its corners follow the radius).<blockquote>s; one left-inset parity fix so it matches the native box model.Commonmark note
The box rendering of the commonmark path is unchanged, but it is not strictly untouched:
ParagraphRendererno longer uses the tight single-newline path for blockquotes (it still does for lists). Paragraphs inside a quote now emit block margins so spacing matches the top-level document. Because the commonmark inline blockquote renderer shares the sameblockquoteDepthsignal, blockquotes rendered inline (e.g. nested inside a list item) pick up the same paragraph spacing. This is intentional - spacing is now consistent with the document.Testing
Verified with maestro tests (new test) and with example app's storybook (new stories)
Screenshots
Screen.Recording.2026-08-20.at.11.09.27.mov
Screen.Recording.2026-08-20.at.11.11.56.mov
PR Checklist