Skip to content

feat: gfm blockquotes - #674

Open
eszlamczyk wants to merge 1 commit into
mainfrom
feat/608-gfm-blockquotes
Open

feat: gfm blockquotes#674
eszlamczyk wants to merge 1 commit into
mainfrom
feat/608-gfm-blockquotes

Conversation

@eszlamczyk

@eszlamczyk eszlamczyk commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

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:

  • New reusable ContainerNodeView (Android ViewGroup / iOS ENRMContainerNodeView) 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 of non-leaf AST nodes
  • BlockquoteContainerView subclasses it and draws the box itself - accent bar, backgroundColor, and padding, with borderRadius (the accent bar is clipped to the rounded box so its corners follow the radius).
  • A blockquote splits its own content into segments recursively, so a code block, table, or nested quote inside a quote becomes a real nested block component - not inline text. This is what enables future styling extension and gives real box padding on all sides (fixes the Blockquotes "padding right" does not apply on Android #621 right-padding gap).
  • Inner text is rendered as blockquote content (quote font/color/line-height). Paragraphs inside a quote now emit block margins so spacing between blocks matches the top-level document, instead of the old tight single-newline path shared with lists.
  • Nested quotes carry no extra vertical margin (only the outermost quote does), matching commonmark.
  • Web already rendered recursive <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: ParagraphRenderer no 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 same blockquoteDepth signal, 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

iOS Android
Commonmark image image
GFM image image
nested GFM image image
GFM with other block elements
Screen.Recording.2026-08-20.at.11.09.27.mov
Screen.Recording.2026-08-20.at.11.11.56.mov

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

@eszlamczyk eszlamczyk changed the title Feat/608 gfm blockquotes feat: gfm blockquotes Aug 17, 2026
@eszlamczyk
eszlamczyk requested a lite review from Copilot August 18, 2026 15:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • allowFontScaling and maxFontSizeMultiplier are hard-coded / sourced from config for 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 into ENRMBlockquoteContainerView (store on the instance when created from EnrichedMarkdown.mm) and pass them through to ENRMRenderSegmentsFromAST / 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 block as non-nullable, but the header declares it as nullable and call sites pass nil. Please align nullability between .h and .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: mutates context.blockquoteDepth but there is no corresponding “pop/restore” step in the decorator protocol, so callers must rely on allocating a fresh RenderContext each time to avoid state leakage. Consider adding a symmetric popFromContext: (or capturing/restoring the previous blockquoteDepth) and invoking it from AttributedRenderer in a @try/@finally pattern (similar to the Android implementation) to make reuse of RenderContext safe.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@eszlamczyk
eszlamczyk requested a review from hryhoriiK97 August 19, 2026 09:13
@eszlamczyk
eszlamczyk marked this pull request as ready for review August 19, 2026 09:13
@eszlamczyk
eszlamczyk force-pushed the feat/608-gfm-blockquotes branch from d1fe127 to ecd4dfa Compare August 20, 2026 09:21
// gates the last child's bottom margin).
static NSArray<ENRMRenderedSegment *> *ENRMRenderBlockquoteChildren(MarkdownASTNode *node, StyleConfig *config)
{
return ENRMRenderSegmentsFromAST(node, config, /*allowTrailingMargin*/ NO, /*allowFontScaling*/ YES,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same value as SPAN_FLAGS_CONTAINER_BACKGROUND above. Intentional semantic alias or should they have different priorities?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@eszlamczyk
eszlamczyk force-pushed the feat/608-gfm-blockquotes branch from ecd4dfa to 060879c Compare August 31, 2026 06:07
eszlamczyk added a commit that referenced this pull request Aug 31, 2026
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
@eszlamczyk
eszlamczyk requested a review from hryhoriiK97 August 31, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blockquotes "padding right" does not apply on Android Rework the way blockquotes are handled for GFM

3 participants